NestJS. Upload de arquivos para armazenamento S3 (minio)

NestJS é uma estrutura para construir aplicativos do lado do servidor eficientes e escalonáveis ​​na plataforma Node.js. Você pode se deparar com a afirmação de que o NestJS é uma estrutura independente de plataforma. Isso significa que ele pode funcionar com base em uma das duas estruturas de sua escolha: NestJS + Express ou NestJS + Fastify. Isso é realmente assim, ou quase isso. Esta independência de plataforma termina com o tratamento de solicitações de Content-Type: multipart / form-data. Ou seja, praticamente no segundo dia de desenvolvimento. E isso não é um grande problema se você estiver usando a plataforma NestJS + Express - há um exemplo de como Content-Type: multipart / form-data funciona na documentação. Não existe tal exemplo para NestJS + Fastify, e não existem tantos exemplos na rede. E alguns desses exemplos seguem um caminho muito complicado.



Escolhendo entre a plataforma NestJS + Fastify e NestJS + Express, optei por NestJS + Fastify. Conhecendo a inclinação dos desenvolvedores em qualquer situação incompreensível para pendurar propriedades adicionais no objeto req no Express e assim se comunicar entre as diferentes partes do aplicativo, decidi firmemente que o Express não estará no próximo projeto.



Eu só precisava resolver um problema técnico com Content-Type: multipart / form-data. Além disso, planejei salvar os arquivos recebidos por meio de solicitações de Content-Type: multipart / form-data no armazenamento S3. A este respeito, a implementação de solicitações de Content-Type: multipart / form-data na plataforma NestJS + Express me confundiu por não funcionar com streams.



Iniciando o S3 Local Storage



S3 é um armazenamento de dados (pode-se dizer, embora não estritamente falando, um armazenamento de arquivos) acessível através do protocolo http. S3 foi originalmente fornecido pela AWS. A API S3 é atualmente suportada por outros serviços em nuvem também. Mas não só. Existem implementações de servidor S3 que você pode trazer localmente para usar durante o desenvolvimento e, possivelmente, colocar seus servidores S3 em produção.



Primeiro, você precisa decidir sobre a motivação para usar o armazenamento de dados S3. Em alguns casos, isso pode reduzir custos. Por exemplo, você pode usar o armazenamento S3 mais lento e barato para armazenar backups. Os armazenamentos rápidos com alto tráfego (o tráfego é cobrado separadamente) para carregar dados do armazenamento provavelmente custarão comparáveis ​​aos drives SSD do mesmo tamanho.



Um motivo mais significativo é 1) escalabilidade - você não precisa pensar no fato de que o espaço em disco pode acabar e 2) confiabilidade - os servidores funcionam em um cluster e você não precisa pensar em backup, já que o número necessário de cópias está sempre disponível.



Para aumentar a implementação de servidores S3 - minio - localmente, você só precisa do docker e do docker-compose instalados no computador. Arquivo docker-compose.yml correspondente:



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


Começamos - e sem problemas obtemos um cluster de 4 servidores S3.



NestJS + Fastify + S3



Descreverei como trabalhar com o servidor NestJS desde os primeiros passos, embora parte deste material esteja perfeitamente descrito na documentação. Instala CLI NestJS:



npm install -g @nestjs/cli


Um novo projeto NestJS é criado:



nest new s3-nestjs-tut


Os pacotes necessários são instalados (incluindo aqueles necessários para trabalhar com S3):




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


Por padrão, o projeto instala a plataforma NestJS + Express. Como instalar o Fastify está descrito na documentação docs.nestjs.com/techniques/performance . Além disso, precisamos instalar um plugin para lidar com o 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();


Agora vamos descrever o serviço que faz upload de arquivos para o repositório S3, tendo reduzido o código para tratamento de alguns tipos de erros (o texto completo está no repositório do artigo):



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


Dos recursos, deve-se notar que escrevemos um fluxo de entrada em dois fluxos de saída se uma imagem for carregada. Um dos streams comprime a imagem para um tamanho de 200x200. Em todos os casos, o estilo de trabalho com fluxos é usado. Porém, para detectar possíveis erros e devolvê-los ao controlador, chamamos o método Promessa (), que é definido na biblioteca aws-sdk. Acumulamos as promessas recebidas na matriz de promessas:



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


E, além disso, esperamos sua resolução no método Promise.all(promises).



O código do controlador, no qual ainda tive que encaminhar FastifyRequest para o serviço:



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


O projeto é lançado:



npm run start:dev


Repositório de artigos github.com/apapacy/s3-nestjs-tut



apapacy@gmail.com

13 de agosto de 2020



All Articles