INT-4117: File Outbound Set File Permissions

JIRA: https://jira.spring.io/browse/INT-4117

Add an option to set file permissions on the `FileWritingMessageHandler` when the file system supports it.

Polishing - PR Comments; more robust file system detection.

Add DSL support and fix NPE with missing timestamp header.

Fix javadoc

Fix javadoc

Some polishing:

* Add `chmod >= 0` assertion
* Make some assertion with string concatenation as `Supplier`-based to
deffer that string concatenation
This commit is contained in:
Gary Russell
2017-05-22 21:00:31 -04:00
committed by Artem Bilan
parent c8aafde20b
commit 2b99c191f6
13 changed files with 234 additions and 23 deletions

View File

@@ -29,10 +29,14 @@ import java.io.OutputStreamWriter;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.PosixFilePermission;
import java.util.BitSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.locks.Lock;
import java.util.regex.Matcher;
@@ -48,6 +52,7 @@ import org.springframework.expression.common.LiteralExpression;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.integration.expression.ExpressionUtils;
import org.springframework.integration.file.support.FileExistsMode;
import org.springframework.integration.file.support.FileUtils;
import org.springframework.integration.handler.AbstractReplyProducingMessageHandler;
import org.springframework.integration.handler.MessageTriggerAction;
import org.springframework.integration.support.locks.DefaultLockRegistry;
@@ -152,6 +157,8 @@ public class FileWritingMessageHandler extends AbstractReplyProducingMessageHand
private volatile boolean preserveTimestamp;
private Set<PosixFilePermission> permissions;
/**
* Constructor which sets the {@link #destinationDirectoryExpression} using
* a {@link LiteralExpression}.
@@ -367,6 +374,75 @@ public class FileWritingMessageHandler extends AbstractReplyProducingMessageHand
this.preserveTimestamp = preserveTimestamp;
}
/**
* String setter for Spring XML convenience.
* @param chmod permissions as an octal string e.g "600";
* @see #setChmod(int)
* @since 5.0
*/
public void setChmodOctal(String chmod) {
Assert.notNull(chmod, "'chmod' cannot be null");
setChmod(Integer.parseInt(chmod, 8));
}
/**
* Set the file permissions after uploading, e.g. 0600 for
* owner read/write. Only applies to file systems that support posix
* file permissions.
* @param chmod the permissions.
* @throws IllegalArgumentException if the value is higher than 0777.
* @since 5.0
*/
public void setChmod(int chmod) {
Assert.isTrue(chmod >= 0 && chmod <= 0777, "'chmod' must be between 0 and 0777 (octal)");
if (!FileUtils.IS_POSIX) {
this.logger.error("'chmod' setting ignored - the file system does not support Posix attributes");
return;
}
/*
* Bitset.valueOf(byte[]) takes a little-endian array of bytes to create a BitSet.
* Since we are interested in 9 bits, we construct an array with the low-order byte
* (bits 0-7) followed by the second order byte (bit 8).
* BitSet.stream() returns a stream of ints representing those bits that are set.
* We use that stream with a switch to create the set of PosixFilePermissions
* representing the bits that were set in the chmod value.
*/
BitSet bits = BitSet.valueOf(new byte[] { (byte) chmod, (byte) (chmod >> 8) });
final Set<PosixFilePermission> permissions = new HashSet<>();
bits.stream().forEach(b -> {
switch (b) {
case 0:
permissions.add(PosixFilePermission.OTHERS_EXECUTE);
break;
case 1:
permissions.add(PosixFilePermission.OTHERS_WRITE);
break;
case 2:
permissions.add(PosixFilePermission.OTHERS_READ);
break;
case 3:
permissions.add(PosixFilePermission.GROUP_EXECUTE);
break;
case 4:
permissions.add(PosixFilePermission.GROUP_WRITE);
break;
case 5:
permissions.add(PosixFilePermission.GROUP_READ);
break;
case 6:
permissions.add(PosixFilePermission.OWNER_EXECUTE);
break;
case 7:
permissions.add(PosixFilePermission.OWNER_WRITE);
break;
case 8:
permissions.add(PosixFilePermission.OWNER_READ);
break;
}
});
this.permissions = permissions;
}
@Override
protected void doInit() {
this.evaluationContext = ExpressionUtils.createStandardEvaluationContext(getBeanFactory());
@@ -414,14 +490,14 @@ public class FileWritingMessageHandler extends AbstractReplyProducingMessageHand
private void validateDestinationDirectory(File destinationDirectory, boolean autoCreateDirectory) {
if (!destinationDirectory.exists() && autoCreateDirectory) {
Assert.isTrue(destinationDirectory.mkdirs(),
"Destination directory [" + destinationDirectory + "] could not be created.");
() -> "Destination directory [" + destinationDirectory + "] could not be created.");
}
Assert.isTrue(destinationDirectory.exists(),
"Destination directory [" + destinationDirectory + "] does not exist.");
() -> "Destination directory [" + destinationDirectory + "] does not exist.");
Assert.isTrue(destinationDirectory.isDirectory(),
"Destination path [" + destinationDirectory + "] does not point to a directory.");
() -> "Destination path [" + destinationDirectory + "] does not point to a directory.");
Assert.isTrue(Files.isWritable(destinationDirectory.toPath()),
"Destination directory [" + destinationDirectory + "] is not writable.");
() -> "Destination directory [" + destinationDirectory + "] is not writable.");
}
@Override
@@ -485,7 +561,7 @@ public class FileWritingMessageHandler extends AbstractReplyProducingMessageHand
else {
if (this.logger.isWarnEnabled()) {
this.logger.warn("Could not set lastModified, header " + FileHeaders.SET_MODIFIED
+ " must be a Number, not " + timestamp.getClass());
+ " must be a Number, not " + (timestamp == null ? "null" : timestamp.getClass()));
}
}
}
@@ -730,7 +806,7 @@ public class FileWritingMessageHandler extends AbstractReplyProducingMessageHand
fileToWriteTo = tempFile;
break;
default:
throw new IllegalStateException("Unsupported FileExistsMode " + this.fileExistsMode);
throw new IllegalStateException("Unsupported FileExistsMode: " + this.fileExistsMode);
}
return fileToWriteTo;
}
@@ -745,6 +821,20 @@ public class FileWritingMessageHandler extends AbstractReplyProducingMessageHand
if (this.deleteSourceFiles && originalFile != null) {
originalFile.delete();
}
setPermissions(resultFile);
}
/**
* Set permissions on newly written files.
* @param resultFile the file.
* @throws IOException any exception.
* @since 5.0
*/
protected void setPermissions(File resultFile) throws IOException {
if (this.permissions != null) {
Files.setPosixFilePermissions(resultFile.toPath(), this.permissions);
}
}
private void renameTo(File tempFile, File resultFile) throws IOException {

View File

@@ -75,6 +75,7 @@ abstract class FileWritingMessageHandlerBeanDefinitionBuilder {
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "flush-interval");
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "flush-when-idle");
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "flush-predicate");
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "chmod");
String remoteFileNameGenerator = element.getAttribute("filename-generator");
String remoteFileNameGeneratorExpression = element.getAttribute("filename-generator-expression");
boolean hasRemoteFileNameGenerator = StringUtils.hasText(remoteFileNameGenerator);

View File

@@ -73,6 +73,8 @@ public class FileWritingMessageHandlerFactoryBean
private volatile MessageFlushPredicate flushPredicate;
private String chmod;
public void setFileExistsMode(String fileExistsModeAsString) {
this.fileExistsMode = FileExistsMode.getForString(fileExistsModeAsString);
}
@@ -137,6 +139,10 @@ public class FileWritingMessageHandlerFactoryBean
this.flushPredicate = flushPredicate;
}
public void setChmod(String chmod) {
this.chmod = chmod;
}
@Override
protected FileWritingMessageHandler createHandler() {
@@ -195,6 +201,9 @@ public class FileWritingMessageHandlerFactoryBean
if (this.flushPredicate != null) {
handler.setFlushPredicate(this.flushPredicate);
}
if (this.chmod != null) {
handler.setChmodOctal(this.chmod);
}
return handler;
}

View File

@@ -38,6 +38,7 @@ import org.springframework.util.Assert;
* The {@link MessageHandlerSpec} for the {@link FileWritingMessageHandler}.
*
* @author Artem Bilan
* @author Gary Russell
*
* @since 5.0
*/
@@ -245,6 +246,20 @@ public class FileWritingMessageHandlerSpec
return this;
}
/**
* Set the file permissions after uploading, e.g. 0600 for
* owner read/write. Only applies to file systems that support posix
* file permissions.
* @param chmod the permissions.
* @throws IllegalArgumentException if the value is higher than 0777.
* @return the spec.
* @see FileWritingMessageHandler#setChmod(int)
*/
public FileWritingMessageHandlerSpec chmod(int chmod) {
this.target.setChmod(chmod);
return this;
}
@Override
public Collection<Object> getComponentsToRegister() {
if (this.defaultFileNameGenerator != null) {

View File

@@ -0,0 +1,36 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.file.support;
import java.nio.file.FileSystems;
/**
* Utilities for operations on Files.
*
* @author Gary Russell
* @since 5.0
*
*/
public final class FileUtils {
public static final boolean IS_POSIX = FileSystems.getDefault().supportedFileAttributeViews().contains("posix");
private FileUtils() {
super();
}
}

View File

@@ -597,6 +597,7 @@ Only files matching this regular expression will be picked up by this adapter.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attributeGroup ref="chmod" />
<xsd:attributeGroup ref="integration:smartLifeCycleAttributeGroup"/>
</xsd:complexType>
@@ -1027,4 +1028,15 @@ Only files matching this regular expression will be picked up by this adapter.
</xsd:attribute>
</xsd:attributeGroup>
<xsd:attributeGroup name="chmod">
<xsd:attribute name="chmod" type="xsd:string">
<xsd:annotation>
<xsd:documentation>
Change the mode of the file (or remote file) after writing. Integer value
expressed in Octal, e.g. '644'.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
</xsd:attributeGroup>
</xsd:schema>