Subir/Bajar archivos de FTP con Java / Spring / Spring Boot

FTP fue en su tiempo (y es) probablemente uno de los protocolos de transferencia de archivos más extendidos. Soluciones más moderas como el servicio de archivos de Amazon (S3) y de otros proveedores cloud han ganado terreno en la actualidad, sin embargo como protocolo FTP es el más usado para la gestión de archivos dentro de redes privadas.

En el año 2017 cuando estaba empezando a aprender Spring Boot una de las primeras aplicaciones que hice fue un cliente en Spring que permite conectarse a un FTP (autenticado), subir archivos en una ruta determinada o descargar archivos, es un ejemplo muy simple, sin embargo de los ejemplos que tengo mi repositorio público de GitHub es el proyecto que más estrellas tiene (no es que sean muchas tampoco 🙂 ).

En esta entrada compartiré para del código tal cual lo puse en el 2017 (sepan que fue cuando estaba empezando en Spring, alguna mala práctica se van a encontrar probablemente).

Apache commons net

Para encargarse del manejo del protocolo FTP tenemos de la fundación Apache la biblioteca commons net, en el momento en que la use la versión fue 3.6, actualmente 3.8 (Jun/2021).

Apache commnes net también nos sirve para realizar operaciones sobre los siguientes protocolos de red:

  • FTP/FTPS
  • FTP over HTTP (experimental)
  • NNTP
  • SMTP(S)
  • POP3(S)
  • IMAP(S)
  • Telnet
  • TFTP
  • Finger
  • Whois
  • rexec/rcmd/rlogin
  • Time (rdate) and Daytime
  • Echo
  • Discard
  • NTP/SNTP

Para incluirla en un proyecto Maven como dependencia usamos:

	<dependency>
		<groupId>commons-net</groupId>
		<artifactId>commons-net</artifactId>
		<version>${version}</version>
	</dependency>

Código principal de nuestra aplicación para subir y bajar archivos de un FTP desde Java / Spring

Pondremos solo en el artículo el servicio principal de nuestra aplicación de ejemplo:

@Service
public class FTPServiceImpl implements FTPService {
    /**
     * FTP connection handler
     */
    FTPClient ftpconnection;
    private Logger logger = LoggerFactory.getLogger(FTPServiceImpl.class);
    /**
     * Method that implement FTP connection.
     * @param host IP of FTP server
     * @param user FTP valid user
     * @param pass FTP valid pass for user
     * @throws FTPErrors Set of possible errors associated with connection process.
     */
    @Override
    public void connectToFTP(String host, String user, String pass) throws FTPErrors {
        ftpconnection = new FTPClient();
        ftpconnection.addProtocolCommandListener(new PrintCommandListener(new PrintWriter(System.out)));
        int reply;
        try {
            ftpconnection.connect(host);
        } catch (IOException e) {
            ErrorMessage errorMessage = new ErrorMessage(-1, "No fue posible conectarse al FTP a través del host=" + host);
            logger.error(errorMessage.toString());
            throw new FTPErrors(errorMessage);
        }
        reply = ftpconnection.getReplyCode();
        if (!FTPReply.isPositiveCompletion(reply)) {
            try {
                ftpconnection.disconnect();
            } catch (IOException e) {
                ErrorMessage errorMessage = new ErrorMessage(-2, "No fue posible conectarse al FTP, el host=" + host + " entregó la respuesta=" + reply);
                logger.error(errorMessage.toString());
                throw new FTPErrors(errorMessage);
            }
        }
        try {
            ftpconnection.login(user, pass);
        } catch (IOException e) {
            ErrorMessage errorMessage = new ErrorMessage(-3, "El usuario=" + user + ", y el pass=**** no fueron válidos para la autenticación.");
            logger.error(errorMessage.toString());
            throw new FTPErrors(errorMessage);
        }
        try {
            ftpconnection.setFileType(FTP.BINARY_FILE_TYPE);
        } catch (IOException e) {
            ErrorMessage errorMessage = new ErrorMessage(-4, "El tipo de dato para la transferencia no es válido.");
            logger.error(errorMessage.toString());
            throw new FTPErrors(errorMessage);
        }
        ftpconnection.enterLocalPassiveMode();
    }
    /**
     * Method that allow upload file to FTP
     * @param file File object of file to upload
     * @param ftpHostDir FTP host internal directory to save file
     * @param serverFilename Name to put the file in FTP server.
     * @throws FTPErrors Set of possible errors associated with upload process.
     */
    @Override
    public void uploadFileToFTP(File file, String ftpHostDir , String serverFilename) throws FTPErrors {
        try {
            InputStream input = new FileInputStream(file);
            this.ftpconnection.storeFile(ftpHostDir + serverFilename, input);
        } catch (IOException e) {
            ErrorMessage errorMessage = new ErrorMessage(-5, "No se pudo subir el archivo al servidor.");
            logger.error(errorMessage.toString());
            throw new FTPErrors(errorMessage);
        }
    }
    /**
     * Method for download files from FTP.
     * @param ftpRelativePath Relative path of file to download into FTP server.
     * @param copytoPath Path to copy the file in download process.
     * @throws FTPErrors Set of errors associated with download process.
     */
    @Override
    public void downloadFileFromFTP(String ftpRelativePath, String copytoPath) throws FTPErrors {
        FileOutputStream fos;
        try {
            fos = new FileOutputStream(copytoPath);
        } catch (FileNotFoundException e) {
            ErrorMessage errorMessage = new ErrorMessage(-6, "No se pudo obtener la referencia a la carpeta relativa donde guardar, verifique la ruta y los permisos.");
            logger.error(errorMessage.toString());
            throw new FTPErrors(errorMessage);
        }
        try {
            this.ftpconnection.retrieveFile(ftpRelativePath, fos);
        } catch (IOException e) {
            ErrorMessage errorMessage = new ErrorMessage(-7, "No se pudo descargar el archivo.");
            logger.error(errorMessage.toString());
            throw new FTPErrors(errorMessage);
        }
    }
    /**
     * Method for release the FTP connection.
     * @throws FTPErrors Error if unplugged process failed.
     */
    @Override
    public void disconnectFTP() throws FTPErrors {
        if (this.ftpconnection.isConnected()) {
            try {
                this.ftpconnection.logout();
                this.ftpconnection.disconnect();
            } catch (IOException f) {
               throw new FTPErrors( new ErrorMessage(-8, "Ha ocurrido un error al realizar la desconexión del servidor FTP"));
            }
        }
    }
}

Como verán es muy simple, el código es bastante claro.

Algunas mejoras a este código que te recomiendo si lo vas a usar en algún proyecto real:

  • Pasa a configuración las propiedades que así lo ameriten, por ejemplo los parámetros de conexión.
  • Create un pool de conexiones.
  • Usa la inyección de dependencias de Spring por constructor y no por atributo.
  • Mejora el tema de las excepciones, puedes ponerlas en inglés y usar excepciones de tipo no chequeadas (Por debajo de RunTimeExceptions).

Espero te sirva este ejemplo, nota que aunque esta hecho con Spring básicamente, la forma en que esta programado no usa casi nada de Spring framework, por lo que sacarlo y usarlo solo en Java sería muy sencillo tal cual está.

Puedes acceder a todo el código acá en GitHub.