NestJS — фреймворк для создания эффективных, масштабируемых серверных приложений на платформе Node.js. Вы можете встретить утверждение, что NestJS является платформо-независимым фреймворком. Имеется в виду, что он может работать на базе одного из двух фрейморков по Вашему выбору: NestJS+Express или NestJS+Fastify. Это действительно так, или почти так. Эта платформо-независимость заканчивается, на обработке запросов Content-Type: multipart/form-data. То есть практически на второй день разработки. И это не является большой проблемой, если Вы используете платформу NestJS+Express — в документации есть пример работы для Content-Type: multipart/form-data. Для NestJS+Fastify такого примера нет, и примеров в сети не так уж и много. И некоторые из этих примеров идут по весьма усложненному пути.
Выбирая между платформой NestJS+Fastify и NestJS+Express я сделал выбор в сторону NestJS+Fastify. Зная склонность разработчиков в любой непонятной ситуации вешать на объект req в Express дополнительные свойства и так общаться между разными частями приложения, я твердо решил что Express в следующем проекте не будет.
Только нужно было решить технический вопрос с Content-Type: multipart/form-data. Также полученные через запросы Content-Type: multipart/form-data файлы я планировал сохранять в хранилище S3. В этом плане реализация запросов Content-Type: multipart/form-data на платформе NestJS+Express меня смущала тем, что не работала с потоками.
S3 — это хранилище данных (можно сказать, хотя не совсем строго, хранилище файлов), доступное по протоколу http. Изначально S3 предоставлялся AWS. В настоящее время API S3 поддерживается и другими облачными сервисами. Но не только. Появились реализации серверов S3, которые Вы можете поднять локально, чтобы использовать их во время разработки, и, возможно, поднять свои серверы S3 для работы на проде.
Для начала нужно определиться с мотивацией использования S3 хранилища данных. В некоторых случаях это позволяет снизить затраты. Например, для хранения резервных копий можно взять самые медленные и дешевые хранилища S3. Быстрые хранилища с высоким трафиком (трафик тарифицируется отдельно) на загрузку данных из хранилища, возможно, будут стоить сравнимо с SSD дисками того же объема.
Более весомым мотивом является 1) расширяемость — Вам не нужно думать о том, что место на диске может закончиться, и 2) надежность — сервера работают в кластере и Вам не нужно думать о резервном копировании, так как необходимое количество копий есть всегда.
Для поднятия реализации серверов S3 — minio — локально нужен только установленный на компьютере docker и docker-compose. Соответсвующий файл docker-compose.yml:
Запускаем — и без проблем получаем кластер из 4 серверов S3.
Работу с сервером NestJS опишу с самых первых шагов, хотя часть этого материала отлично описана в документации. Устанавливается CLI NestJS:
Создается новый проект NestJS:
Инсталлируются необходимые пакеты (включая те что нужны для работы с S3):
По умолчанию в проекте устанавливается платформа NestJS+Express. Как установить Fastify описано в документации docs.nestjs.com/techniques/performance. Дополнительно нам нужно установить плагин для обработки Content-Type: multipart/form-data — fastify-multipart
Теперь опишем сервис, загружающий файлы в хранилище S3, сократив код по обработке некоторых видов ошибок (полный текст есть в репозитарии статьи):
Из особенностей следует отметить, что мы пишем входной поток в два выходных потока, если загружается картинка. Один из потоков сжимает картинку до размеров 200х200. Во всех случаях используется стиль работы с потоками (stream). Но для того, чтобы отловить возможные ошибки и вернуть их в контроллер, мы вызываем метод promise(), который определен в библиотеке aws-sdk. Полученные промисы накапливаем в массиве promises:
И, далее, ожидаем их разрешение в методе
Код контроллера, в котором таки пришлось пробросить FastifyRequest в сервис:
Запускается проект:
Репозитарий статьи github.com/apapacy/s3-nestjs-tut
apapacy@gmail.com
13 августа 2020 года
Выбирая между платформой NestJS+Fastify и NestJS+Express я сделал выбор в сторону NestJS+Fastify. Зная склонность разработчиков в любой непонятной ситуации вешать на объект req в Express дополнительные свойства и так общаться между разными частями приложения, я твердо решил что Express в следующем проекте не будет.
Только нужно было решить технический вопрос с Content-Type: multipart/form-data. Также полученные через запросы Content-Type: multipart/form-data файлы я планировал сохранять в хранилище S3. В этом плане реализация запросов Content-Type: multipart/form-data на платформе NestJS+Express меня смущала тем, что не работала с потоками.
Запуск локального хранилища S3
S3 — это хранилище данных (можно сказать, хотя не совсем строго, хранилище файлов), доступное по протоколу http. Изначально S3 предоставлялся AWS. В настоящее время API S3 поддерживается и другими облачными сервисами. Но не только. Появились реализации серверов S3, которые Вы можете поднять локально, чтобы использовать их во время разработки, и, возможно, поднять свои серверы S3 для работы на проде.
Для начала нужно определиться с мотивацией использования S3 хранилища данных. В некоторых случаях это позволяет снизить затраты. Например, для хранения резервных копий можно взять самые медленные и дешевые хранилища S3. Быстрые хранилища с высоким трафиком (трафик тарифицируется отдельно) на загрузку данных из хранилища, возможно, будут стоить сравнимо с SSD дисками того же объема.
Более весомым мотивом является 1) расширяемость — Вам не нужно думать о том, что место на диске может закончиться, и 2) надежность — сервера работают в кластере и Вам не нужно думать о резервном копировании, так как необходимое количество копий есть всегда.
Для поднятия реализации серверов S3 — minio — локально нужен только установленный на компьютере docker и docker-compose. Соответсвующий файл 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
Запускаем — и без проблем получаем кластер из 4 серверов S3.
NestJS + Fastify + S3
Работу с сервером NestJS опишу с самых первых шагов, хотя часть этого материала отлично описана в документации. Устанавливается CLI NestJS:
npm install -g @nestjs/cli
Создается новый проект NestJS:
nest new s3-nestjs-tut
Инсталлируются необходимые пакеты (включая те что нужны для работы с S3):
npm install --save @nestjs/platform-fastify fastify-multipart aws-sdk sharp
npm install --save-dev @types/fastify-multipart @types/aws-sdk @types/sharp
По умолчанию в проекте устанавливается платформа NestJS+Express. Как установить Fastify описано в документации docs.nestjs.com/techniques/performance. Дополнительно нам нужно установить плагин для обработки 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();
Теперь опишем сервис, загружающий файлы в хранилище S3, сократив код по обработке некоторых видов ошибок (полный текст есть в репозитарии статьи):
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);
}
});
}
}
Из особенностей следует отметить, что мы пишем входной поток в два выходных потока, если загружается картинка. Один из потоков сжимает картинку до размеров 200х200. Во всех случаях используется стиль работы с потоками (stream). Но для того, чтобы отловить возможные ошибки и вернуть их в контроллер, мы вызываем метод promise(), который определен в библиотеке aws-sdk. Полученные промисы накапливаем в массиве promises:
const promise = s3Stream
.upload({ Bucket: 'test', Key: filename, Body: file })
.promise();
promises.push(promise);
И, далее, ожидаем их разрешение в методе
Promise.all(promises)
.Код контроллера, в котором таки пришлось пробросить FastifyRequest в сервис:
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;
}
}
Запускается проект:
npm run start:dev
Репозитарий статьи github.com/apapacy/s3-nestjs-tut
apapacy@gmail.com
13 августа 2020 года
devopg
жаль нету сравнения с самым популярным решением resumablejs
apapacy Автор
Бегло просмотрел это решение. Его цель сделать загрузку файлов (особенно больших файлов) отказоустойчивой. В качестве способа файлы на фронтенде разбиваются на чанки и на бэкенде из этих чанков собираются. Этот способ лучше реализовывать на чем-то отличном от Node.js. так как приведенный мною код в статье как раз позволяет создать поток между браузером и хранилищем S3 при этом задействуются средства неблокирующего ввода/вывода Node.js и совсем не задействуется блокирующий JS.
Загрузка файлов resumable-node.js тянет на создание отдельного микросервиса и скорее всего не Node.js, чтобы эта загрузка не блокировала все приложение.