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();
}