diff --git a/spring-integration-ftp/src/main/java/org/springframework/integration/ftp/config/FtpOutboundChannelAdapterParser.java b/spring-integration-ftp/src/main/java/org/springframework/integration/ftp/config/FtpOutboundChannelAdapterParser.java index eef85eb4fb..c789fa0b46 100644 --- a/spring-integration-ftp/src/main/java/org/springframework/integration/ftp/config/FtpOutboundChannelAdapterParser.java +++ b/spring-integration-ftp/src/main/java/org/springframework/integration/ftp/config/FtpOutboundChannelAdapterParser.java @@ -20,6 +20,7 @@ import org.w3c.dom.Element; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.integration.config.xml.IntegrationNamespaceUtils; import org.springframework.integration.file.config.RemoteFileOutboundChannelAdapterParser; import org.springframework.integration.file.remote.RemoteFileOperations; import org.springframework.integration.ftp.outbound.FtpMessageHandler; @@ -56,6 +57,7 @@ public class FtpOutboundChannelAdapterParser extends RemoteFileOutboundChannelAd .getValue(); templateDefinition.getPropertyValues() // NOSONAR never null .add("existsMode", FtpRemoteFileTemplate.ExistsMode.NLST); + IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "chmod", "chmodOctal"); } } diff --git a/spring-integration-ftp/src/main/java/org/springframework/integration/ftp/config/FtpOutboundGatewayParser.java b/spring-integration-ftp/src/main/java/org/springframework/integration/ftp/config/FtpOutboundGatewayParser.java index 8fa54001ba..3e26e94ac7 100644 --- a/spring-integration-ftp/src/main/java/org/springframework/integration/ftp/config/FtpOutboundGatewayParser.java +++ b/spring-integration-ftp/src/main/java/org/springframework/integration/ftp/config/FtpOutboundGatewayParser.java @@ -71,6 +71,7 @@ public class FtpOutboundGatewayParser extends AbstractRemoteFileOutboundGatewayP IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "working-dir-expression", "workingDirExpressionString"); + IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "chmod", "chmodOctal"); } } diff --git a/spring-integration-ftp/src/main/java/org/springframework/integration/ftp/gateway/FtpOutboundGateway.java b/spring-integration-ftp/src/main/java/org/springframework/integration/ftp/gateway/FtpOutboundGateway.java index 8df167a8a6..d39f340502 100644 --- a/spring-integration-ftp/src/main/java/org/springframework/integration/ftp/gateway/FtpOutboundGateway.java +++ b/spring-integration-ftp/src/main/java/org/springframework/integration/ftp/gateway/FtpOutboundGateway.java @@ -18,6 +18,7 @@ package org.springframework.integration.ftp.gateway; import java.io.File; import java.io.IOException; +import java.io.UncheckedIOException; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -31,7 +32,9 @@ import org.springframework.expression.Expression; import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.integration.expression.ExpressionUtils; import org.springframework.integration.file.remote.AbstractFileInfo; +import org.springframework.integration.file.remote.ClientCallbackWithoutResult; import org.springframework.integration.file.remote.MessageSessionCallback; +import org.springframework.integration.file.remote.RemoteFileOperations; import org.springframework.integration.file.remote.RemoteFileTemplate; import org.springframework.integration.file.remote.gateway.AbstractRemoteFileOutboundGateway; import org.springframework.integration.file.remote.session.Session; @@ -296,4 +299,22 @@ public class FtpOutboundGateway extends AbstractRemoteFileOutboundGateway remoteFileOperations, final String path, final int chmod) { + remoteFileOperations.executeWithClient((ClientCallbackWithoutResult) client -> { + String chModCommand = "chmod " + Integer.toOctalString(chmod) + " " + path; + try { + client.sendSiteCommand(chModCommand); + } + catch (IOException e) { + throw new UncheckedIOException("Failed to execute '" + chModCommand + "'", e); + } + }); + } + } diff --git a/spring-integration-ftp/src/main/java/org/springframework/integration/ftp/outbound/FtpMessageHandler.java b/spring-integration-ftp/src/main/java/org/springframework/integration/ftp/outbound/FtpMessageHandler.java index dbdbc8092b..69e52a1bb4 100644 --- a/spring-integration-ftp/src/main/java/org/springframework/integration/ftp/outbound/FtpMessageHandler.java +++ b/spring-integration-ftp/src/main/java/org/springframework/integration/ftp/outbound/FtpMessageHandler.java @@ -16,8 +16,13 @@ package org.springframework.integration.ftp.outbound; +import java.io.IOException; +import java.io.UncheckedIOException; + +import org.apache.commons.net.ftp.FTPClient; import org.apache.commons.net.ftp.FTPFile; +import org.springframework.integration.file.remote.ClientCallbackWithoutResult; import org.springframework.integration.file.remote.RemoteFileTemplate; import org.springframework.integration.file.remote.handler.FileTransferringMessageHandler; import org.springframework.integration.file.remote.session.SessionFactory; @@ -47,4 +52,22 @@ public class FtpMessageHandler extends FileTransferringMessageHandler { super(remoteFileTemplate, mode); } + @Override + public boolean isChmodCapable() { + return true; + } + + @Override + protected void doChmod(RemoteFileTemplate remoteFileTemplate, final String path, final int chmod) { + remoteFileTemplate.executeWithClient((ClientCallbackWithoutResult) client -> { + String chModCommand = "chmod " + Integer.toOctalString(chmod) + " " + path; + try { + client.sendSiteCommand(chModCommand); + } + catch (IOException e) { + throw new UncheckedIOException("Failed to execute '" + chModCommand + "'", e); + } + }); + } + } diff --git a/spring-integration-ftp/src/main/resources/org/springframework/integration/ftp/config/spring-integration-ftp-5.1.xsd b/spring-integration-ftp/src/main/resources/org/springframework/integration/ftp/config/spring-integration-ftp-5.1.xsd index 44f557e7e8..9198c6d620 100644 --- a/spring-integration-ftp/src/main/resources/org/springframework/integration/ftp/config/spring-integration-ftp-5.1.xsd +++ b/spring-integration-ftp/src/main/resources/org/springframework/integration/ftp/config/spring-integration-ftp-5.1.xsd @@ -63,6 +63,7 @@ + @@ -517,6 +518,7 @@ + diff --git a/spring-integration-ftp/src/test/java/org/springframework/integration/ftp/outbound/FtpServerOutboundTests-context.xml b/spring-integration-ftp/src/test/java/org/springframework/integration/ftp/outbound/FtpServerOutboundTests-context.xml index 9acdf70695..ba7fc086d8 100644 --- a/spring-integration-ftp/src/test/java/org/springframework/integration/ftp/outbound/FtpServerOutboundTests-context.xml +++ b/spring-integration-ftp/src/test/java/org/springframework/integration/ftp/outbound/FtpServerOutboundTests-context.xml @@ -86,6 +86,7 @@ auto-create-directory="true" filename-pattern="*.txt" expression="payload" + chmod="600" remote-directory="ftpTarget" mput-filter="sortingFilter" reply-channel="output"/> @@ -141,6 +142,7 @@ session-factory="ftpSessionFactory" channel="appending" mode="APPEND" + chmod="600" use-temporary-file-name="false" remote-directory="ftpTarget" auto-create-directory="true" diff --git a/spring-integration-ftp/src/test/java/org/springframework/integration/ftp/outbound/FtpServerOutboundTests.java b/spring-integration-ftp/src/test/java/org/springframework/integration/ftp/outbound/FtpServerOutboundTests.java index 645308c44b..4b9bbc3e87 100644 --- a/spring-integration-ftp/src/test/java/org/springframework/integration/ftp/outbound/FtpServerOutboundTests.java +++ b/spring-integration-ftp/src/test/java/org/springframework/integration/ftp/outbound/FtpServerOutboundTests.java @@ -34,6 +34,8 @@ import static org.junit.Assert.fail; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -58,6 +60,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mockito; +import org.springframework.beans.DirectFieldAccessor; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; @@ -395,7 +398,12 @@ public class FtpServerOutboundTests extends FtpTestSupport { } @Test - public void testInt3088MPutNotRecursive() { + public void testInt3088MPutNotRecursive() throws IOException { + Session session = ftpSessionFactory.getSession(); + session.close(); + session = TestUtils.getPropertyValue(session, "targetSession", Session.class); + FTPClient client = spy(TestUtils.getPropertyValue(session, "client", FTPClient.class)); + new DirectFieldAccessor(session).setPropertyValue("client", client); this.inboundMPut.send(new GenericMessage(getSourceLocalDirectory())); @SuppressWarnings("unchecked") Message> out = (Message>) this.output.receive(1000); @@ -409,6 +417,8 @@ public class FtpServerOutboundTests extends FtpTestSupport { assertThat( out.getPayload().get(1), anyOf(equalTo("ftpTarget/localSource1.txt"), equalTo("ftpTarget/localSource2.txt"))); + verify(client).sendSiteCommand("chmod 600 ftpTarget/localSource1.txt"); + verify(client).sendSiteCommand("chmod 600 ftpTarget/localSource1.txt"); } @Test @@ -454,7 +464,12 @@ public class FtpServerOutboundTests extends FtpTestSupport { } @Test - public void testInt3412FileMode() { + public void testInt3412FileMode() throws IOException { + Session session = ftpSessionFactory.getSession(); + session.close(); + session = TestUtils.getPropertyValue(session, "targetSession", Session.class); + FTPClient client = spy(TestUtils.getPropertyValue(session, "client", FTPClient.class)); + new DirectFieldAccessor(session).setPropertyValue("client", client); FtpRemoteFileTemplate template = new FtpRemoteFileTemplate(ftpSessionFactory); assertFalse(template.exists("ftpTarget/appending.txt")); Message m = MessageBuilder.withPayload("foo") @@ -474,7 +489,7 @@ public class FtpServerOutboundTests extends FtpTestSupport { catch (MessagingException e) { assertThat(e.getCause().getCause().getMessage(), containsString("The destination file already exists")); } - + verify(client, times(2)).sendSiteCommand("chmod 600 ftpTarget/appending.txt"); } @Test diff --git a/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/gateway/SftpOutboundGateway.java b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/gateway/SftpOutboundGateway.java index 54b1a9f952..cdb7e44437 100644 --- a/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/gateway/SftpOutboundGateway.java +++ b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/gateway/SftpOutboundGateway.java @@ -158,7 +158,8 @@ public class SftpOutboundGateway extends AbstractRemoteFileOutboundGateway client.chmod(chmod, path); } catch (SftpException e) { - throw new GeneralSftpException("Failed to execute chmod", e); + throw new GeneralSftpException( + "Failed to execute 'chmod " + Integer.toOctalString(chmod) + " " + path + "'", e); } }); } diff --git a/src/reference/asciidoc/ftp.adoc b/src/reference/asciidoc/ftp.adoc index b9cd333be1..6c7d16f521 100644 --- a/src/reference/asciidoc/ftp.adoc +++ b/src/reference/asciidoc/ftp.adoc @@ -833,6 +833,7 @@ The following example shows how to configure an `outbound-channel-adapter`: temporary-remote-directory-expression="headers['temp_remote_dir']" filename-generator="fileNameGenerator" use-temporary-filename="true" + chmod="600" mode="REPLACE"/> ---- ==== @@ -860,6 +861,11 @@ The modes are defined by the `FileExistsMode` enumeration, which includes the fo `IGNORE` and `FAIL` do not transfer the file. `FAIL` causes an exception to be thrown, while `IGNORE` silently ignores the transfer (although a `DEBUG` log entry is produced). +Version 5.2 introduced the `chmod` attribute, which you can use to change the remote file permissions after upload. +You can use the conventional Unix octal format (for example, `600` allows read-write for the file owner only). +When configuring the adapter using java, you can use `setChmodOctal("600")` or `setChmod(0600)`. +Only applies if your FTP server supports the `SITE CHMOD` subcommand. + ==== Avoiding Partially Written Files One of the common problems that arises when dealing with file transfers is the possibility of processing a partial file. @@ -1185,6 +1191,11 @@ See the https://github.com/spring-projects/spring-integration/tree/master/spring The message payload resulting from a `put` operation is a `String` that represents the full path of the file on the server after transfer. +Version 5.2 introduced the `chmod` attribute, which changes the remote file permissions after upload. +You can use the conventional Unix octal format (for example, `600` allows read-write for the file owner only). +When configuring the adapter using java, you can use `setChmod(0600)`. +Only applies if your FTP server supports the `SITE CHMOD` subcommand. + Using the `mput` Command The `mput` sends multiple files to the server and supports only one option: @@ -1204,6 +1215,11 @@ The message payload resulting from an `mget` operation is a `List` objec See also <>. +Version 5.2 introduced the `chmod` attribute, which lets you change the remote file permissions after upload. +You can use the conventional Unix octal format (for example, `600` allows read-write for the file owner only). +When configuring the adapter with Java, you can use `setChmodOctal("600")` or `setChmod(0600)`. +Only applies if your FTP server supports the `SITE CHMOD` subcommand. + ==== Using the `rm` Command The `rm` command removes files. diff --git a/src/reference/asciidoc/sftp.adoc b/src/reference/asciidoc/sftp.adoc index 1dcf37157e..b44fc0f7b8 100644 --- a/src/reference/asciidoc/sftp.adoc +++ b/src/reference/asciidoc/sftp.adoc @@ -855,6 +855,10 @@ The modes are defined by the `FileExistsMode` enumeration, which has the followi With `IGNORE` and `FAIL`, the file is not transferred. `FAIL` causes an exception to be thrown, while `IGNORE` silently ignores the transfer (although a `DEBUG` log entry is produced). +Version 4.3 introduced the `chmod` attribute, which you can use to change the remote file permissions after upload. +You can use the conventional Unix octal format (for example, `600` allows read-write for the file owner only). +When configuring the adapter using java, you can use `setChmodOctal("600")` or `setChmod(0600)`. + ==== Avoiding Partially Written Files One of the common problems when dealing with file transfers is the possibility of processing a partial file. @@ -869,10 +873,6 @@ However, there may be situations where you do not want to use this technique (fo For situations like this, you can disable this feature by setting `use-temporary-file-name` to `false` (the default is `true`). When this attribute is `false`, the file is written with its final name, and the consuming application needs some other mechanism to detect that the file is completely uploaded before accessing it. -Version 4.3 introduced the `chmod` attribute, which you can use to change the remote file permissions after upload. -You can use the conventional Unix octal format (for example, `600` allows read-write for the file owner only). -When configuring the adapter using java, you can use `setChmodOctal("600")` or `setChmodDecimal(384)`. - ==== Configuring with Java Configuration The following Spring Boot application shows an example of how to configure the outbound adapter with Java: @@ -1174,7 +1174,7 @@ See also <>. Version 4.3 introduced the `chmod` attribute, which lets you change the remote file permissions after upload. You can use the conventional Unix octal format (for example, `600` allows read-write for the file owner only). -When configuring the adapter with Java, you can use `setChmodOctal("600")` or `setChmodDecimal(384)`. +When configuring the adapter with Java, you can use `setChmodOctal("600")` or `setChmod(0600)`. Using the `rm` Command