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>

View File

@@ -46,7 +46,10 @@ import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.attribute.PosixFilePermission;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Matcher;
@@ -61,9 +64,11 @@ import org.junit.rules.TemporaryFolder;
import org.springframework.beans.DirectFieldAccessor;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.expression.Expression;
import org.springframework.integration.channel.NullChannel;
import org.springframework.integration.channel.QueueChannel;
import org.springframework.integration.file.support.FileExistsMode;
import org.springframework.integration.file.support.FileUtils;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.integration.test.util.TestUtils;
import org.springframework.messaging.Message;
@@ -123,10 +128,40 @@ public class FileWritingMessageHandlerTests {
}
@Test
public void supportedType() throws Exception {
public void permissions() {
if (FileUtils.IS_POSIX) {
FileWritingMessageHandler handler = new FileWritingMessageHandler(mock(Expression.class));
handler.setChmod(0421);
Set<?> permissions = TestUtils.getPropertyValue(handler, "permissions", Set.class);
assertThat(permissions.size(), equalTo(3));
assertTrue(permissions.contains(PosixFilePermission.OWNER_READ));
assertTrue(permissions.contains(PosixFilePermission.GROUP_WRITE));
assertTrue(permissions.contains(PosixFilePermission.OTHERS_EXECUTE));
handler.setChmod(0600);
permissions = TestUtils.getPropertyValue(handler, "permissions", Set.class);
assertThat(permissions.size(), equalTo(2));
assertTrue(permissions.contains(PosixFilePermission.OWNER_READ));
assertTrue(permissions.contains(PosixFilePermission.OWNER_WRITE));
handler.setChmod(0777);
permissions = TestUtils.getPropertyValue(handler, "permissions", Set.class);
assertThat(permissions.size(), equalTo(9));
}
}
@Test
public void supportedTypeAndPermissions() throws Exception {
if (FileUtils.IS_POSIX) {
handler.setChmod(0777);
}
handler.setOutputChannel(new NullChannel());
handler.handleMessage(new GenericMessage<String>("test"));
assertThat(outputDirectory.listFiles()[0], notNullValue());
File[] output = outputDirectory.listFiles();
assertThat(output.length, equalTo(1));
assertThat(output[0], notNullValue());
if (FileUtils.IS_POSIX) {
Set<PosixFilePermission> permissions = Files.getPosixFilePermissions(output[0].toPath());
assertThat(permissions.size(), equalTo(9));
}
}
@Test

View File

@@ -17,6 +17,7 @@
channel="testChannel"
directory="${java.io.tmpdir}"
temporary-file-suffix=".foo"
chmod="777"
filename-generator-expression="'foo.txt'"/>
<file:outbound-channel-adapter id="adapterWithCustomNameGenerator"

View File

@@ -17,6 +17,7 @@
package org.springframework.integration.file.config;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertSame;
@@ -26,6 +27,7 @@ import static org.junit.Assert.assertTrue;
import java.io.File;
import java.lang.reflect.Method;
import java.nio.charset.Charset;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@@ -41,6 +43,7 @@ import org.springframework.integration.file.DefaultFileNameGenerator;
import org.springframework.integration.file.FileWritingMessageHandler;
import org.springframework.integration.file.FileWritingMessageHandler.MessageFlushPredicate;
import org.springframework.integration.file.support.FileExistsMode;
import org.springframework.integration.file.support.FileUtils;
import org.springframework.integration.handler.advice.AbstractRequestHandlerAdvice;
import org.springframework.integration.test.util.TestUtils;
import org.springframework.messaging.Message;
@@ -140,6 +143,9 @@ public class FileOutboundChannelAdapterParserTests {
assertEquals("'foo.txt'", expression.getExpressionString());
assertEquals(Boolean.FALSE, handlerAccessor.getPropertyValue("deleteSourceFiles"));
assertEquals(Boolean.TRUE, handlerAccessor.getPropertyValue("flushWhenIdle"));
if (FileUtils.IS_POSIX) {
assertThat(TestUtils.getPropertyValue(handler, "permissions", Set.class).size(), equalTo(9));
}
}
@Test

View File

@@ -17,6 +17,7 @@
package org.springframework.integration.file.dsl;
import static org.hamcrest.Matchers.endsWith;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.instanceOf;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
@@ -74,6 +75,7 @@ import org.springframework.integration.file.filters.ChainFileListFilter;
import org.springframework.integration.file.filters.ExpressionFileListFilter;
import org.springframework.integration.file.splitter.FileSplitter;
import org.springframework.integration.file.support.FileExistsMode;
import org.springframework.integration.file.support.FileUtils;
import org.springframework.integration.file.tail.ApacheCommonsFileTailingMessageProducer;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.integration.test.util.TestUtils;
@@ -240,6 +242,9 @@ public class FileTests {
endsWith(TestUtils.applySystemFileSeparator("fileWritingFlow/foo.write")));
String fileContent = StreamUtils.copyToString(new FileInputStream(resultFile), Charset.defaultCharset());
assertEquals(payload, fileContent);
if (FileUtils.IS_POSIX) {
assertThat(java.nio.file.Files.getPosixFilePermissions(resultFile.toPath()).size(), equalTo(9));
}
}
@Autowired
@@ -389,7 +394,9 @@ public class FileTests {
return IntegrationFlows.from("fileWritingInput")
.enrichHeaders(h -> h.header(FileHeaders.FILENAME, "foo.write")
.header("directory", new File(tmpDir.getRoot(), "fileWritingFlow")))
.handle(Files.outboundGateway(m -> m.getHeaders().get("directory")))
.handle(Files.outboundGateway(m -> m.getHeaders().get("directory"))
.preserveTimestamp(true)
.chmod(0777))
.channel(MessageChannels.queue("fileWritingResultChannel"))
.get();
}

View File

@@ -66,7 +66,7 @@
</xsd:annotation>
</xsd:attribute>
<xsd:attributeGroup ref="int-file:remoteOutboundAttributeGroup" />
<xsd:attributeGroup ref="chmod" />
<xsd:attributeGroup ref="int-file:chmod" />
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
@@ -496,7 +496,7 @@
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attributeGroup ref="chmod" />
<xsd:attributeGroup ref="int-file:chmod" />
<xsd:attributeGroup ref="int-file:remoteOutboundAttributeGroup" />
</xsd:extension>
</xsd:complexContent>
@@ -641,17 +641,6 @@
<xsd:attributeGroup ref="integration:smartLifeCycleAttributeGroup"/>
</xsd:complexType>
<xsd:attributeGroup name="chmod">
<xsd:attribute name="chmod" type="xsd:string">
<xsd:annotation>
<xsd:documentation>
Change the mode of the remote file after transferring. Integer value
expressed in Octal, e.g. '644'.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
</xsd:attributeGroup>
<xsd:attributeGroup name="tempSuffixGroup">
<xsd:attribute name="temporary-file-suffix" type="xsd:string">
<xsd:annotation>

View File

@@ -706,6 +706,12 @@ copy was required).
For other payloads, if the `FileHeaders.SET_MODIFIED` header (`file_setModified`) is present, it will be used to set
the destination file's `lastModified` timestamp, as long as the header is a `Number`.
[[file-permissions]]
==== File Permissions
Starting with _version 5.0_, when writing files to a file system that supports Posix permissions, you can specify those permissions on the outbound channel adapter or gateway.
The property is an integer and is usually supplied in the familiar octal format; e.g. `0640` meaning the owner has read/write permissions, the group has read only permission and others have no access.
[[file-outbound-channel-adapter]]
==== File Outbound Channel Adapter

View File

@@ -111,7 +111,11 @@ See <<file-tailing>> for more information.
The flush predicates for the `FileWritingMessageHandler` now have an additional parameter.
See <<file-flushing>> for more information.
The file outbound channel adapter (`FileWritingMessageHandler`) now supports the `REPLACE_IF_MODIFIED` `FileExistsMode`.
The file outbound channel adapter and gateway (`FileWritingMessageHandler`) now support the `REPLACE_IF_MODIFIED` `FileExistsMode`.
See <<file-writing-destination-exists>> for more information.
They also now support setting file permissions on the newly written file.
See <<file-permissions>> for more information.
==== (S)FTP Changes