Implement FTP Client (#1064)

* Start with tests against fake FTP

* Dummy test file

* Switch to our FTPClient, make tests break

* Implement FTPClient

* Rely on end caller to set path delimiters

* User port 8021 for tests (not protected in azure pipelines)

* Let mocks use Windows filesystem

* Gentle refactor

Co-authored-by: dotasek <david.otasek@smilecdr.com>
This commit is contained in:
dotasek 2023-01-09 20:06:21 -05:00 committed by GitHub
parent 02cdad6f68
commit edf6d75551
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 211 additions and 7 deletions

View File

@ -86,6 +86,12 @@
<version>72.1</version>
</dependency>
<dependency>
<groupId>commons-net</groupId>
<artifactId>commons-net</artifactId>
<version>3.6</version>
</dependency>
<!-- JUnit Jupiter -->
<dependency>
<groupId>org.junit.jupiter</groupId>
@ -142,7 +148,12 @@
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.mockftpserver</groupId>
<artifactId>MockFtpServer</artifactId>
<version>3.1.0</version>
<scope>test</scope>
</dependency>
<!--
<dependency>

View File

@ -1,21 +1,67 @@
package org.hl7.fhir.utilities;
import org.apache.commons.net.ftp.FTPReply;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
public class FTPClient {
private final org.apache.commons.net.ftp.FTPClient clientImpl;
final String server;
final String path;
final String user;
final String password;
final int port;
/**
* Connect to an FTP server
* @param server - the server to connect to (uusally just an IP address). It's up to the system to figure out access (VPN etc)
* @param server - the server to connect to (usually just an IP address). It's up to the system to figure out access (VPN etc)
* @param path - the path on the FTP server to treat all the operations as relative to
* @param user - username for the FTP server
* @param password - password for the FTP server
*/
public FTPClient(String server, String path, String user, String password) {
this (server, -1, path, user, password);
}
protected FTPClient(String server, int port, String path, String user, String password) {
this.server = server;
this.port = port;
this.path = path;
this.user = user;
this.password = password;
clientImpl = new org.apache.commons.net.ftp.FTPClient();
}
/**
* Connect to the server, throw an exception if it fails
*/
public void connect() {
public void connect() throws IOException {
if (port != -1) {
clientImpl.connect(server, port);
}
else {
clientImpl.connect(server);
}
clientImpl.login(user, password);
clientImpl.getSystemType();
int reply = clientImpl.getReplyCode();
if(!FTPReply.isPositiveCompletion(reply)) {
clientImpl.disconnect();
throw new IOException("FTP server refused connection.");
}
}
/**
@ -23,7 +69,13 @@ public class FTPClient {
*
* @param path - relative to the path provided in the constructor
*/
public void delete(String path) {
public void delete(String path) throws IOException {
String resolvedPath = resolveRemotePath(path);
clientImpl.deleteFile(resolvedPath);
}
private String resolveRemotePath(String path) {
return String.join("", this.path, path);
}
/**
@ -31,9 +83,10 @@ public class FTPClient {
* @param source - absolute path on local system
* @param path - relative to the path provided in the constructor
*/
public void upload(String source, String path) {
public void upload(String source, String path) throws IOException {
String resolvedPath = resolveRemotePath(path);
FileInputStream localStream = new FileInputStream(new File(source));
clientImpl.storeFile( resolvedPath, localStream);
}
}

View File

@ -0,0 +1,140 @@
package org.hl7.fhir.utilities;
import org.hl7.fhir.utilities.tests.ResourceLoaderTests;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.mockftpserver.fake.FakeFtpServer;
import org.mockftpserver.fake.UserAccount;
import org.mockftpserver.fake.filesystem.*;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class FTPClientTest implements ResourceLoaderTests {
public static final String DUMMY_PASSWORD = "dummyPassword123";
public static final String DUMMY_USER = "dummyUser";
public static final String RELATIVE_PATH_1 = "relativePath1";
public static final String RELATIVE_PATH_2 = "relativePath2";
public static final String DUMMY_FILE_TO_DELETE = "dummyFileToDelete";
public static final String DUMMY_FILE_TO_UPLOAD = "dummyFileToUpload";
public static final int FAKE_FTP_PORT = 8021;
FakeFtpServer fakeFtpServer;
Path fakeFtpDirectory;
Path relativePath1;
Path relativePath2;
Path dummyFileToDeletePath;
Path dummyFileToUploadPath;
Path dummyUploadedFilePath;
@BeforeAll
public void setup() throws IOException {
setupDummyFileToUpload();
setupFakeFtpDirectory();
setupFakeFtpServer();
}
private void setupDummyFileToUpload() throws IOException {
dummyFileToUploadPath = Files.createTempFile("dummyFtpFileToUpload", "dummy");
}
public void setupFakeFtpServer() throws IOException {
fakeFtpServer = new FakeFtpServer();
fakeFtpServer.setServerControlPort(FAKE_FTP_PORT);
fakeFtpServer.addUserAccount(new UserAccount(DUMMY_USER, DUMMY_PASSWORD, fakeFtpDirectory.toFile().getAbsolutePath()));
FileSystem fileSystem = useWindowsFileSystem()
? new WindowsFakeFileSystem()
: new UnixFakeFileSystem();
fileSystem.add(new DirectoryEntry(fakeFtpDirectory.toFile().getAbsolutePath()));
fileSystem.add(new DirectoryEntry(relativePath1.toFile().getAbsolutePath()));
fileSystem.add(new DirectoryEntry(relativePath2.toFile().getAbsolutePath()));
fileSystem.add(new FileEntry(dummyFileToDeletePath.toFile().getAbsolutePath()));
//fileSystem.add(new FileEntry("c:\\data\\run.exe"));
fakeFtpServer.setFileSystem(fileSystem);
fakeFtpServer.start();
}
private static boolean useWindowsFileSystem() {
return System.getProperty("os.name") != null && System.getProperty("os.name").startsWith("Windows");
}
private void setupFakeFtpDirectory() throws IOException {
fakeFtpDirectory = Files.createTempDirectory("fakeFtp");
relativePath1 = fakeFtpDirectory.resolve(RELATIVE_PATH_1);
relativePath2 = relativePath1.resolve(RELATIVE_PATH_2);
Files.createDirectory(relativePath1);
Files.createDirectory(relativePath2);
dummyFileToDeletePath = Files.createFile(relativePath2.resolve(DUMMY_FILE_TO_DELETE));
dummyUploadedFilePath = relativePath2.resolve(DUMMY_FILE_TO_UPLOAD);
}
@AfterAll
public void tearDownFakeFtpServer() {
fakeFtpServer.stop();
}
@Test
public void testDelete() throws IOException {
FTPClient client = connectToFTPClient();
String deleteFilePath = dummyFileToDeletePath.toFile().getAbsolutePath();
assertTrue(fakeFtpServer.getFileSystem().exists(deleteFilePath));
client.delete( RELATIVE_PATH_2 + "/" + DUMMY_FILE_TO_DELETE);
assertFalse(fakeFtpServer.getFileSystem().exists(deleteFilePath));
}
@NotNull
private static FTPClient connectToFTPClient() throws IOException {
FTPClient client = new FTPClient("localhost", FAKE_FTP_PORT, RELATIVE_PATH_1 + "/", DUMMY_USER, DUMMY_PASSWORD);
client.connect();
return client;
}
@Test
public void testUpload() throws IOException {
FTPClient client = connectToFTPClient();
String uploadFilePath = dummyUploadedFilePath.toFile().getAbsolutePath();
assertFalse(fakeFtpServer.getFileSystem().exists(uploadFilePath));
client.upload(dummyFileToUploadPath.toFile().getAbsolutePath(), RELATIVE_PATH_2 + "/" + DUMMY_FILE_TO_UPLOAD);
assertTrue(fakeFtpServer.getFileSystem().exists(uploadFilePath));
}
}