[NestJS] 파일 업로드

김승현·2023년 12월 31일
0
post-thumbnail

package 설치

npm i -D @types/multer

단일 파일 업로드

FileInterceptor() 는 두개의 인자를 가진다.

  • fieldName
    • 파일을 담고 있는 HTML form의 필드 이름을 제공하는 문자열.
    • multipart/form-data 에서 name에 해당하는 값
  • options : (option) MulterOptions 타입의 객체 [참조]
import { Controller, Post, UploadedFile, UseInterceptors} from '@nestjs/common';
import { FileService } from './file.service';
import { FileInterceptor } from '@nestjs/platform-express';

@Controller('file')
export class FileController {
  constructor(private readonly fileService: FileService) {}

  @Post('upload')
  @UseInterceptors(FileInterceptor('file'))
  upload(@UploadedFile() file: Express.Multer.File) {
    console.log(file);
  }
}

MulterOptions

Option을 생략하면 파일은 디스크가 아니라 메모리에 저장되며 기본적으로 Multer는 이름의 중복을 방지하기 위해 내부적으로 임의로 이름을 생성하며 확장자는 붙어있지 않게 된다.

  • dest : 파일 저장 위치 설정. 지정한 path에 디렉토리가 존재하지 않으면 자동적으로 생성되고 확장자가 없는 상태에서 Multer 내부에서 임의로 생성한 이름으로 저장된다.
  • storage : dest 보다 세밀하게 upload를 제어하고 싶을때 사용. DiskStorage , MemoryStorage를 제공
    • DiskStorage 기준로 설명. destinationfilename을 설정할 수 있다.
      • destination : 파일 저장 위치 설정. 문자열이나 함수로 설정할 수 있다. 문자열로 설정할 경우 해당 디렉토리가 존재 하지 않으면 자동적으로 생성된다. 문자열이나 함수를 전달하지 않으면 기본적으로 os.tmpdir() 로 설정된다.
      • filename : 업로드한 파일의 이름을 설정하는 함수. 설정하지않으면 Multer는 확장자가 없는 32자의 난수 16진수 문자열을 생성한다.
  • fileFilter : 업로드할 파일을 필터링하는 함수. 원하는 파일 형식만 허용하거나 거부할 수 있다.
  • limits : 업로드할 파일의 크기 및 파일 개수 제한
    KeyDescriptionDefault
    fieldNameSize최대 필드 이름 크기100 bytes
    fieldSize최대 필드 값 크기(바이트)1MB
    fields비파일 필드의 최대 개수Infinity
    fileSizemultipart forms의 최대 파일 크기(바이트)Infinity
    filesmultipart forms의 최대 파일 필드의 수Infinity
    partsmultipart/form-data의 파일과 필드의 개수Infinity
    headerPairsmultipart form에서 파싱할 헤더의 key-value 쌍의 최대 개수2000
  • preservePath : 파일의 경로를 보존할지 여부 설정. 기본값은 false이며, true로 설정하면 클라이언트가 제공한 파일 경로를 그대로 사용한다.

코드 예시
import { Controller, Post, UploadedFile, UseInterceptors } from '@nestjs/common';
import { FileService } from './file.service';
import { FileInterceptor } from '@nestjs/platform-express';
import { diskStorage } from 'multer';
import { extname } from 'path';

@Controller('file')
export class FileController {
  constructor(private readonly fileService: FileService) {}

  @Post('upload')
  @UseInterceptors(
    FileInterceptor('image', {
      storage: diskStorage({
        destination: './uploads',
        filename(_, file, cb): void {
          const randomName = Array(32)
            .fill(null)
            .map(() => Math.round(Math.random() * 16).toString(16))
            .join('');
          return cb(null, `${randomName}${extname(file.originalname)}`);
        },
      }),
      fileFilter(req, file, callback) {
        if (!file.originalname.match(/\.(jpg|jpeg|png)$/)) {
          return callback(new Error('Only .jpg, .jpeg, .png format allowed!'), false);
        }
        callback(null, true);
      },
      limits: {
        fileSize: 1024 * 1024 * 5,
      },
    }),
  )
  upload(@UploadedFile() files: Express.Multer.File) {
    console.log(files);
  }
}

Custom Interceptor를 사용하여 분리

upload-interceoptor.ts

import { Injectable, NestInterceptor } from '@nestjs/common';
import { CallHandler, ExecutionContext, RequestHandler } from '@nestjs/common/interfaces';
import * as multer from 'multer';
import { diskStorage } from 'multer';
import { extname } from 'path';
import { Observable } from 'rxjs';

@Injectable()
export class UploadInterceptor implements NestInterceptor {
  private upload: RequestHandler;

  constructor() {
    this.upload = multer({
      storage: diskStorage({
        destination: './uploads',
        filename(_, file, cb): void {
          const randomName = Array(32)
            .fill(null)
            .map(() => Math.round(Math.random() * 16).toString(16))
            .join('');
          return cb(null, `${randomName}${extname(file.originalname)}`);
        },
      }),
      fileFilter(req, file, callback) {
        if (!file.originalname.match(/\.(jpg|jpeg|png)$/)) {
          return callback(new Error('Only .jpg, .jpeg, .png format allowed!'));
        }
        callback(null, true);
      },
      limits: {
        fileSize: 1024 * 1024 * 5,
      },
    }).single('image');
  }
  intercept(context: ExecutionContext, next: CallHandler<any>): Observable<any> | Promise<Observable<any>> {
    const req = context.switchToHttp().getRequest();
    return new Observable((observer) => {
      this.upload(req, null, async (err: any) => {
        if (err) observer.error(err);
        else next.handle().subscribe(observer);
      });
    });
  }
}

앞선 컨트롤러 단에서 FileInterceptor를 사용하였을때는 해당 인터셉터들 사용하여 multer 함수를 따로 만들어 줄 필요이 제공받을 수 있는 반면 커스텀 인터셉터의 경우 constructor 내부에 multer 함수를 직접 정의한다.

file.controller.ts

import { Controller, Post, UploadedFile, UseInterceptors } from '@nestjs/common';
import { FileService } from './file.service';
import { UploadInterceptor } from 'src/interceptor/upload-interceptor';

@Controller('file')
export class FileController {
  constructor(private readonly fileService: FileService) {}

  @Post('upload')
  @UseInterceptors(UploadInterceptor)
  upload(@UploadedFile() files: Express.Multer.File) {
    console.log(files);
  }
}

File validation

  • 파일 크기나 파일의 mime-type과 같은 파일 메타 데이터의 유효성을 검사하는 기능
  • ParseFilePipeBuilder 클래스를 사용하여 아래와 같이 각 유효성 검사기의 수동 인스턴스화를 피하고 옵션을 직접 전달할 수 있다.
@Post('upload')
@UseInterceptors(FileInterceptor('file'))
upload(
  @UploadedFile(
    new ParseFilePipeBuilder()
      .addFileTypeValidator({ fileType: /(mp4|png)$/ })
      .addMaxSizeValidator({ maxSize: 1000 * 1024 * 1024 })
      .build({ errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY }),
  )
  file: Express.Multer.File,
) {
  console.log(file);
}
  • addFileTypeValidator()fileType은 문자열 또는 정규식 타입으로 설정
  • addMaxSizeValidator()maxSizemessage 설정
    • maxSize : 바이트 기준
    • message : 오류 발생 메시지 설정
  • builderrorHttpStatusCode 는 오류 코드 설정

멀티 파일 업로드

단일 field name(파일 배열)으로 여러 파일 업로드

FilesInterceptor() 는 세개의 인자를 가진다.

  • fieldName
    • 파일을 담고 있는 HTML form의 필드 이름을 제공하는 문자열.
    • multipart/form-data 에서 name에 해당하는 값
  • maxCount : (option) 최대 파일 수를 설정
  • options : (option) MulterOptions 타입의 객체 [참조]

FilesInterceptor() 를 사용하는 경우 @UploadedFiles() 데코레이터를 사용하여 request에서 파일을 추출한다.

import {
  Controller,
  Post,
  UseInterceptors,
  UploadedFiles,
} from '@nestjs/common';
import { FileService } from './file.service';
import { FilesInterceptor } from '@nestjs/platform-express';

@Controller('file')
export class FileController {
  constructor(private readonly fileService: FileService) {}

  @Post('upload')
  @UseInterceptors(FilesInterceptor('files'))
  upload(
    @UploadedFiles()
    files: Array<Express.Multer.File>,
  ) {
    console.log(files);
  }
}

field name이 각기 다른 여러 파일 업로드

  • FileFieldsInterceptor() 데코레이터 이용하고, 이 데코레이터는 2개의 인자를 받는다.
    • uploadedFields : 객체의 배열로, 각 객체는 2개의 속성을 설정할 수 있다.
      • name 속성을 통해 필드 이름을 문자열 값으로 설정
      • maxCount : (option) 최대 파일 수를 설정
    • options : (option) MulterOptions 타입의 객체 [참조]
import {
  Controller,
  Post,
  UseInterceptors,
  UploadedFiles,
} from '@nestjs/common';
import { FileService } from './file.service';
import { FileFieldsInterceptor } from '@nestjs/platform-express';

@Controller('file')
export class FileController {
  constructor(private readonly fileService: FileService) {}

  @Post('upload')
  @UseInterceptors(
    FileFieldsInterceptor([
      { name: 'image', maxCount: 1 },
      { name: 'video', maxCount: 1 },
    ]),
  )
  upload(
    @UploadedFiles()
    files: {
      image?: Express.Multer.File[];
      video?: Express.Multer.File[];
    },
  ) {
    console.log(files);
  }
}

파일과 데이터 한번에 처리하기

file.controller.ts

import { Body, Controller, Post, UploadedFile, UseInterceptors } from '@nestjs/common';
import { FileService } from './file.service';
import { UploadInterceptor } from 'src/interceptor/upload-interceptor';
import { CreateFileDto } from './dto/create-file.dto';

@Controller('file')
export class FileController {
  constructor(private readonly fileService: FileService) {}

  @Post('upload')
  @UseInterceptors(UploadInterceptor)
  upload(@UploadedFile() files: Express.Multer.File, @Body() createFileDto: CreateFileDto) {
    console.log(files);
		console.log(createFileDto);
  }
}

create-file.dto.ts

export class CreateFileDto {
  title: string;
  content: string;
  image: any;
}

console.log

{
  fieldname: 'image',
  originalname: '테스트 이미지.png',
  encoding: '7bit',
  mimetype: 'image/png',
  destination: './uploads',
  filename: '0fc031767a579eee74fce9d0d49e161f.png',
  path: 'uploads/0fc031767a579eee74fce9d0d49e161f.png',
  size: 20751
}
[Object: null prototype] { title: '제목이요!', content: '컨텐츠' }

프로트단에서는 formdata.append() 를 이용하여 key-value 형태로 데이터를 추가하면 된다.



참고

Documentation | NestJS - A progressive Node.js framework

GitHub - expressjs/multer: Node.js middleware for handling multipart/form-data.

[NestJS] File-upload & Use of Interceptor (feat. nest를 바라보는 시각..)

[Nest] 이미지와 데이터 처리하기 (with. FormData)

profile
개발자로 매일 한 걸음

0개의 댓글