INT-2866: Add (S)FTP local-directory-expression
* Add `local-directory-expression` to (S)FTP Outbound Gateways * Add `FtpServerRule` to Apache Mina embedded FtpServer * Add tests for (M)GET and `local-directory-expression`: FTP tests uses `FtpServerRule`, SFTP tests need testing on real SFTP server JIRA: https://jira.springsource.org/browse/INT-2866 INT-2866: Documentation INT-2866 Polishing - Remove leading / - Change \ to / in invalid test - Clean up after sftp Tested with real SSH. INT-2866 Polishing - Add Mock SFTP Test Run with -Dspring-profiles-active=realSSH to run with a real SSH server. Assumes ftptest/ftptest account on localhost with the following directory tree in the user's root... $ tree sftpSource/ sftpSource/ ├── sftpSource1.txt ├── sftpSource2.txt └── subSftpSource └── subSftpSource1.txt INT-2866: Polishing INT-2866: change `remotePath` to `remoteDirectory` Doc Polishing.
This commit is contained in:
committed by
Gary Russell
parent
06979d7678
commit
dd479a3ce7
@@ -415,6 +415,23 @@
|
||||
Identifies directory path (e.g.,
|
||||
"/local/mytransfers") where file will be
|
||||
transferred TO.
|
||||
This attribute is mutually exclusive with 'local-directory-expression'.
|
||||
</xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="local-directory-expression" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation>
|
||||
Specifies SpEL expression to
|
||||
generate the directory path where file will be
|
||||
transferred TO, when using 'get' and 'mget' commands.
|
||||
The root object of the SpEL evaluation is the request Message,
|
||||
but the name of the source
|
||||
remote directory is also provided as the 'remoteDirectory' variable.
|
||||
For example, a valid expression might be:
|
||||
"'/local/' + #remoteDirectory.toUpperCase() + headers.foo".
|
||||
Only used with 'get' and 'mget' commands.
|
||||
This attribute is mutually exclusive with 'local-directory'.
|
||||
</xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
/*
|
||||
* Copyright 2013 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.ftp;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.apache.ftpserver.FtpServer;
|
||||
import org.apache.ftpserver.FtpServerFactory;
|
||||
import org.apache.ftpserver.ftplet.Authentication;
|
||||
import org.apache.ftpserver.ftplet.AuthenticationFailedException;
|
||||
import org.apache.ftpserver.ftplet.FtpException;
|
||||
import org.apache.ftpserver.ftplet.User;
|
||||
import org.apache.ftpserver.ftplet.UserManager;
|
||||
import org.apache.ftpserver.listener.ListenerFactory;
|
||||
import org.apache.ftpserver.usermanager.impl.BaseUser;
|
||||
import org.apache.ftpserver.usermanager.impl.ConcurrentLoginPermission;
|
||||
import org.apache.ftpserver.usermanager.impl.TransferRatePermission;
|
||||
import org.apache.ftpserver.usermanager.impl.WritePermission;
|
||||
import org.junit.rules.ExternalResource;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
|
||||
import org.springframework.integration.test.util.SocketUtils;
|
||||
|
||||
/**
|
||||
* @author Artem Bilan
|
||||
* @since 3.0
|
||||
*/
|
||||
public class FtpServerRule extends ExternalResource {
|
||||
|
||||
public static int FTP_PORT = SocketUtils.findAvailableServerSocket();
|
||||
|
||||
private final TemporaryFolder ftpFolder;
|
||||
|
||||
private final TemporaryFolder localFolder;
|
||||
|
||||
private volatile File ftpRootFolder;
|
||||
|
||||
private volatile File sourceFtpDirectory;
|
||||
|
||||
private volatile File targetFtpDirectory;
|
||||
|
||||
private volatile File sourceLocalDirectory;
|
||||
|
||||
private volatile File targetLocalDirectory;
|
||||
|
||||
private volatile FtpServer server;
|
||||
|
||||
public FtpServerRule(final String root) {
|
||||
this.ftpFolder = new TemporaryFolder() {
|
||||
|
||||
@Override
|
||||
public void create() throws IOException {
|
||||
super.create();
|
||||
ftpRootFolder = this.newFolder(root);
|
||||
sourceFtpDirectory = new File(ftpRootFolder, "ftpSource");
|
||||
sourceFtpDirectory.mkdir();
|
||||
File file = new File(sourceFtpDirectory, "ftpSource1.txt");
|
||||
file.createNewFile();
|
||||
file = new File(sourceFtpDirectory, "ftpSource2.txt");
|
||||
file.createNewFile();
|
||||
|
||||
File subSourceFtpDirectory = new File(sourceFtpDirectory, "subFtpSource");
|
||||
subSourceFtpDirectory.mkdir();
|
||||
file = new File(subSourceFtpDirectory, "subFtpSource1.txt");
|
||||
file.createNewFile();
|
||||
|
||||
targetFtpDirectory = new File(ftpRootFolder, "ftpTarget");
|
||||
targetFtpDirectory.mkdirs();
|
||||
}
|
||||
};
|
||||
this.localFolder = new TemporaryFolder() {
|
||||
|
||||
@Override
|
||||
public void create() throws IOException {
|
||||
super.create();
|
||||
File rootFolder = this.newFolder(root);
|
||||
sourceLocalDirectory = new File(rootFolder, "localSource");
|
||||
sourceLocalDirectory.mkdirs();
|
||||
File file = new File(sourceLocalDirectory, "localSource1.txt");
|
||||
file.createNewFile();
|
||||
file = new File(sourceLocalDirectory, "localSource2.txt");
|
||||
file.createNewFile();
|
||||
|
||||
File subSourceLocalDirectory = new File(sourceLocalDirectory, "subLocalSource");
|
||||
subSourceLocalDirectory.mkdir();
|
||||
file = new File(subSourceLocalDirectory, "subLocalSource1.txt");
|
||||
file.createNewFile();
|
||||
|
||||
targetLocalDirectory = new File(rootFolder, "localTarget");
|
||||
targetLocalDirectory.mkdirs();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public File getSourceFtpDirectory() {
|
||||
return sourceFtpDirectory;
|
||||
}
|
||||
|
||||
public File getTargetFtpDirectory() {
|
||||
return targetFtpDirectory;
|
||||
}
|
||||
|
||||
public File getSourceLocalDirectory() {
|
||||
return sourceLocalDirectory;
|
||||
}
|
||||
|
||||
public File getTargetLocalDirectory() {
|
||||
return targetLocalDirectory;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void before() throws Throwable {
|
||||
this.ftpFolder.create();
|
||||
this.localFolder.create();
|
||||
|
||||
FtpServerFactory serverFactory = new FtpServerFactory();
|
||||
serverFactory.setUserManager(new TestUserManager(this.ftpRootFolder.getAbsolutePath()));
|
||||
|
||||
ListenerFactory factory = new ListenerFactory();
|
||||
factory.setPort(FTP_PORT);
|
||||
serverFactory.addListener("default", factory.createListener());
|
||||
|
||||
server = serverFactory.createServer();
|
||||
server.start();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void after() {
|
||||
this.server.stop();
|
||||
this.ftpFolder.delete();
|
||||
this.localFolder.delete();
|
||||
}
|
||||
|
||||
|
||||
public static void recursiveDelete(File file) {
|
||||
File[] files = file.listFiles();
|
||||
if (files != null) {
|
||||
for (File each : files) {
|
||||
recursiveDelete(each);
|
||||
}
|
||||
}
|
||||
file.delete();
|
||||
}
|
||||
|
||||
|
||||
private class TestUserManager implements UserManager {
|
||||
|
||||
private final BaseUser testUser;
|
||||
|
||||
private TestUserManager(String homeDirectory) {
|
||||
this.testUser = new BaseUser();
|
||||
this.testUser.setAuthorities(Arrays.asList(new ConcurrentLoginPermission(1024, 1024),
|
||||
new WritePermission(),
|
||||
new TransferRatePermission(1024, 1024)));
|
||||
this.testUser.setHomeDirectory(homeDirectory);
|
||||
this.testUser.setName("TEST_USER");
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public User getUserByName(String s) throws FtpException {
|
||||
return this.testUser;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getAllUserNames() throws FtpException {
|
||||
return new String[]{"TEST_USER"};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String s) throws FtpException {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(User user) throws FtpException {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean doesExist(String s) throws FtpException {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public User authenticate(Authentication authentication) throws AuthenticationFailedException {
|
||||
return this.testUser;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAdminName() throws FtpException {
|
||||
return "admin";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAdmin(String s) throws FtpException {
|
||||
return s.equals("admin");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -77,7 +77,7 @@ public class FtpOutboundGatewayParserTests {
|
||||
assertEquals("X", TestUtils.getPropertyValue(gateway, "remoteFileSeparator"));
|
||||
assertNotNull(TestUtils.getPropertyValue(gateway, "sessionFactory"));
|
||||
assertNotNull(TestUtils.getPropertyValue(gateway, "outputChannel"));
|
||||
assertEquals(new File("local-test-dir"), TestUtils.getPropertyValue(gateway, "localDirectory"));
|
||||
assertEquals("local-test-dir", TestUtils.getPropertyValue(gateway, "localDirectoryExpression.literalValue"));
|
||||
assertFalse((Boolean) TestUtils.getPropertyValue(gateway, "autoCreateLocalDirectory"));
|
||||
assertNotNull(TestUtils.getPropertyValue(gateway, "filter"));
|
||||
assertEquals(Command.LS, TestUtils.getPropertyValue(gateway, "command"));
|
||||
@@ -100,7 +100,7 @@ public class FtpOutboundGatewayParserTests {
|
||||
assertNotNull(TestUtils.getPropertyValue(gateway, "sessionFactory"));
|
||||
assertTrue(TestUtils.getPropertyValue(gateway, "sessionFactory") instanceof CachingSessionFactory);
|
||||
assertNotNull(TestUtils.getPropertyValue(gateway, "outputChannel"));
|
||||
assertEquals(new File("local-test-dir"), TestUtils.getPropertyValue(gateway, "localDirectory"));
|
||||
assertEquals("local-test-dir", TestUtils.getPropertyValue(gateway, "localDirectoryExpression.literalValue"));
|
||||
assertFalse((Boolean) TestUtils.getPropertyValue(gateway, "autoCreateLocalDirectory"));
|
||||
assertEquals(Command.GET, TestUtils.getPropertyValue(gateway, "command"));
|
||||
@SuppressWarnings("unchecked")
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<beans xmlns="http://www.springframework.org/schema/beans"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:int-ftp="http://www.springframework.org/schema/integration/ftp"
|
||||
xmlns:int="http://www.springframework.org/schema/integration"
|
||||
xsi:schemaLocation="http://www.springframework.org/schema/integration/ftp
|
||||
http://www.springframework.org/schema/integration/ftp/spring-integration-ftp.xsd
|
||||
http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration.xsd
|
||||
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
|
||||
|
||||
<bean id="ftpSessionFactory" class="org.springframework.integration.ftp.session.DefaultFtpSessionFactory">
|
||||
<property name="host" value="localhost"/>
|
||||
<property name="port" value="#{T(org.springframework.integration.ftp.FtpServerRule).FTP_PORT}"/>
|
||||
<property name="username" value="foo"/>
|
||||
<property name="password" value="foo"/>
|
||||
</bean>
|
||||
|
||||
<int:spel-function id="localDir" class="org.springframework.integration.ftp.outbound.FtpServerOutboundTests" method="localDirectory"/>
|
||||
|
||||
<int:channel id="output">
|
||||
<int:queue/>
|
||||
</int:channel>
|
||||
|
||||
<int:channel id="inboundGet"/>
|
||||
|
||||
<int-ftp:outbound-gateway session-factory="ftpSessionFactory"
|
||||
request-channel="inboundGet"
|
||||
command="get"
|
||||
expression="payload"
|
||||
local-directory-expression="#localDir() + #remoteDirectory.toUpperCase()"
|
||||
local-filename-generator-expression="#remoteFileName.replaceFirst('ftpSource', 'localTarget')"
|
||||
reply-channel="output"/>
|
||||
|
||||
<int:channel id="invalidDirExpression"/>
|
||||
|
||||
<int-ftp:outbound-gateway session-factory="ftpSessionFactory"
|
||||
request-channel="invalidDirExpression"
|
||||
command="get"
|
||||
expression="payload"
|
||||
local-directory-expression="T(java.io.File).separator + #remoteDirectory + '?:'"
|
||||
reply-channel="output"/>
|
||||
|
||||
<int:channel id="inboundMGet"/>
|
||||
|
||||
<int-ftp:outbound-gateway session-factory="ftpSessionFactory"
|
||||
request-channel="inboundMGet"
|
||||
command="mget"
|
||||
expression="payload"
|
||||
local-directory-expression="#localDir() + #remoteDirectory"
|
||||
local-filename-generator-expression="#remoteFileName.replaceFirst('ftpSource', 'localTarget')"
|
||||
reply-channel="output"/>
|
||||
|
||||
|
||||
</beans>
|
||||
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
* Copyright 2013 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.ftp.outbound;
|
||||
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.Before;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.integration.Message;
|
||||
import org.springframework.integration.channel.DirectChannel;
|
||||
import org.springframework.integration.core.PollableChannel;
|
||||
import org.springframework.integration.ftp.FtpServerRule;
|
||||
import org.springframework.integration.message.GenericMessage;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
|
||||
/**
|
||||
* @author Artem Bilan
|
||||
* @since 3.0
|
||||
*/
|
||||
@ContextConfiguration
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
public class FtpServerOutboundTests {
|
||||
|
||||
@ClassRule
|
||||
public static final FtpServerRule FTP_SERVER = new FtpServerRule(FtpServerOutboundTests.class.getSimpleName());
|
||||
|
||||
@Autowired
|
||||
private PollableChannel output;
|
||||
|
||||
@Autowired
|
||||
private DirectChannel inboundGet;
|
||||
|
||||
@Autowired
|
||||
private DirectChannel invalidDirExpression;
|
||||
|
||||
@Autowired
|
||||
private DirectChannel inboundMGet;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
FtpServerRule.recursiveDelete(FTP_SERVER.getTargetLocalDirectory());
|
||||
FtpServerRule.recursiveDelete(FTP_SERVER.getTargetFtpDirectory());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInt2866LocalDirectoryExpressionGET() {
|
||||
String dir = "ftpSource/";
|
||||
this.inboundGet.send(new GenericMessage<Object>(dir + "ftpSource1.txt"));
|
||||
Message<?> result = this.output.receive(1000);
|
||||
assertNotNull(result);
|
||||
File localFile = (File) result.getPayload();
|
||||
assertThat(localFile.getPath().replaceAll(java.util.regex.Matcher.quoteReplacement(File.separator), "/"),
|
||||
Matchers.containsString(dir.toUpperCase()));
|
||||
|
||||
dir = "ftpSource/subFtpSource/";
|
||||
this.inboundGet.send(new GenericMessage<Object>(dir + "subFtpSource1.txt"));
|
||||
result = this.output.receive(1000);
|
||||
assertNotNull(result);
|
||||
localFile = (File) result.getPayload();
|
||||
assertThat(localFile.getPath().replaceAll(java.util.regex.Matcher.quoteReplacement(File.separator), "/"),
|
||||
Matchers.containsString(dir.toUpperCase()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInt2866InvalidLocalDirectoryExpression() {
|
||||
try {
|
||||
this.invalidDirExpression.send(new GenericMessage<Object>("/ftpSource/ftpSource1.txt"));
|
||||
fail("Exception expected.");
|
||||
}
|
||||
catch (Exception e) {
|
||||
Throwable cause = e.getCause();
|
||||
assertThat(cause, Matchers.instanceOf(IllegalArgumentException.class));
|
||||
assertThat(cause.getMessage(), Matchers.startsWith("Failed to make local directory"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void testInt2866LocalDirectoryExpressionMGET() {
|
||||
String dir = "ftpSource/";
|
||||
this.inboundMGet.send(new GenericMessage<Object>(dir + "*.txt"));
|
||||
Message<?> result = this.output.receive(1000);
|
||||
assertNotNull(result);
|
||||
List<File> localFiles = (List<File>) result.getPayload();
|
||||
|
||||
for (File file : localFiles) {
|
||||
assertThat(file.getPath().replaceAll(java.util.regex.Matcher.quoteReplacement(File.separator), "/"),
|
||||
Matchers.containsString(dir));
|
||||
}
|
||||
|
||||
dir = "ftpSource/subFtpSource/";
|
||||
this.inboundMGet.send(new GenericMessage<Object>(dir + "*.txt"));
|
||||
result = this.output.receive(1000);
|
||||
assertNotNull(result);
|
||||
localFiles = (List<File>) result.getPayload();
|
||||
|
||||
for (File file : localFiles) {
|
||||
assertThat(file.getPath().replaceAll(java.util.regex.Matcher.quoteReplacement(File.separator), "/"),
|
||||
Matchers.containsString(dir));
|
||||
}
|
||||
}
|
||||
|
||||
public static String localDirectory() {
|
||||
return FTP_SERVER.getTargetLocalDirectory().getAbsolutePath() + File.separator;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user