Implementando um cliente FTP em Java

Implementando um cliente FTP em Java

1. Visão geral

Neste tutorial, daremos uma olhada em como aproveitar a bibliotecaApache Commons Net para interagir com um servidor FTP externo.

2. Configuração

Ao usar bibliotecas, que são usadas para interagir com sistemas externos, muitas vezes é uma boa ideia escrever alguns testes de integração adicionais, a fim de ter certeza de que estamos usando a biblioteca corretamente.

Hoje em dia, normalmente usamos o Docker para ativar esses sistemas para nossos testes de integração. No entanto, especialmente quando usado no modo passivo, um servidor FTP não é o aplicativo mais fácil de ser executado de forma transparente dentro de um contêiner se quisermos fazer uso de mapeamentos de porta dinâmicos (o que geralmente é necessário para que os testes possam ser executados em um servidor CI compartilhado )

É por isso que usaremosMockFtpServer em vez disso, um servidor FTP Fake / Stub escrito em Java, que fornece uma API extensa para fácil uso em testes JUnit:


    commons-net
    commons-net
    3.6


    org.mockftpserver
    MockFtpServer
    2.7.1
    test

É recomendável usar sempre a versão mais recente. Esses podem ser encontradoshere ehere.

3. Suporte FTP no JDK

Surpreendentemente, já existe suporte básico para FTP em alguns tipos de JDK na forma desun.net.www.protocol.ftp.FtpURLConnection.

No entanto, não devemos usar essa classe diretamente e, em vez disso, é possível usar a classejava.net.URL do JDK como uma abstração.

Esse suporte FTP é muito básico, mas aproveitando as APIs de conveniência dejava.nio.file.Files, pode ser suficiente para casos de uso simples:

@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
}

Como esse suporte básico a FTP já possui recursos básicos ausentes, como listagens de arquivos, usaremos o suporte a FTP na biblioteca Apache Net Commons nos exemplos a seguir.

4. Conectando

Primeiro, precisamos nos conectar ao servidor FTP. Vamos começar criando uma classeFtpClient.

Ele servirá como uma API de abstração para o cliente FTP da 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();
    }
}

Precisamos do endereço do servidor e da porta, além do nome de usuário e da senha. Depois de conectar, é necessário verificar o código de resposta para ter certeza de que a conexão foi bem-sucedida. Também adicionamos umPrintCommandListener, para imprimir as respostas que normalmente veríamos ao nos conectar a um servidor FTP usando ferramentas de linha de comando para stdout.

Como nossos testes de integração terão algum código padrão, como iniciar / parar o MockFtpServer e conectar / desconectar nosso cliente, podemos fazer essas coisas nos métodos@Beforee@After:

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();
    }
}

Ao definir a porta de controle do servidor simulado para o valor 0, estamos iniciando o servidor simulado e uma porta aleatória livre.

É por isso que temos que recuperar a porta real ao criar oFtpClient após o servidor ter sido iniciado, usandofakeFtpServer.getServerControlPort().

5. Arquivos de listagem

O primeiro caso de uso real será listar arquivos.

Vamos começar com o teste primeiro, no estilo TDD:

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

A implementação em si é igualmente simples. Para tornar a estrutura de dados retornada um pouco mais simples por causa deste exemplo, transformamos o arrayFTPFile retornado em uma lista deStrings usando Java 8Streams:

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

6. Baixando

Para baixar um arquivo do servidor FTP, estamos definindo uma API.

Aqui, definimos o arquivo de origem e o destino no sistema de arquivos local:

@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
}

O cliente FTP Apache Net Commons contém uma API conveniente, que gravará diretamente em umOutputStream. definido. Isso significa que podemos usar isso diretamente:

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

7. Enviando

O MockFtpServer fornece alguns métodos úteis para acessar o conteúdo do seu sistema de arquivos. Podemos usar esse recurso para escrever um teste de integração simples para a funcionalidade de upload:

@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();
}

O upload de um arquivo funciona em termos de API de maneira bastante semelhante ao download, mas em vez de usar umOutputStream, precisamos fornecer umInputStream:

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

8. Conclusão

Vimos que o uso de Java junto com o Apache Net Commons nos permite interagir facilmente com um servidor FTP externo, para acesso de leitura e gravação.

Como de costume, o código completo deste artigo está disponível em nossoGitHub repository.