NestJS (fastify) でファイルアップロード
情報が少ないのでまとめることに。
このあたりを参考に。
まずはどんな形でもいいのでアップロードできるようにする
fastify-multipartというやつをつかってみる。
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import {
FastifyAdapter,
NestFastifyApplication,
} from '@nestjs/platform-fastify';
+ import fmp = require('fastify-multipart');
async function bootstrap() {
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter({ logger: true }),
);
+ app.register(fmp);
const port = Number(process.env.PORT) || 3000;
await app.listen(port, '0.0.0.0');
}
bootstrap();
app.register(fmp);
で型エラー。幸先ヨシ!
公式を見ろという話。型情報を追加。
arn add -D @types/busboy
起動時エラー:FastifyError: The decorator 'corsPreflightEnabled' has already been added!
たぶんどこかで cors
Decorator のための設定がすでに入ってるってことなのかな。
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import {
FastifyAdapter,
NestFastifyApplication,
} from '@nestjs/platform-fastify';
import fmp from 'fastify-multipart';
async function bootstrap() {
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter({ logger: true }),
);
await app.register(fmp);
- app.enableCors();
const port = Number(process.env.PORT) || 3000;
await app.listen(port, '0.0.0.0');
}
bootstrap();
アップロード部分
まずはローカルにファイルを作るように。既存のコントローラーに記述を追加します。
import {
ClassSerializerInterceptor,
Controller,
Get,
HttpStatus,
Post,
Req,
Res,
UseInterceptors,
} from '@nestjs/common';
import { PresentService } from './present.service';
import { Present } from './present';
import { FastifyRequest, FastifyReply } from 'fastify';
import { AppResponseDto } from '../app-response-dto';
@Controller('present')
export class PresentController {
constructor(private presentService: PresentService) {}
@UseInterceptors(ClassSerializerInterceptor)
@Get()
async findAll(): Promise<Present[]> {
return this.presentService.findAll();
}
+ // これ以下を追加
@Post('uploadFile')
async uploadFile(
@Req() req: FastifyRequest,
@Res() res: FastifyReply,
): Promise<AppResponseDto> {
try {
return await this.presentService.uploadFile(req);
} catch (e) {
console.error(e);
res.status(HttpStatus.INTERNAL_SERVER_ERROR);
return new AppResponseDto(
res.statusCode,
undefined,
'INTERNAL_SERVER_ERROR',
);
}
}
}
コントローラーから呼び出しているサービス。これもアップロード処理部分を追加。
import { BadRequestException, HttpException, Injectable } from '@nestjs/common';
import { Present } from './present';
import { FastifyRequest } from 'fastify';
import * as util from 'util';
import * as stream from 'stream';
import * as fs from 'fs';
import { AppResponseDto } from '../app-response-dto';
@Injectable()
export class PresentService {
private readonly presents: Present[] = [];
findAll(): Present[] {
this.presents.push({
name: 'happy_birthday_pack',
prize: 1,
});
this.presents.push({
name: 'good',
prize: 2,
});
return this.presents;
}
+ // これ以下を追加
// upload file
async uploadFile(req: FastifyRequest): Promise<any> {
//Check request is multipart
if (!req.isMultipart()) {
return new BadRequestException(
new AppResponseDto(400, undefined, 'Request is not multipart'),
);
}
const mp = await req.multipart(this.handler, onEnd);
// for key value pairs in request
mp.on('field', function (key: any, value: any) {
console.log('form-data', key, value);
});
// Uploading finished
async function onEnd(err: any) {
console.log(err);
if (err) {
throw new HttpException('Internal server error', 500);
}
return;
}
}
//Save files in directory
async handler(
field: string,
file: any,
filename: string,
encoding: string,
mimetype: string,
): Promise<void> {
const pipeline = util.promisify(stream.pipeline);
const writeStream = fs.createWriteStream(`upload/${filename}`); //File path
try {
await pipeline(file, writeStream);
} catch (err) {
console.error('Pipeline failed', err);
}
}
}
postmanからアップロード
適当に選んでアップロード。upload
ディレクトリにファイルがアップロードされており、成功した。
デコレーターを使った fastify でのファイルアップロード事情
NestJS でプルリクは出ているものの…
どれもマージされていない。やりとりをみていてもよくわからんね。
せめて FastifyRequest と FastifyReply は回避したい
デコレータでファイルを取り出したい。
このとおりに実装したらアップロードできた。ただ、@UseGuards(UploadGuard)
でリクエストからファイルを取り出したりで少し形を整えたい。アップロードにいたるまでの作業を整理した上で何のデコレータも使うかも考えていく。
Pipe は?
Pipe はリクエストボディやリクエストパラメータの属性に対して使うものなので今回は合ってなさそう。Requestオブジェクトがまるっと欲しい。
Guard は?
Guardの責務は認可らしいので今回はやめておく
Intercepter は?
やっぱりこれに帰ってくるのか。FileIntercepter
は fastify モードでは使えないっぽいけど、👆で実装してみて AsyncIterator<Multipart>
が取ってこれればいいいことがわかったのでなんとかなるか?
Interceptor で
できた…けど、ほんとにこんなんでええんか。まあ、ええか。
import {
BadRequestException,
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { FastifyRequest } from 'fastify';
@Injectable()
export class AsyncMultipartFilesInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
console.log('Before...');
const req = context.switchToHttp().getRequest() as FastifyRequest;
const isMultipart = req.isMultipart();
if (!isMultipart) {
throw new BadRequestException('multipart/form-data expected.');
}
req.incomingFiles = req.files();
return next.handle();
}
}
リサイズ考察
Multer は express 依存らしいのでいったん横においておく。sharp が依存少なく使えるらしいので使ってみる。
yarn add sharp
stream API の transform が提供されているっぽいので使う。transformを使いまわしていたけど、二枚目以降が正しく処理されなかったので画像ごとに新しく定義したほうがいいかもしれない。
async upload(files: AsyncMultipartFiles): Promise<void> {
const pipeline = util.promisify(stream.pipeline);
for await (const file of files) {
try {
const roundedCorners = Buffer.from(
'<svg><rect x="0" y="0" width="200" height="200" rx="50" ry="50"/></svg>',
);
const roundedCornerResizer = sharp()
.resize(200, 200)
.composite([
{
input: roundedCorners,
blend: 'dest-in',
},
])
.png();
const writeStream = fs.createWriteStream(`upload/${file.filename}`); // File path
await pipeline(file.file, roundedCornerResizer, writeStream);
} catch (e) {
console.error(e);
throw new HttpException('Internal server error', 500);
}
}
}
でかめの画像を二枚用意
リサイズ実行
あらかわいい
これでだいたいのやり方はわかりました。あとは責務をどうするか…また別で考えます。