NestJS. Subir archivos al almacenamiento S3 (minio)

NestJS es un marco para crear aplicaciones del lado del servidor escalables y eficientes en la plataforma Node.js. Puede encontrarse con la afirmación de que NestJS es un marco independiente de la plataforma. Significa que puede funcionar sobre la base de uno de los dos marcos de su elección: NestJS + Express o NestJS + Fastify. Esto es realmente así, o casi. Esta independencia de la plataforma termina con el manejo de Content-Type: solicitudes multipart / form-data. Es decir, prácticamente en el segundo día de desarrollo. Y esto no es un gran problema si está utilizando la plataforma NestJS + Express; hay un ejemplo de cómo funciona Content-Type: multipart / form-data en la documentación. No existe tal ejemplo para NestJS + Fastify, y no hay tantos ejemplos en la red. Y algunos de estos ejemplos siguen un camino muy complicado.



Al elegir entre la plataforma NestJS + Fastify y NestJS + Express, elegí NestJS + Fastify. Conociendo la tendencia de los desarrolladores en cualquier situación incomprensible a colgar propiedades adicionales en el objeto req en Express y así comunicarse entre diferentes partes de la aplicación, decidí firmemente que Express no estará en el próximo proyecto.



Solo necesitaba resolver un problema técnico con Content-Type: multipart / form-data. Además, planeé guardar los archivos recibidos a través de Content-Type: solicitudes multipart / form-data en el almacenamiento S3. En este sentido, la implementación de las solicitudes de Content-Type: multipart / form-data en la plataforma NestJS + Express me confundió de que no funcionaba con streams.



Lanzamiento del almacenamiento local S3



S3 es un almacén de datos (se podría decir, aunque no estrictamente hablando, un almacén de archivos) accesible a través del protocolo http. S3 fue proporcionado originalmente por AWS. Actualmente, la API de S3 también es compatible con otros servicios en la nube. Pero no solo. Hay implementaciones de servidor S3 que puede abrir localmente para usar durante el desarrollo y posiblemente poner sus servidores S3 en producción.



Primero, debe decidir la motivación para usar el almacenamiento de datos S3. En algunos casos, esto puede reducir los costos. Por ejemplo, puede utilizar el almacenamiento S3 más lento y económico para almacenar copias de seguridad. El almacenamiento rápido con mucho tráfico (el tráfico se cobra por separado) para cargar datos desde el almacenamiento probablemente tendrá un costo comparable al de los discos SSD del mismo tamaño.



Un motivo más importante es 1) escalabilidad: no necesita pensar en el hecho de que el espacio en disco puede agotarse y 2) confiabilidad: los servidores funcionan en un clúster y no necesita pensar en la copia de seguridad, ya que la cantidad requerida de copias siempre está disponible.



Para aumentar la implementación de los servidores S3 (minio) localmente, solo necesita docker y docker-compose instalados en la computadora. Archivo docker-compose.yml correspondiente:



version: '3'
services:
  minio1:
    image: minio/minio:RELEASE.2020-08-08T04-50-06Z
    volumes:
      - ./s3/data1-1:/data1
      - ./s3/data1-2:/data2
    ports:
      - '9001:9000'
    environment:
      MINIO_ACCESS_KEY: minio
      MINIO_SECRET_KEY: minio123
    command: server http://minio{1...4}/data{1...2}
    healthcheck:
      test: ['CMD', 'curl', '-f', 'http://localhost:9000/minio/health/live']
      interval: 30s
      timeout: 20s
      retries: 3

  minio2:
    image: minio/minio:RELEASE.2020-08-08T04-50-06Z
    volumes:
      - ./s3/data2-1:/data1
      - ./s3/data2-2:/data2
    ports:
      - '9002:9000'
    environment:
      MINIO_ACCESS_KEY: minio
      MINIO_SECRET_KEY: minio123
    command: server http://minio{1...4}/data{1...2}
    healthcheck:
      test: ['CMD', 'curl', '-f', 'http://localhost:9000/minio/health/live']
      interval: 30s
      timeout: 20s
      retries: 3

  minio3:
    image: minio/minio:RELEASE.2020-08-08T04-50-06Z
    volumes:
      - ./s3/data3-1:/data1
      - ./s3/data3-2:/data2
    ports:
      - '9003:9000'
    environment:
      MINIO_ACCESS_KEY: minio
      MINIO_SECRET_KEY: minio123
    command: server http://minio{1...4}/data{1...2}
    healthcheck:
      test: ['CMD', 'curl', '-f', 'http://localhost:9000/minio/health/live']
      interval: 30s
      timeout: 20s
      retries: 3

  minio4:
    image: minio/minio:RELEASE.2020-08-08T04-50-06Z
    volumes:
      - ./s3/data4-1:/data1
      - ./s3/data4-2:/data2
    ports:
      - '9004:9000'
    environment:
      MINIO_ACCESS_KEY: minio
      MINIO_SECRET_KEY: minio123
    command: server http://minio{1...4}/data{1...2}
    healthcheck:
      test: ['CMD', 'curl', '-f', 'http://localhost:9000/minio/health/live']
      interval: 30s
      timeout: 20s
      retries: 3


Comenzamos - y sin ningún problema obtenemos un clúster de 4 servidores S3.



NestJS + Fastify + S3



Describiré cómo trabajar con el servidor NestJS desde los primeros pasos, aunque parte de este material se describe perfectamente en la documentación. Instala CLI NestJS:



npm install -g @nestjs/cli


Se crea un nuevo proyecto NestJS:



nest new s3-nestjs-tut


Se instalan los paquetes necesarios (incluidos los necesarios para trabajar con S3):




npm install --save @nestjs/platform-fastify fastify-multipart aws-sdk sharp
npm install --save-dev @types/fastify-multipart  @types/aws-sdk @types/sharp


De forma predeterminada, el proyecto instala la plataforma NestJS + Express. La forma de instalar Fastify se describe en la documentación docs.nestjs.com/techniques/performance . Además, necesitamos instalar un complemento para manejar Content-Type: multipart / form-data - fastify-multipart



import { NestFactory } from '@nestjs/core';
import {
  FastifyAdapter,
  NestFastifyApplication,
} from '@nestjs/platform-fastify';
import fastifyMultipart from 'fastify-multipart';
import { AppModule } from './app.module';

async function bootstrap() {
  const fastifyAdapter = new FastifyAdapter();
  fastifyAdapter.register(fastifyMultipart, {
    limits: {
      fieldNameSize: 1024, // Max field name size in bytes
      fieldSize: 128 * 1024 * 1024 * 1024, // Max field value size in bytes
      fields: 10, // Max number of non-file fields
      fileSize: 128 * 1024 * 1024 * 1024, // For multipart forms, the max file size
      files: 2, // Max number of file fields
      headerPairs: 2000, // Max number of header key=>value pairs
    },
  });
  const app = await NestFactory.create<NestFastifyApplication>(
    AppModule,
    fastifyAdapter,
  );
  await app.listen(3000, '127.0.0.1');
}

bootstrap();


Ahora describiremos el servicio que sube archivos al repositorio de S3, habiendo reducido el código para manejar algunos tipos de errores (el texto completo está en el repositorio de artículos):



import { Injectable, HttpException, BadRequestException } from '@nestjs/common';
import { S3 } from 'aws-sdk';
import fastify = require('fastify');
import { AppResponseDto } from './dto/app.response.dto';
import * as sharp from 'sharp';

@Injectable()
export class AppService {
  async uploadFile(req: fastify.FastifyRequest): Promise<any> {

    const promises = [];

    return new Promise((resolve, reject) => {

      const mp = req.multipart(handler, onEnd);

      function onEnd(err) {
        if (err) {
          reject(new HttpException(err, 500));
        } else {
          Promise.all(promises).then(
            data => {
              resolve({ result: 'OK' });
            },
            err => {
              reject(new HttpException(err, 500));
            },
          );
        }
      }

      function handler(field, file, filename, encoding, mimetype: string) {
        if (mimetype && mimetype.match(/^image\/(.*)/)) {
          const imageType = mimetype.match(/^image\/(.*)/)[1];
          const s3Stream = new S3({
            accessKeyId: 'minio',
            secretAccessKey: 'minio123',
            endpoint: 'http://127.0.0.1:9001',
            s3ForcePathStyle: true, // needed with minio?
            signatureVersion: 'v4',
          });
          const promise = s3Stream
            .upload(
              {
                Bucket: 'test',
                Key: `200x200_${filename}`,
                Body: file.pipe(
                  sharp()
                    .resize(200, 200)
                    [imageType](),
                ),
              }
            )
            .promise();
          promises.push(promise);
        }
        const s3Stream = new S3({
          accessKeyId: 'minio',
          secretAccessKey: 'minio123',
          endpoint: 'http://127.0.0.1:9001',
          s3ForcePathStyle: true, // needed with minio?
          signatureVersion: 'v4',
        });
        const promise = s3Stream
          .upload({ Bucket: 'test', Key: filename, Body: file })
          .promise();
        promises.push(promise);
      }
    });
  }
}


De las características, cabe señalar que escribimos un flujo de entrada en dos flujos de salida si se carga una imagen. Una de las secuencias comprime la imagen a un tamaño de 200x200. En todos los casos, se utiliza el estilo de flujo. Pero para detectar posibles errores y devolverlos al controlador, llamamos al método promise (), que se define en la biblioteca aws-sdk. Acumulamos las promesas recibidas en la matriz de promesas:



        const promise = s3Stream
          .upload({ Bucket: 'test', Key: filename, Body: file })
          .promise();
        promises.push(promise);


Y, además, esperamos su resolución en el método Promise.all(promises).



El código del controlador, en el que todavía tenía que reenviar FastifyRequest al servicio:



import { Controller, Post, Req } from '@nestjs/common';
import { AppService } from './app.service';
import { FastifyRequest } from 'fastify';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Post('/upload')
  async uploadFile(@Req() req: FastifyRequest): Promise<any> {
    const result = await this.appService.uploadFile(req);
    return result;
  }
}


Se lanza el proyecto:



npm run start:dev


Repositorio de artículos github.com/apapacy/s3-nestjs-tut



apapacy@gmail.com

13 de agosto de 2020



All Articles