Descargar archivos de FTP usando NodeJS

Photo by Verne Ho on Unsplash

Descargar archivos de FTP usando NodeJS

Esto no es una continuación del artículo anterior, sin embargo, a efectos prácticos, se usará el repositorio creado anteriormente para ver cómo sincronizarnos a un servidor FTP, descargar un archivo de un proveedor, guardarlo temporalmente de forma local y desconectarnos.

Requisitos necesarios

Instalaciones

Necesitaremos tener instaladas las dependencias mencionadas anteriormente. Para ello, bastaría con ejecutar en una sola línea:

npm i node-cron uuid basic-ftp

Comenzamos a codear

Raíz de la aplicación

Comenzaremos editando nuestro index.js, agregando un cron schedule para definir el horario de sincronización. Quedándonos de la siguiente forma:

const express = require("express");
const mongoose = require("mongoose");
const cors = require("cors");
const cron = require('node-cron');

const config = require('./config/config');
const cronjobService = require('./services/cron.service');
const indexRoutes = require('./routes');

mongoose
  .connect(config.db.mongo)
  .then((res) => {

    const app = express();

    app.use(
      express.urlencoded({
        extended: true,
      })
    );

    app.use(express.json());

    app.use(cors());

    app.use("/", indexRoutes);

    cron.schedule('00 22 * * *', function () {
      cronjobService.scrapProvider();
    });

    app.listen(config.app.port, () => {
      console.log(`🔥 Server is running at port ${config.app.port}`);
    });

    console.log("Connected to MongoDB");
  })
  .catch((error) => console.log(error));

Como se ve, nuestro cron schedule, hace uso de nuestro cronjobService, pero éste no está definido. Dentro de la carpeta services, crearemos un archivo llamado cron.service.js

const scrapperService = require("./scrapper.service");

class CronJobService {
    constructor() { }

    async scrapProvider() {
        await scrapperService.scrap();
    }

}

module.exports = new CronJobService();

Services

Nuevamente, tenemos una importación que no está definida. Por ende, dentro de la carpeta services, crearemos un archivo llamado scrapper.service.js

const provider = require('../scrappers/provider.js');

class ScrapperService {
    constructor() { }

    async scrap() {
        return await provider.scrap();
    }
}

module.exports = new ScrapperService();

Como se aprecia en las importaciones, hemos importado a nuestro proveedor. Que también es un scrapper a definir. Por ende, crearemos una carpeta scrapers a la altura de src. Y dentro de esta, un archivo llamado provider.js

const ftpService = require('../services/ftp.service');
const { provider } = require('../config');

class Scrapper {
    constructor(config) {
        this.ftpService = new ftpService(config);
    }

    async scrap() {
        return await this.ftpService.run();
    }
}

module.exports = new Scrapper(provider);

En última instancia, por fin, se hace uso de nuestro servicio de FTP, el que se encargará de procesar las credenciales que vienene desde nuestro archivo de configuración, para saber cuál es el proveedor en cuestión y acceder efectivamente al servidor de FTP.

Para ello, crearemos, dentro de la carpeta services, un archivo llamado ftp.services.js

const ftp = require("basic-ftp");

class FtpService {
    constructor(config) {

        this.credentials = config.credentials;
        this.localPath = config.localPath;
        this.remotePath = config.remotePath;

        this.client = new ftp.Client();
    }

    async run() {
        this.client.ftp.verbose = true;
        await this.client.access(this.credentials);
        console.log('Connected to FTP');

        await this.download();
        await this.disconnect();
    }

    async download() {
        await this.client.downloadTo(this.localPath, this.remotePath);
        console.log('Downloaded file from FTP');
    }

    async disconnect() {
        await this.client.close();
        console.log('Disconnected from FTP');
    }
}

module.exports = FtpService;

Si se han fijado, anteriormente una de las importaciones es, precisamente una destructuración de provider, que viene desde un archivo de configuración. Sin embargo, éste aún, no está definido. Para ello, dentro de src, en la carpeta config, crearemos un archivo llamado provider.js

const path = require("path");

module.exports = {
    credentials: {
        host: process.env.FTP_HOST,
        user: process.env.FTP_USER,
        password: process.env.FTP_PASSWORD,
        port: process.env.FTP_PORT,
    },
    localPath: path.join(__dirname, "../temp", "provider.csv"),
    remotePath: 'provider.csv',
}

Nota: El puerto default de un servidor FTP, es 21 además, es necesario crear, a la altura de src, una carpeta llamada temp, donde se almacenará nuestro provider.csv

Config

Una vez tenemos definida nuestras credenciales en nuestro archivo de configuración, procedemos a crear un índice dentro de la misma carpeta config. Por ello, crearemos un archivo index.js

const providerConfig = require("../config/providers");

module.exports = {
  provider: providerConfig,
}

Routes

En esta instancia, tenemos desarrollada nuestra feature con las capas de abstracción correspondientes. Por lo que podemos simplemente, cambiar la hora en nuestro cron schedule para testear que, efectivamente funcione, o, mejor aún, podemos crear un endpoint para testear nuestra implementación de forma manual.

Dentro de nuestra carpeta routes, crearemos un archivo llamado test.route.js que haga uso, de nuestro cronjobService, el primer punto de nuestro flujo definido anteriormente.

const { Router } = require("express");
const routes = Router();

const cronjobService = require("../services/cron.service");

routes.post('/ftp', (req, res) => {
    cronjobService.scrapProvider();

    res.status(200).send('Ok');
});

module.exports = routes;

Nuevamente, dentro de la carpeta routes, debemos agregar el nuevo archivo de rutas a nuestro índice, por ello, dentro de index.js, agregamos lo siguiente:

const { Router } = require('express');
const routes = Router();

const productsRoutes = require('./products.route');
const testRoutes = require('./test.route');

routes.use('/products', productsRoutes);
routes.use('/test', testRoutes);

module.exports = routes;

¡Hasta pronto 👋!

Si deseas colaborar con el contenido, regalame un cafecito