From 2b99c191f6a02c44eebf8b7ba35a1ccc20c34ea1 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Mon, 22 May 2017 21:00:31 -0400 Subject: [PATCH] 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 --- .../file/FileWritingMessageHandler.java | 102 ++++++++++++++++-- ...ngMessageHandlerBeanDefinitionBuilder.java | 1 + .../FileWritingMessageHandlerFactoryBean.java | 9 ++ .../dsl/FileWritingMessageHandlerSpec.java | 15 +++ .../integration/file/support/FileUtils.java | 36 +++++++ .../config/spring-integration-file-5.0.xsd | 12 +++ .../file/FileWritingMessageHandlerTests.java | 39 ++++++- ...boundChannelAdapterParserTests-context.xml | 1 + ...FileOutboundChannelAdapterParserTests.java | 6 ++ .../integration/file/dsl/FileTests.java | 9 +- .../config/spring-integration-sftp-5.0.xsd | 15 +-- src/reference/asciidoc/file.adoc | 6 ++ src/reference/asciidoc/whats-new.adoc | 6 +- 13 files changed, 234 insertions(+), 23 deletions(-) create mode 100644 spring-integration-file/src/main/java/org/springframework/integration/file/support/FileUtils.java diff --git a/spring-integration-file/src/main/java/org/springframework/integration/file/FileWritingMessageHandler.java b/spring-integration-file/src/main/java/org/springframework/integration/file/FileWritingMessageHandler.java index 6df2f08008..5c4f0d0fac 100644 --- a/spring-integration-file/src/main/java/org/springframework/integration/file/FileWritingMessageHandler.java +++ b/spring-integration-file/src/main/java/org/springframework/integration/file/FileWritingMessageHandler.java @@ -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 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 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 { diff --git a/spring-integration-file/src/main/java/org/springframework/integration/file/config/FileWritingMessageHandlerBeanDefinitionBuilder.java b/spring-integration-file/src/main/java/org/springframework/integration/file/config/FileWritingMessageHandlerBeanDefinitionBuilder.java index a609c2b496..64d8ff90e8 100644 --- a/spring-integration-file/src/main/java/org/springframework/integration/file/config/FileWritingMessageHandlerBeanDefinitionBuilder.java +++ b/spring-integration-file/src/main/java/org/springframework/integration/file/config/FileWritingMessageHandlerBeanDefinitionBuilder.java @@ -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); diff --git a/spring-integration-file/src/main/java/org/springframework/integration/file/config/FileWritingMessageHandlerFactoryBean.java b/spring-integration-file/src/main/java/org/springframework/integration/file/config/FileWritingMessageHandlerFactoryBean.java index 81fd1bc1c2..9375d90650 100644 --- a/spring-integration-file/src/main/java/org/springframework/integration/file/config/FileWritingMessageHandlerFactoryBean.java +++ b/spring-integration-file/src/main/java/org/springframework/integration/file/config/FileWritingMessageHandlerFactoryBean.java @@ -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; } diff --git a/spring-integration-file/src/main/java/org/springframework/integration/file/dsl/FileWritingMessageHandlerSpec.java b/spring-integration-file/src/main/java/org/springframework/integration/file/dsl/FileWritingMessageHandlerSpec.java index 35693f2505..78418aa711 100644 --- a/spring-integration-file/src/main/java/org/springframework/integration/file/dsl/FileWritingMessageHandlerSpec.java +++ b/spring-integration-file/src/main/java/org/springframework/integration/file/dsl/FileWritingMessageHandlerSpec.java @@ -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 getComponentsToRegister() { if (this.defaultFileNameGenerator != null) { diff --git a/spring-integration-file/src/main/java/org/springframework/integration/file/support/FileUtils.java b/spring-integration-file/src/main/java/org/springframework/integration/file/support/FileUtils.java new file mode 100644 index 0000000000..4badab662e --- /dev/null +++ b/spring-integration-file/src/main/java/org/springframework/integration/file/support/FileUtils.java @@ -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(); + } + +} diff --git a/spring-integration-file/src/main/resources/org/springframework/integration/file/config/spring-integration-file-5.0.xsd b/spring-integration-file/src/main/resources/org/springframework/integration/file/config/spring-integration-file-5.0.xsd index cc7ac97d5d..e9caee535e 100644 --- a/spring-integration-file/src/main/resources/org/springframework/integration/file/config/spring-integration-file-5.0.xsd +++ b/spring-integration-file/src/main/resources/org/springframework/integration/file/config/spring-integration-file-5.0.xsd @@ -597,6 +597,7 @@ Only files matching this regular expression will be picked up by this adapter. + @@ -1027,4 +1028,15 @@ Only files matching this regular expression will be picked up by this adapter. + + + + + Change the mode of the file (or remote file) after writing. Integer value + expressed in Octal, e.g. '644'. + + + + + diff --git a/spring-integration-file/src/test/java/org/springframework/integration/file/FileWritingMessageHandlerTests.java b/spring-integration-file/src/test/java/org/springframework/integration/file/FileWritingMessageHandlerTests.java index 9bd073c231..68a59d83d1 100644 --- a/spring-integration-file/src/test/java/org/springframework/integration/file/FileWritingMessageHandlerTests.java +++ b/spring-integration-file/src/test/java/org/springframework/integration/file/FileWritingMessageHandlerTests.java @@ -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("test")); - assertThat(outputDirectory.listFiles()[0], notNullValue()); + File[] output = outputDirectory.listFiles(); + assertThat(output.length, equalTo(1)); + assertThat(output[0], notNullValue()); + if (FileUtils.IS_POSIX) { + Set permissions = Files.getPosixFilePermissions(output[0].toPath()); + assertThat(permissions.size(), equalTo(9)); + } } @Test diff --git a/spring-integration-file/src/test/java/org/springframework/integration/file/config/FileOutboundChannelAdapterParserTests-context.xml b/spring-integration-file/src/test/java/org/springframework/integration/file/config/FileOutboundChannelAdapterParserTests-context.xml index 4a5839b473..24dc7e24e1 100644 --- a/spring-integration-file/src/test/java/org/springframework/integration/file/config/FileOutboundChannelAdapterParserTests-context.xml +++ b/spring-integration-file/src/test/java/org/springframework/integration/file/config/FileOutboundChannelAdapterParserTests-context.xml @@ -17,6 +17,7 @@ channel="testChannel" directory="${java.io.tmpdir}" temporary-file-suffix=".foo" + chmod="777" filename-generator-expression="'foo.txt'"/> 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(); } diff --git a/spring-integration-sftp/src/main/resources/org/springframework/integration/sftp/config/spring-integration-sftp-5.0.xsd b/spring-integration-sftp/src/main/resources/org/springframework/integration/sftp/config/spring-integration-sftp-5.0.xsd index f975d10090..85bbb4dd91 100644 --- a/spring-integration-sftp/src/main/resources/org/springframework/integration/sftp/config/spring-integration-sftp-5.0.xsd +++ b/spring-integration-sftp/src/main/resources/org/springframework/integration/sftp/config/spring-integration-sftp-5.0.xsd @@ -66,7 +66,7 @@ - + @@ -496,7 +496,7 @@ - + @@ -641,17 +641,6 @@ - - - - - Change the mode of the remote file after transferring. Integer value - expressed in Octal, e.g. '644'. - - - - - diff --git a/src/reference/asciidoc/file.adoc b/src/reference/asciidoc/file.adoc index b777c2bfc5..2e1069d45e 100644 --- a/src/reference/asciidoc/file.adoc +++ b/src/reference/asciidoc/file.adoc @@ -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 diff --git a/src/reference/asciidoc/whats-new.adoc b/src/reference/asciidoc/whats-new.adoc index 868f9e92f0..3120986d61 100644 --- a/src/reference/asciidoc/whats-new.adoc +++ b/src/reference/asciidoc/whats-new.adoc @@ -111,7 +111,11 @@ See <> for more information. The flush predicates for the `FileWritingMessageHandler` now have an additional parameter. See <> 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 <> for more information. + +They also now support setting file permissions on the newly written file. +See <> for more information. ==== (S)FTP Changes