Implemented FTPConnection

- Recursive downloading for FTP connections
 - Start of better environment config
This commit is contained in:
Josh Stark 2015-11-02 20:14:31 +00:00
parent fccef4298e
commit 0e3fdfedf0
11 changed files with 548 additions and 15 deletions

View File

@ -90,3 +90,11 @@ task updateBuildVersion << {
file('version.txt').text = "${updatedVersion}\n"
}
}
task copyConfig(type: Copy) {
from 'conf/app/application.${env}.properties'
from 'conf/app/log4j2.${env}.xml'
to 'src/main/resources/'
}

View File

View File

@ -28,7 +28,6 @@
<Root level="info">
<AppenderRef ref="Console" />
<!-- <AppenderRef ref="File" /> -->
</Root>
</Loggers>

View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
<Appenders>
<Console name="Console" target="SYSTEM_OUT" follow="true">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} - %5p - [%c{1}] - %msg%n" />
</Console>
<RollingFile name="File" fileName="davos.log"
filePattern="/config/logs/$${date:yyyy-MM}/app-%d{yyyy-MM-dd-HH}-%i.log">
<PatternLayout>
<Pattern>
%d{yyyy-MM-dd HH:mm:ss.SSS} - $5p - [%c{1}] - %msg%n
</Pattern>
</PatternLayout>
<Policies>
<SizeBasedTriggeringPolicy size="10 MB" />
</Policies>
</RollingFile>
</Appenders>
<Loggers>
<Logger name="io.linuxserver" level="debug" />
<Logger name="org.thymeleaf" level="warn" />
<Root level="info">
<AppenderRef ref="File" />
</Root>
</Loggers>
</Configuration>

View File

@ -6,15 +6,15 @@ public class FTPFile {
private String name;
private long size;
private String absolutePath;
private String path;
private DateTime lastModified;
private boolean directory;
public FTPFile(String name, long size, String absolutePath, long mTime, boolean directory) {
public FTPFile(String name, long size, String path, long mTime, boolean directory) {
this.name = name;
this.size = size;
this.absolutePath = absolutePath;
this.path = path;
this.lastModified = new DateTime(mTime);
this.directory = directory;
}
@ -28,7 +28,7 @@ public class FTPFile {
}
public String getPath() {
return absolutePath;
return path;
}
public DateTime getLastModified() {

View File

@ -0,0 +1,8 @@
package io.linuxserver.davos.transfer.ftp.client;
public class FTPSClient extends FTPClient {
public FTPSClient() {
ftpClient = new org.apache.commons.net.ftp.FTPSClient("SSL", true);
}
}

View File

@ -1,37 +1,141 @@
package io.linuxserver.davos.transfer.ftp.connection;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.linuxserver.davos.transfer.ftp.FTPFile;
import io.linuxserver.davos.transfer.ftp.exception.DownloadFailedException;
import io.linuxserver.davos.transfer.ftp.exception.FileListingException;
import io.linuxserver.davos.util.FileStreamFactory;
import io.linuxserver.davos.util.FileUtils;
public class FTPConnection implements Connection {
private static final Logger LOGGER = LoggerFactory.getLogger(FTPConnection.class);
private org.apache.commons.net.ftp.FTPClient client;
private FileStreamFactory fileStreamFactory = new FileStreamFactory();
private FileUtils fileUtils = new FileUtils();
public FTPConnection(org.apache.commons.net.ftp.FTPClient client) {
// TODO Auto-generated constructor stub
this.client = client;
}
@Override
public String currentDirectory() {
// TODO Auto-generated method stub
return null;
try {
return client.printWorkingDirectory();
} catch (IOException e) {
throw new FileListingException("Unable to print the working directory", e);
}
}
@Override
public void download(FTPFile remoteFilePath, String localFilePath) {
// TODO Auto-generated method stub
public void download(FTPFile file, String localFilePath) {
String cleanRemotePath = FileUtils.ensureTrailingSlash(file.getPath()) + file.getName();
String cleanLocalPath = FileUtils.ensureTrailingSlash(localFilePath);
try {
if (file.isDirectory())
downloadDirectoryAndContents(file, cleanLocalPath, cleanRemotePath);
else
doDownload(file, cleanRemotePath, cleanLocalPath);
} catch (FileNotFoundException e) {
throw new DownloadFailedException(
String.format("Unable to write to local directory %s", cleanLocalPath + file.getName()), e);
} catch (IOException e) {
throw new DownloadFailedException(String.format("Unable to download file %s", cleanRemotePath), e);
}
}
@Override
public List<FTPFile> listFiles() {
// TODO Auto-generated method stub
return null;
return listFiles(currentDirectory());
}
@Override
public List<FTPFile> listFiles(String remoteDirectory) {
// TODO Auto-generated method stub
return null;
List<FTPFile> files = new ArrayList<FTPFile>();
try {
String cleanRemoteDirectory = FileUtils.ensureTrailingSlash(remoteDirectory);
LOGGER.debug("Listing all files in {}", cleanRemoteDirectory);
org.apache.commons.net.ftp.FTPFile[] ftpFiles = client.listFiles(cleanRemoteDirectory);
for (org.apache.commons.net.ftp.FTPFile file : ftpFiles)
files.add(toFtpFile(file, cleanRemoteDirectory));
} catch (IOException e) {
throw new FileListingException(String.format("Unable to list files in directory %s", remoteDirectory), e);
}
return files.stream().filter(removeCurrentAndParentDirs()).collect(Collectors.toList());
}
private void doDownload(FTPFile file, String cleanRemotePath, String cleanLocalPath)
throws FileNotFoundException, IOException {
LOGGER.info("Downloading {} to {}", cleanRemotePath, cleanLocalPath);
LOGGER.debug("Creating output stream for file {}", cleanLocalPath + file.getName());
OutputStream outputStream = fileStreamFactory.createOutputStream(cleanLocalPath + file.getName());
boolean hasDownloaded = client.retrieveFile(cleanRemotePath, outputStream);
outputStream.close();
if (!hasDownloaded)
throw new DownloadFailedException("Server returned failure while downloading.");
}
private void downloadDirectoryAndContents(FTPFile file, String localDownloadFolder, String path) throws IOException {
LOGGER.info("Item {} is a directory. Will now check sub-items", file.getName());
List<FTPFile> subItems = listFiles(path).stream().filter(removeCurrentAndParentDirs()).collect(Collectors.toList());
String fullLocalDownloadPath = FileUtils.ensureTrailingSlash(localDownloadFolder + file.getName());
LOGGER.debug("Creating new local directory {}", fullLocalDownloadPath);
fileUtils.createLocalDirectory(fullLocalDownloadPath);
for (FTPFile subItem : subItems) {
String subItemPath = FileUtils.ensureTrailingSlash(subItem.getPath()) + subItem.getName();
if (subItem.isDirectory()) {
String subLocalFilePath = FileUtils.ensureTrailingSlash(fullLocalDownloadPath);
downloadDirectoryAndContents(subItem, subLocalFilePath, FileUtils.ensureTrailingSlash(subItemPath));
}
else
doDownload(subItem, subItemPath, fullLocalDownloadPath);
}
}
private Predicate<? super FTPFile> removeCurrentAndParentDirs() {
return file -> !file.getName().equals(".") && !file.getName().equals("..");
}
private FTPFile toFtpFile(org.apache.commons.net.ftp.FTPFile ftpFile, String filePath) throws IOException {
String name = ftpFile.getName();
long fileSize = ftpFile.getSize();
long mTime = ftpFile.getTimestamp().getTime().getTime();
boolean isDirectory = ftpFile.isDirectory();
return new FTPFile(name, fileSize, filePath, mTime, isDirectory);
}
}

View File

@ -0,0 +1,17 @@
package io.linuxserver.davos.util;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
public class FileStreamFactory {
public FileInputStream createInputStream(String filePath) throws FileNotFoundException {
return new FileInputStream(new File(filePath));
}
public FileOutputStream createOutputStream(String filePath) throws FileNotFoundException {
return new FileOutputStream(new File(filePath));
}
}

View File

@ -0,0 +1,15 @@
package io.linuxserver.davos.transfer.ftp.client;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.Test;
public class FTPSClientTest {
private FTPClient client = new FTPSClient();
@Test
public void newFtpsClientShouldCreateFTPSClientInstance() {
assertThat(client.ftpClient).isInstanceOf(org.apache.commons.net.ftp.FTPSClient.class);
}
}

View File

@ -0,0 +1,347 @@
package io.linuxserver.davos.transfer.ftp.connection;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.mockito.InOrder;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import io.linuxserver.davos.transfer.ftp.FTPFile;
import io.linuxserver.davos.transfer.ftp.exception.DownloadFailedException;
import io.linuxserver.davos.transfer.ftp.exception.FileListingException;
import io.linuxserver.davos.util.FileStreamFactory;
import io.linuxserver.davos.util.FileUtils;
public class FTPConnectionTest {
private static final String LOCAL_DIRECTORY = ".";
private static final String DIRECTORY_PATH = "this/is/a/directory";
@InjectMocks
private FTPConnection ftpConnection;
@Mock
private FileStreamFactory mockFileStreamFactory;
@Mock
private FileUtils mockFileUtils;
@Mock
private FileOutputStream mockFileOutputStream;
private org.apache.commons.net.ftp.FTPClient mockFtpClient;
@Rule
public ExpectedException expectedException = ExpectedException.none();
@Before
public void setUp() throws IOException {
mockFtpClient = mock(org.apache.commons.net.ftp.FTPClient.class);
when(mockFtpClient.changeWorkingDirectory(anyString())).thenReturn(true);
when(mockFtpClient.printWorkingDirectory()).thenReturn(DIRECTORY_PATH);
when(mockFtpClient.retrieveFile(anyString(), any(OutputStream.class))).thenReturn(true);
org.apache.commons.net.ftp.FTPFile[] files = createRemoteFTPFiles();
ftpConnection = new FTPConnection(mockFtpClient);
initMocks(this);
when(mockFtpClient.listFiles(anyString())).thenReturn(files);
when(mockFileStreamFactory.createOutputStream("./remote.file")).thenReturn(mockFileOutputStream);
}
@Test
public void whenListingFilesThenFtpClientListFilesMethodShouldBeCalledForCurrentWorkingDirectory() throws IOException {
ftpConnection.listFiles();
verify(mockFtpClient).listFiles("this/is/a/directory/");
}
@Test
public void ifWhenListingFilesFtpClientThrowsExceptionThenCatchAndRethrowFileListingExcepton() throws IOException {
expectedException.expect(FileListingException.class);
expectedException.expectMessage(is(equalTo("Unable to list files in directory " + DIRECTORY_PATH)));
when(mockFtpClient.listFiles("this/is/a/directory/")).thenThrow(new IOException());
ftpConnection.listFiles();
}
@Test
public void whenListingFilesThenFileArrayThatListFilesReturnsShouldBeConvertedToListOfFtpFilesAndReturned()
throws IOException {
List<FTPFile> returnedFiles = ftpConnection.listFiles();
assertThat(returnedFiles.get(0).getName()).isEqualTo("File 1");
assertThat(returnedFiles.get(0).getSize()).isEqualTo(1000l);
assertThat(returnedFiles.get(0).getPath()).isEqualTo("this/is/a/directory/");
assertThat(returnedFiles.get(0).isDirectory()).isFalse();
assertThat(returnedFiles.get(1).getName()).isEqualTo("File 2");
assertThat(returnedFiles.get(1).getSize()).isEqualTo(2000l);
assertThat(returnedFiles.get(1).getPath()).isEqualTo("this/is/a/directory/");
assertThat(returnedFiles.get(1).isDirectory()).isTrue();
assertThat(returnedFiles.get(2).getName()).isEqualTo("File 3");
assertThat(returnedFiles.get(2).getSize()).isEqualTo(3000l);
assertThat(returnedFiles.get(2).getPath()).isEqualTo("this/is/a/directory/");
assertThat(returnedFiles.get(2).isDirectory()).isFalse();
}
@Test
public void returnedFtpFilesShouldHaveCorrectModifiedDateTimesAgainstThem() {
List<FTPFile> files = ftpConnection.listFiles();
assertThat(files.get(0).getLastModified().toString("dd/MM/yyyy HH:mm:ss")).isEqualTo("19/03/2014 21:40:00");
assertThat(files.get(1).getLastModified().toString("dd/MM/yyyy HH:mm:ss")).isEqualTo("19/03/2014 21:40:00");
assertThat(files.get(2).getLastModified().toString("dd/MM/yyyy HH:mm:ss")).isEqualTo("19/03/2014 21:40:00");
}
@Test
public void whenListingFilesAndGivingRelativePathThenThatPathShouldBeUsedAlongsideCurrentWorkingDir() throws IOException {
ftpConnection.listFiles("relativePath");
verify(mockFtpClient).listFiles("relativePath/");
}
@Test
public void downloadMethodShouldCreateLocalFileStreamFromCorrectPathBasedOnRemoteFileName() throws FileNotFoundException {
FTPFile file = new FTPFile("remote.file", 0l, "path/to", 0, false);
ftpConnection.download(file, LOCAL_DIRECTORY);
verify(mockFileStreamFactory).createOutputStream(LOCAL_DIRECTORY + "/remote.file");
}
@Test
public void downloadMethodShouldCallOnFtpClientRetrieveFilesMethodWithRemoteFilename() throws IOException {
FTPFile file = new FTPFile("remote.file", 0l, "path/to", 0, false);
ftpConnection.download(file, LOCAL_DIRECTORY);
verify(mockFtpClient).retrieveFile("path/to/remote.file", mockFileOutputStream);
}
@Test
public void downloadMethodShouldThrowExceptionIfUnableToOpenStreamToLocalFile() throws IOException {
expectedException.expect(DownloadFailedException.class);
expectedException.expectMessage(is(equalTo("Unable to write to local directory " + LOCAL_DIRECTORY + "/remote.file")));
when(mockFtpClient.retrieveFile("path/to/remote.file", mockFileOutputStream)).thenThrow(new FileNotFoundException());
FTPFile file = new FTPFile("remote.file", 0l, "path/to", 0, false);
ftpConnection.download(file, LOCAL_DIRECTORY);
}
@Test
public void shouldDownloadFailForAnyReasonWhileInProgressThenCatchIOExceptionAndThrowNewDownloadFailedException()
throws IOException {
expectedException.expect(DownloadFailedException.class);
expectedException.expectMessage(is(equalTo("Unable to download file path/to/remote.file")));
when(mockFtpClient.retrieveFile("path/to/remote.file", mockFileOutputStream)).thenThrow(new IOException());
FTPFile file = new FTPFile("remote.file", 0l, "path/to", 0, false);
ftpConnection.download(file, LOCAL_DIRECTORY);
}
@Test
public void ifRetrieveFileMethodInClientReturnsFalseThenThrowDownloadFailedException() throws IOException {
expectedException.expect(DownloadFailedException.class);
expectedException.expectMessage(is(equalTo("Server returned failure while downloading.")));
when(mockFtpClient.retrieveFile("path/to/remote.file", mockFileOutputStream)).thenReturn(false);
FTPFile file = new FTPFile("remote.file", 0l, "path/to", 0, false);
ftpConnection.download(file, LOCAL_DIRECTORY);
}
@Test
public void printingWorkingDirectoryShouldCallOnUnderlyingClientMethodToGetCurrentDirectory() throws IOException {
ftpConnection.currentDirectory();
verify(mockFtpClient).printWorkingDirectory();
}
@Test
public void printingWorkingDirectoryShouldReturnExactlyWhatTheUnderlyingClientReturns() {
assertThat(ftpConnection.currentDirectory()).isEqualTo(DIRECTORY_PATH);
}
@Test
public void ifClientThrowsExceptionWhenTryingToGetWorkingDirectoryThenCatchExceptionAndRethrow() throws IOException {
expectedException.expect(FileListingException.class);
expectedException.expectMessage(is(equalTo("Unable to print the working directory")));
when(mockFtpClient.printWorkingDirectory()).thenThrow(new IOException());
ftpConnection.currentDirectory();
}
@Test
public void downloadShouldRecursivelyCheckFileIfFolderThenLsThatAndGetOnlyFiles() throws IOException {
initRecursiveListings();
FileOutputStream stream1 = mock(FileOutputStream.class);
FileOutputStream stream2 = mock(FileOutputStream.class);
FileOutputStream stream3 = mock(FileOutputStream.class);
FileOutputStream stream4 = mock(FileOutputStream.class);
FileOutputStream stream5 = mock(FileOutputStream.class);
FileOutputStream stream6 = mock(FileOutputStream.class);
when(mockFileStreamFactory.createOutputStream("some/directory/folder/file1.txt")).thenReturn(stream1);
when(mockFileStreamFactory.createOutputStream("some/directory/folder/file2.txt")).thenReturn(stream2);
when(mockFileStreamFactory.createOutputStream("some/directory/folder/directory1/file3.txt")).thenReturn(stream3);
when(mockFileStreamFactory.createOutputStream("some/directory/folder/directory1/directory2/file5.txt"))
.thenReturn(stream4);
when(mockFileStreamFactory.createOutputStream("some/directory/folder/directory1/directory2/file6.txt"))
.thenReturn(stream5);
when(mockFileStreamFactory.createOutputStream("some/directory/folder/directory1/file4.txt")).thenReturn(stream6);
FTPFile directory = new FTPFile("folder", 0, "path/to", 0, true);
ftpConnection.download(directory, "some/directory");
verify(mockFileUtils).createLocalDirectory("some/directory/folder/");
verify(mockFtpClient).listFiles("path/to/folder/");
verify(mockFileUtils).createLocalDirectory("some/directory/folder/directory1/");
verify(mockFtpClient).listFiles("path/to/folder/directory1/");
verify(mockFileUtils).createLocalDirectory("some/directory/folder/directory1/directory2/");
verify(mockFtpClient).listFiles("path/to/folder/directory1/directory2/");
InOrder inOrder = Mockito.inOrder(mockFtpClient, stream1, stream2, stream3, stream4, stream5, stream6);
inOrder.verify(mockFtpClient).retrieveFile("path/to/folder/file1.txt", stream1);
inOrder.verify(stream1).close();
inOrder.verify(mockFtpClient).retrieveFile("path/to/folder/file2.txt", stream2);
inOrder.verify(stream2).close();
inOrder.verify(mockFtpClient).retrieveFile("path/to/folder/directory1/file3.txt", stream3);
inOrder.verify(stream3).close();
inOrder.verify(mockFtpClient).retrieveFile("path/to/folder/directory1/directory2/file5.txt", stream4);
inOrder.verify(stream4).close();
inOrder.verify(mockFtpClient).retrieveFile("path/to/folder/directory1/directory2/file6.txt", stream5);
inOrder.verify(stream5).close();
inOrder.verify(mockFtpClient).retrieveFile("path/to/folder/directory1/file4.txt", stream6);
inOrder.verify(stream6).close();
}
private void initRecursiveListings() throws IOException {
org.apache.commons.net.ftp.FTPFile[] entries = new org.apache.commons.net.ftp.FTPFile[5];
entries[0] = (createSingleEntry(".", 123l, 1394525265, true));
entries[1] = (createSingleEntry("..", 123l, 1394525265, true));
entries[2] = (createSingleEntry("file1.txt", 123l, 1394525265, false));
entries[3] = (createSingleEntry("file2.txt", 456l, 1394652161, false));
entries[4] = (createSingleEntry("directory1", 789l, 1391879364, true));
when(mockFtpClient.listFiles("path/to/folder/")).thenReturn(entries);
org.apache.commons.net.ftp.FTPFile[] subEntries = new org.apache.commons.net.ftp.FTPFile[5];
subEntries[0] = (createSingleEntry(".", 123l, 1394525265, true));
subEntries[1] = (createSingleEntry("..", 123l, 1394525265, true));
subEntries[2] = (createSingleEntry("file3.txt", 789l, 1394525265, false));
subEntries[3] = (createSingleEntry("directory2", 789l, 1394525265, true));
subEntries[4] = (createSingleEntry("file4.txt", 789l, 1394525265, false));
when(mockFtpClient.listFiles("path/to/folder/directory1/")).thenReturn(subEntries);
org.apache.commons.net.ftp.FTPFile[] subSubEntries = new org.apache.commons.net.ftp.FTPFile[4];
subSubEntries[0] = (createSingleEntry(".", 123l, 1394525265, true));
subSubEntries[1] = (createSingleEntry("..", 123l, 1394525265, true));
subSubEntries[2] = (createSingleEntry("file5.txt", 789l, 1394525265, false));
subSubEntries[3] = (createSingleEntry("file6.txt", 789l, 1394525265, false));
when(mockFtpClient.listFiles("path/to/folder/directory1/directory2/")).thenReturn(subSubEntries);
}
private org.apache.commons.net.ftp.FTPFile createSingleEntry(String fileName, long size, int mTime, boolean directory) {
org.apache.commons.net.ftp.FTPFile file = mock(org.apache.commons.net.ftp.FTPFile.class);
Calendar calendar = Calendar.getInstance();
calendar.setTime(new Date(mTime));
when(file.getName()).thenReturn(fileName);
when(file.getTimestamp()).thenReturn(calendar);
when(file.getSize()).thenReturn(size);
when(file.isDirectory()).thenReturn(directory);
return file;
}
private org.apache.commons.net.ftp.FTPFile[] createRemoteFTPFiles() {
Calendar calendar = Calendar.getInstance();
calendar.set(2014, 2, 19, 21, 40, 00);
org.apache.commons.net.ftp.FTPFile[] files = new org.apache.commons.net.ftp.FTPFile[5];
org.apache.commons.net.ftp.FTPFile currentDir = mock(org.apache.commons.net.ftp.FTPFile.class);
when(currentDir.getName()).thenReturn(".");
when(currentDir.getTimestamp()).thenReturn(calendar);
org.apache.commons.net.ftp.FTPFile parentDir = mock(org.apache.commons.net.ftp.FTPFile.class);
when(parentDir.getName()).thenReturn("..");
when(parentDir.getTimestamp()).thenReturn(calendar);
files[0] = currentDir;
files[1] = parentDir;
for (int i = 2; i < 5; i++) {
org.apache.commons.net.ftp.FTPFile file = mock(org.apache.commons.net.ftp.FTPFile.class);
when(file.getName()).thenReturn("File " + (i - 1));
when(file.getSize()).thenReturn((long) (i - 1) * 1000);
when(file.getTimestamp()).thenReturn(calendar);
when(file.isDirectory()).thenReturn(setTrueIfNumberIsEven(i));
files[i] = file;
}
return files;
}
private boolean setTrueIfNumberIsEven(int i) {
return (i + 1) % 2 == 0 ? true : false;
}
}