NestJS. Téléchargement de fichiers vers le stockage S3 (minio)

NestJS est un framework permettant de créer des applications côté serveur efficaces et évolutives sur la plate-forme Node.js. Vous pouvez tomber sur l'affirmation que NestJS est un cadre indépendant de la plate-forme. Cela signifie qu'il peut fonctionner sur la base de l'un des deux frameworks de votre choix: NestJS + Express ou NestJS + Fastify. C'est vraiment le cas, ou presque. Cette indépendance de plate-forme se termine par le traitement des requêtes Content-Type: multipart / form-data. Autrement dit, pratiquement au deuxième jour du développement. Et ce n'est pas un gros problème si vous utilisez la plate-forme NestJS + Express - il y a un exemple du fonctionnement de Content-Type: multipart / form-data dans la documentation. Il n'y a pas d'exemple de ce type pour NestJS + Fastify, et il n'y a pas tellement d'exemples sur le net. Et certains de ces exemples suivent un chemin très compliqué.



En choisissant entre la plateforme NestJS + Fastify et NestJS + Express, j'ai fait un choix vers NestJS + Fastify. Connaissant l'inclination des développeurs dans toute situation incompréhensible à accrocher des propriétés supplémentaires sur l'objet req dans Express et ainsi communiquer entre les différentes parties de l'application, j'ai fermement décidé qu'Express ne serait pas dans le prochain projet.



Je n'avais besoin que de résoudre un problème technique avec Content-Type: multipart / form-data. De plus, j'avais prévu de sauvegarder les fichiers reçus via Content-Type: requêtes multipart / form-data dans le stockage S3. À cet égard, la mise en œuvre de Content-Type: demandes multipart / form-data sur la plate-forme NestJS + Express m'a confus que cela ne fonctionnait pas avec les flux.



Lancement du stockage local S3



S3 est un magasin de données (on pourrait dire, mais pas à proprement parler, un magasin de fichiers) accessible via le protocole http. S3 a été initialement fourni par AWS. L'API S3 est actuellement également prise en charge par d'autres services cloud. Mais pas seulement. Il existe des implémentations de serveur S3 que vous pouvez hisser localement pour les utiliser pendant le développement, et éventuellement faire fonctionner vos serveurs S3 en production.



Tout d'abord, vous devez décider de la motivation pour utiliser le stockage de données S3. Dans certains cas, cela peut réduire les coûts. Par exemple, vous pouvez utiliser le stockage S3 le plus lent et le moins cher pour stocker les sauvegardes. Les stockages rapides avec un trafic élevé (le trafic est facturé séparément) pour le chargement de données à partir du stockage coûteront probablement comparable à des disques SSD de même taille.



Un motif plus puissant est 1) l'évolutivité - vous n'avez pas besoin de penser au fait que l'espace disque peut manquer, et 2) la fiabilité - les serveurs fonctionnent dans un cluster et vous n'avez pas besoin de penser à la sauvegarde, car le nombre requis de copies est toujours disponible.



Pour augmenter l'implémentation des serveurs S3 - minio - localement, vous n'avez besoin que de docker et docker-compose installés sur l'ordinateur. Fichier docker-compose.yml correspondant:



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


Nous commençons - et sans aucun problème, nous obtenons un cluster de 4 serveurs S3.



NestJS + Fastify + S3



Je décrirai le travail avec le serveur NestJS dès les premières étapes, bien qu'une partie de ce matériel soit parfaitement décrite dans la documentation. Installe CLI NestJS:



npm install -g @nestjs/cli


Un nouveau projet NestJS est créé:



nest new s3-nestjs-tut


Les packages nécessaires sont installés (y compris ceux nécessaires pour fonctionner avec S3):




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


Par défaut, le projet installe la plate-forme NestJS + Express. La procédure d'installation de Fastify est décrite dans la documentation docs.nestjs.com/techniques/performance . De plus, nous devons installer un plugin pour gérer 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();


Nous allons maintenant décrire le service qui télécharge les fichiers dans le référentiel S3, après avoir réduit le code pour gérer certains types d'erreurs (le texte intégral se trouve dans le référentiel d'articles):



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


Parmi les fonctionnalités, il convient de noter que nous écrivons un flux d'entrée dans deux flux de sortie si une image est chargée. L'un des flux compresse l'image à une taille de 200x200. Dans tous les cas, le style de flux est utilisé. Mais pour détecter d'éventuelles erreurs et les renvoyer au contrôleur, nous appelons la méthode promise (), qui est définie dans la bibliothèque aws-sdk. Nous accumulons les promesses reçues dans le tableau des promesses:



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


Et, en outre, nous attendons leur résolution dans la méthode Promise.all(promises).



Le code du contrôleur, dans lequel je devais encore transmettre FastifyRequest au service:



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


Le projet est lancé:



npm run start:dev


Article référentiel github.com/apapacy/s3-nestjs-tut



apapacy@gmail.com

13 Août, 2020



All Articles