NestJS. Hochladen von Dateien in den S3-Speicher (minio)

NestJS ist ein Framework zum Erstellen effizienter, skalierbarer serverseitiger Anwendungen auf der Node.js-Plattform. Möglicherweise stoßen Sie auf die Behauptung, dass NestJS ein plattformunabhängiges Framework ist. Dies bedeutet, dass es auf der Grundlage eines von zwei Frameworks Ihrer Wahl arbeiten kann: NestJS + Express oder NestJS + Fastify. Das ist wirklich so oder fast so. Diese Plattformunabhängigkeit endet mit der Behandlung von Anforderungen für Inhaltstypen: mehrteilige Daten / Formulardaten. Das heißt, praktisch am zweiten Tag der Entwicklung. Und dies ist kein großes Problem, wenn Sie die NestJS + Express-Plattform verwenden. In der Dokumentation finden Sie ein Beispiel für die Funktionsweise von Inhaltstyp: Mehrteilige / Formulardaten. Es gibt kein solches Beispiel für NestJS + Fastify und es gibt nicht so viele Beispiele im Internet. Einige dieser Beispiele folgen einem sehr komplizierten Weg.



Ich entschied mich zwischen der NestJS + Fastify- und der NestJS + Express-Plattform und entschied mich für NestJS + Fastify. Da ich die Neigung von Entwicklern in einer unverständlichen Situation kannte, zusätzliche Eigenschaften an das erforderliche Objekt in Express zu hängen und so zwischen verschiedenen Teilen der Anwendung zu kommunizieren, entschied ich fest, dass Express nicht im nächsten Projekt enthalten sein wird.



Ich musste nur ein technisches Problem mit Content-Type lösen: Multipart / Formulardaten. Außerdem wollte ich die über Content-Type: Multipart / Formulardatenanforderungen empfangenen Dateien im S3-Speicher speichern. In dieser Hinsicht hat mich die Implementierung von Anforderungen für Content-Type: Multipart / Formulardaten auf der NestJS + Express-Plattform verwirrt, dass es nicht mit Streams funktioniert.



Starten des lokalen S3-Speichers



S3 ist ein Datenspeicher (man könnte sagen, wenn auch nicht streng genommen ein Dateispeicher), auf den über das http-Protokoll zugegriffen werden kann. S3 wurde ursprünglich von AWS bereitgestellt. Die S3-API wird derzeit auch von anderen Cloud-Diensten unterstützt. Aber nicht nur. Es gibt S3-Serverimplementierungen, die Sie lokal zur Verwendung während der Entwicklung aufrufen und möglicherweise Ihre S3-Server in die Produktion bringen können.



Zunächst müssen Sie die Motivation für die Verwendung der S3-Datenspeicherung festlegen. In einigen Fällen kann dies die Kosten senken. Sie können beispielsweise den langsamsten und billigsten S3-Speicher zum Speichern von Sicherungen verwenden. Schneller Speicher mit hohem Datenverkehr (der Datenverkehr wird separat berechnet) zum Laden von Daten aus dem Speicher kostet wahrscheinlich vergleichbare Kosten wie SSD-Datenträger derselben Größe.



Ein stärkeres Motiv ist 1) Skalierbarkeit - Sie müssen nicht daran denken, dass möglicherweise nicht genügend Speicherplatz zur Verfügung steht, und 2) Zuverlässigkeit - die Server arbeiten in einem Cluster und Sie müssen nicht an Backups denken, da immer die erforderliche Anzahl von Kopien verfügbar ist.



Um die Implementierung von S3-Servern - minio - lokal zu erhöhen, müssen nur Docker und Docker-Compose auf dem Computer installiert sein. Entsprechende Datei docker-compose.yml:



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


Wir starten - und ohne Probleme erhalten wir einen Cluster von 4 S3-Servern.



NestJS + Fastify + S3



Ich werde die Arbeit mit dem NestJS-Server von den ersten Schritten an beschreiben, obwohl ein Teil dieses Materials in der Dokumentation perfekt beschrieben ist. Installiert CLI NestJS:



npm install -g @nestjs/cli


Ein neues NestJS-Projekt wird erstellt:



nest new s3-nestjs-tut


Die erforderlichen Pakete sind installiert (einschließlich der für die Arbeit mit S3 erforderlichen):




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


Standardmäßig installiert das Projekt die NestJS + Express-Plattform. Wie Fastify zu installieren , ist in der beschriebenen docs.nestjs.com/techniques/performance Dokumentation . Zusätzlich müssen wir ein Plugin für die Verarbeitung des Inhaltstyps installieren: Multipart / Formulardaten - 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();


Jetzt beschreiben wir den Dienst, der Dateien in das S3-Repository hochlädt, nachdem der Code für die Behandlung einiger Arten von Fehlern reduziert wurde (der vollständige Text befindet sich im Artikel-Repository):



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


Von den Merkmalen sollte beachtet werden, dass wir einen Eingabestream in zwei Ausgabestreams schreiben, wenn ein Bild geladen wird. Einer der Streams komprimiert das Bild auf eine Größe von 200 x 200. In allen Fällen wird der Arbeitsstil mit Streams verwendet. Um mögliche Fehler abzufangen und an den Controller zurückzugeben, rufen wir die Methode commit () auf, die in der Bibliothek aws-sdk definiert ist. Wir sammeln die erhaltenen Versprechen im Versprechungsarray:



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


Und außerdem erwarten wir ihre Auflösung in der Methode Promise.all(promises).



Der Controller-Code, in dem ich noch FastifyRequest an den Dienst weiterleiten musste:



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


Das Projekt wird gestartet:



npm run start:dev


Artikel- Repository github.com/apapacy/s3-nestjs-tut



apapacy@gmail.com

13. August 2020



All Articles