«1. Обзор

В этом руководстве мы рассмотрим, как использовать библиотеку Apache Commons Net для взаимодействия с внешним FTP-сервером.

2. Настройка

При использовании библиотек, которые используются для взаимодействия с внешними системами, часто полезно написать несколько дополнительных интеграционных тестов, чтобы убедиться, что мы используем библиотеку правильно.

В настоящее время мы обычно используем Docker для раскрутки этих систем для наших интеграционных тестов. Однако, особенно при использовании в пассивном режиме, FTP-сервер — не самое простое приложение для прозрачного запуска внутри контейнера, если мы хотим использовать динамические сопоставления портов (что часто необходимо для запуска тестов на общем сервере CI). ).

Вот почему вместо этого мы будем использовать MockFtpServer, FTP-сервер Fake/Stub, написанный на Java, который предоставляет расширенный API для удобного использования в тестах JUnit:

<dependency>
    <groupId>commons-net</groupId>
    <artifactId>commons-net</artifactId>
    <version>3.6</version>
</dependency>
<dependency> 
    <groupId>org.mockftpserver</groupId> 
    <artifactId>MockFtpServer</artifactId> 
    <version>2.7.1</version> 
    <scope>test</scope> 
</dependency>

Рекомендуется всегда использовать последнюю версию. Их можно найти здесь и здесь.

3. Поддержка FTP в JDK

Удивительно, но в некоторых разновидностях JDK уже есть базовая поддержка FTP в форме sun.net.www.protocol.ftp.FtpURLConnection.

Однако мы не должны использовать этот класс напрямую, вместо этого можно использовать класс JDK java.net.URL как абстракцию.

Эта поддержка FTP является очень простой, но с использованием удобных API-интерфейсов java.nio.file.Files этого может быть достаточно для простых случаев использования:

@Test
public void givenRemoteFile_whenDownloading_thenItIsOnTheLocalFilesystem() throws IOException {
    String ftpUrl = String.format(
      "ftp://user:[email protected]:%d/foobar.txt", fakeFtpServer.getServerControlPort());

    URLConnection urlConnection = new URL(ftpUrl).openConnection();
    InputStream inputStream = urlConnection.getInputStream();
    Files.copy(inputStream, new File("downloaded_buz.txt").toPath());
    inputStream.close();

    assertThat(new File("downloaded_buz.txt")).exists();

    new File("downloaded_buz.txt").delete(); // cleanup
}

Поскольку в этой базовой поддержке FTP уже отсутствуют основные функции, такие как файлов, мы собираемся использовать поддержку FTP в библиотеке Apache Net Commons в следующих примерах.

4. Подключение

Сначала нам нужно подключиться к FTP-серверу. Начнем с создания класса FtpClient.


Он будет служить API-интерфейсом абстракции для настоящего FTP-клиента Apache Commons Net:

class FtpClient {

    private String server;
    private int port;
    private String user;
    private String password;
    private FTPClient ftp;

    // constructor

    void open() throws IOException {
        ftp = new FTPClient();

        ftp.addProtocolCommandListener(new PrintCommandListener(new PrintWriter(System.out)));

        ftp.connect(server, port);
        int reply = ftp.getReplyCode();
        if (!FTPReply.isPositiveCompletion(reply)) {
            ftp.disconnect();
            throw new IOException("Exception in connecting to FTP Server");
        }

        ftp.login(user, password);
    }

    void close() throws IOException {
        ftp.disconnect();
    }
}

Нам нужны адрес сервера и порт, а также имя пользователя и пароль. После подключения необходимо действительно проверить ответный код, чтобы убедиться, что подключение прошло успешно. Мы также добавляем PrintCommandListener для печати ответов, которые мы обычно видим при подключении к FTP-серверу с помощью инструментов командной строки, на стандартный вывод.

Так как наши интеграционные тесты будут иметь шаблонный код, например запуск/остановку MockFtpServer и подключение/отключение нашего клиента, мы можем сделать эти вещи в методах @Before и @After: порт управления сервером на значение 0, мы запускаем фиктивный сервер и свободный случайный порт.

public class FtpClientIntegrationTest {

    private FakeFtpServer fakeFtpServer;

    private FtpClient ftpClient;

    @Before
    public void setup() throws IOException {
        fakeFtpServer = new FakeFtpServer();
        fakeFtpServer.addUserAccount(new UserAccount("user", "password", "/data"));

        FileSystem fileSystem = new UnixFakeFileSystem();
        fileSystem.add(new DirectoryEntry("/data"));
        fileSystem.add(new FileEntry("/data/foobar.txt", "abcdef 1234567890"));
        fakeFtpServer.setFileSystem(fileSystem);
        fakeFtpServer.setServerControlPort(0);

        fakeFtpServer.start();

        ftpClient = new FtpClient("localhost", fakeFtpServer.getServerControlPort(), "user", "password");
        ftpClient.open();
    }

    @After
    public void teardown() throws IOException {
        ftpClient.close();
        fakeFtpServer.stop();
    }
}

Вот почему мы должны получить фактический порт при создании FtpClient после запуска сервера, используя fakeFtpServer.getServerControlPort().

5. Список файлов

Первым фактическим вариантом использования будет список файлов.

Начнем с теста в стиле TDD:

Сама реализация столь же проста. Чтобы сделать возвращаемую структуру данных немного проще для этого примера, мы преобразуем возвращенный массив FTPFile в список строк с использованием потоков Java 8:

@Test
public void givenRemoteFile_whenListingRemoteFiles_thenItIsContainedInList() throws IOException {
    Collection<String> files = ftpClient.listFiles("");
    assertThat(files).contains("foobar.txt");
}

6. Загрузка

Collection<String> listFiles(String path) throws IOException {
    FTPFile[] files = ftp.listFiles(path);
    return Arrays.stream(files)
      .map(FTPFile::getName)
      .collect(Collectors.toList());
}

Для загрузки файла с FTP-сервера мы определяем API.

Здесь мы определяем исходный файл и место назначения в локальной файловой системе:

FTP-клиент Apache Net Commons содержит удобный API, который будет напрямую записывать в определенный OutputStream. Это означает, что мы можем использовать это напрямую:

@Test
public void givenRemoteFile_whenDownloading_thenItIsOnTheLocalFilesystem() throws IOException {
    ftpClient.downloadFile("/buz.txt", "downloaded_buz.txt");
    assertThat(new File("downloaded_buz.txt")).exists();
    new File("downloaded_buz.txt").delete(); // cleanup
}

7. Загрузка

void downloadFile(String source, String destination) throws IOException {
    FileOutputStream out = new FileOutputStream(destination);
    ftp.retrieveFile(source, out);
}

MockFtpServer предоставляет несколько полезных методов для доступа к содержимому своей файловой системы. Мы можем использовать эту функцию, чтобы написать простой интеграционный тест для загрузки:

8. Заключение

@Test
public void givenLocalFile_whenUploadingIt_thenItExistsOnRemoteLocation() 
  throws URISyntaxException, IOException {
  
    File file = new File(getClass().getClassLoader().getResource("baz.txt").toURI());
    ftpClient.putFileToPath(file, "/buz.txt");
    assertThat(fakeFtpServer.getFileSystem().exists("/buz.txt")).isTrue();
}

Мы видели, что использование Java вместе с Apache Net Commons позволяет нам легко взаимодействовать с внешним FTP-сервером как для чтения, так и для записи.

void putFileToPath(File file, String path) throws IOException {
    ftp.storeFile(path, new FileInputStream(file));
}

Как обычно, полный код этой статьи доступен в нашем репозитории GitHub.

«

As usual, the complete code for this article is available in our GitHub repository.