Closed7

NestJS (fastify) でファイルアップロード

waddy_uwaddy_u

まずはどんな形でもいいのでアップロードできるようにする

fastify-multipartというやつをつかってみる。

https://github.com/fastify/fastify-multipart

https://medium.com/@427anuragsharma/upload-files-using-multipart-with-fastify-and-nestjs-3f74aafef331

main.ts
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); で型エラー。幸先ヨシ!

https://github.com/fastify/fastify-multipart

公式を見ろという話。型情報を追加。

arn add -D @types/busboy
waddy_uwaddy_u

起動時エラー:FastifyError: The decorator 'corsPreflightEnabled' has already been added!

たぶんどこかで cors Decorator のための設定がすでに入ってるってことなのかな。

main.ts
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();

waddy_uwaddy_u

アップロード部分

まずはローカルにファイルを作るように。既存のコントローラーに記述を追加します。

present.controller.ts
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',
      );
    }
  }
}

コントローラーから呼び出しているサービス。これもアップロード処理部分を追加。

present.service.ts
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 ディレクトリにファイルがアップロードされており、成功した。

waddy_uwaddy_u

せめて FastifyRequest と FastifyReply は回避したい

デコレータでファイルを取り出したい。

https://stackoverflow.com/questions/63724194/nestjs-file-upload-with-fastify-multipart

このとおりに実装したらアップロードできた。ただ、@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();
  }
}

waddy_uwaddy_u

リサイズ考察

Multer は express 依存らしいのでいったん横においておく。sharp が依存少なく使えるらしいので使ってみる。

yarn add sharp

stream API の transform が提供されているっぽいので使う。transformを使いまわしていたけど、二枚目以降が正しく処理されなかったので画像ごとに新しく定義したほうがいいかもしれない。

present.service.ts
  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);
      }
    }
  }

でかめの画像を二枚用意

リサイズ実行

あらかわいい

これでだいたいのやり方はわかりました。あとは責務をどうするか…また別で考えます。

このスクラップは2021/11/24にクローズされました