Skip to content

Commit

Permalink
GH-3026 Support chmod with FTP
Browse files Browse the repository at this point in the history
Resolves #3026

* Fix exception messages; remove test TODOs; test works on Windows
  • Loading branch information
garyrussell authored and artembilan committed Aug 15, 2019
1 parent f256974 commit a4f7412
Show file tree
Hide file tree
Showing 12 changed files with 99 additions and 10 deletions.
Expand Up @@ -20,6 +20,7 @@

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;
Expand Down Expand Up @@ -56,6 +57,7 @@ protected void postProcessBuilder(BeanDefinitionBuilder builder, Element element
.getValue();
templateDefinition.getPropertyValues() // NOSONAR never null
.add("existsMode", FtpRemoteFileTemplate.ExistsMode.NLST);
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "chmod", "chmodOctal");
}

}
Expand Up @@ -71,6 +71,7 @@ protected void postProcessBuilder(BeanDefinitionBuilder builder, Element element

IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "working-dir-expression",
"workingDirExpressionString");
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "chmod", "chmodOctal");
}

}
Expand Up @@ -32,7 +32,9 @@
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;
Expand Down Expand Up @@ -299,4 +301,22 @@ else if (e instanceof RuntimeException) {
}
}

@Override
public boolean isChmodCapable() {
return true;
}

@Override
protected void doChmod(RemoteFileOperations<FTPFile> remoteFileOperations, final String path, final int chmod) {
remoteFileOperations.executeWithClient((ClientCallbackWithoutResult<FTPClient>) client -> {
String chModCommand = "chmod " + Integer.toOctalString(chmod) + " " + path;
try {
client.sendSiteCommand(chModCommand);
}
catch (IOException e) {
throw new UncheckedIOException("Failed to execute '" + chModCommand + "'", e);
}
});
}

}
Expand Up @@ -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;
Expand Down Expand Up @@ -47,4 +52,22 @@ public FtpMessageHandler(RemoteFileTemplate<FTPFile> remoteFileTemplate, FileExi
super(remoteFileTemplate, mode);
}

@Override
public boolean isChmodCapable() {
return true;
}

@Override
protected void doChmod(RemoteFileTemplate<FTPFile> remoteFileTemplate, final String path, final int chmod) {
remoteFileTemplate.executeWithClient((ClientCallbackWithoutResult<FTPClient>) client -> {
String chModCommand = "chmod " + Integer.toOctalString(chmod) + " " + path;
try {
client.sendSiteCommand(chModCommand);
}
catch (IOException e) {
throw new UncheckedIOException("Failed to execute '" + chModCommand + "'", e);
}
});
}

}
Expand Up @@ -63,6 +63,7 @@
</xsd:annotation>
</xsd:attribute>
<xsd:attributeGroup ref="int-file:remoteOutboundAttributeGroup" />
<xsd:attributeGroup ref="int-file:chmod" />
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
Expand Down Expand Up @@ -517,6 +518,7 @@
</xsd:annotation>
</xsd:attribute>
<xsd:attributeGroup ref="int-file:remoteOutboundAttributeGroup" />
<xsd:attributeGroup ref="int-file:chmod" />
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
Expand Down
Expand Up @@ -86,6 +86,7 @@
auto-create-directory="true"
filename-pattern="*.txt"
expression="payload"
chmod="600"
remote-directory="ftpTarget"
mput-filter="sortingFilter"
reply-channel="output"/>
Expand Down Expand Up @@ -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"
Expand Down
Expand Up @@ -21,6 +21,8 @@
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;
Expand All @@ -47,6 +49,7 @@
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;
Expand Down Expand Up @@ -399,7 +402,12 @@ public void testRawGETWithTemplate() throws Exception {
}

@Test
public void testInt3088MPutNotRecursive() {
public void testInt3088MPutNotRecursive() throws IOException {
Session<?> session = sessionFactory.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<File>(getSourceLocalDirectory()));
@SuppressWarnings("unchecked")
Message<List<String>> out = (Message<List<String>>) this.output.receive(1000);
Expand All @@ -410,6 +418,8 @@ public void testInt3088MPutNotRecursive() {
.isIn("ftpTarget/localSource1.txt", "ftpTarget/localSource2.txt");
assertThat(out.getPayload().get(1))
.isIn("ftpTarget/localSource1.txt", "ftpTarget/localSource2.txt");
verify(client).sendSiteCommand("chmod 600 ftpTarget/localSource1.txt");
verify(client).sendSiteCommand("chmod 600 ftpTarget/localSource1.txt");
}

@Test
Expand Down Expand Up @@ -448,7 +458,12 @@ public void testInt3088MPutRecursiveFiltered() {
}

@Test
public void testInt3412FileMode() {
public void testInt3412FileMode() throws IOException {
Session<?> session = sessionFactory.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);
assertThat(template.exists("ftpTarget/appending.txt")).isFalse();
Message<String> m = MessageBuilder.withPayload("foo")
Expand All @@ -468,7 +483,7 @@ public void testInt3412FileMode() {
catch (MessagingException e) {
assertThat(e.getCause().getCause().getMessage()).contains("The destination file already exists");
}

verify(client, times(2)).sendSiteCommand("chmod 600 ftpTarget/appending.txt");
}

@Test
Expand Down
Expand Up @@ -158,7 +158,8 @@ protected void doChmod(RemoteFileOperations<LsEntry> remoteFileOperations, final
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);
}
});
}
Expand Down
Expand Up @@ -78,7 +78,8 @@ protected void doChmod(RemoteFileTemplate<LsEntry> remoteFileTemplate, final Str
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);
}
});
}
Expand Down
16 changes: 16 additions & 0 deletions src/reference/asciidoc/ftp.adoc
Expand Up @@ -837,6 +837,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"/>
----
====
Expand Down Expand Up @@ -864,6 +865,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.
Expand Down Expand Up @@ -1189,6 +1195,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:
Expand All @@ -1208,6 +1219,11 @@ The message payload resulting from an `mget` operation is a `List<String>` objec

See also <<ftp-partial>>.

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.
Expand Down
10 changes: 5 additions & 5 deletions src/reference/asciidoc/sftp.adoc
Expand Up @@ -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.
Expand All @@ -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:
Expand Down Expand Up @@ -1174,7 +1174,7 @@ See also <<sftp-partial>>.

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

Expand Down
6 changes: 6 additions & 0 deletions src/reference/asciidoc/whats-new.adoc
Expand Up @@ -61,6 +61,12 @@ See <<./ftp.adoc#ftp-server-events, Apache Mina FTP Server Events>> and <<./sftp
Simple Apache Avro transformers are now provided.
See <<./transformers.adoc#avro-transformers, Avro Transformers>> for more information.

[[x5.2-ftp-chmod]]
==== FTP File Permissions

The FTP outbound endpoints now support `chmod` to change permissions on the uploaded file.
(SFTP already supported it since version 4.3).
See <<./ftp.adoc#ftp-outbound,FTP Outbound Channel Adapter>> and <<./ftp.adoc#ftp-outbound-gateway,FTP Outbound Gateway>> for more information.

[[x5.2-general]]
=== General Changes
Expand Down

0 comments on commit a4f7412

Please sign in to comment.