[NestJS 带你飞!] DAY20 - File Upload

档案上传(File Upload) 是一项很基本的功能,到处都可以看见它的踪影,如:某某社群网站的上传大头贴、某某影音网站上传影片等。

Nest 针对档案上传功能封装了一套名为 multer 的套件,它会处理格式为 multipart/form-data 的资料,在 Express 的应用程序上经常可以看到它的身影,是非常知名的套件。

使用 multer

虽然 Nest 将其包装成内建模组,但还是建议各位安装 multer 的型别定义档,透过 npm 来进行安装:

$ npm install @types/multer -D

单一档案上传

接收单一档案的方式很简单,只要在特定路由下使用 FileInterceptor 并透过参数装饰器 @UploadedFile 来取得档案。其中,FileInterceptor 有两个参数可以带入,分别是:

  1. fieldName:档案在表单上对应的名称。
  2. options:对应到 MulterOption,详细内容可以参考 multer 官方文档

这边以 app.controller.ts 为例来实作单一档案上传:

import { Controller, Post, UploadedFile, UseInterceptors } from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';

@Controller()
export class AppController {

  @Post('/single')
  @UseInterceptors(FileInterceptor('file'))
  uploadSingleFile(@UploadedFile() file: Express.Multer.File) {
    return file;
  }
  
}

透过 Postman 进行测试,将一个档案名称为 nestjs_logo.svg 的图片上传,会收到该图片的相关讯息:
https://ithelp.ithome.com.tw/upload/images/20210607/201193384WKffjxwBt.png

单一栏位之多个档案上传

如果同一个栏位名称有一个以上的档案,要使用 FilesInterceptor 并透过参数装饰器 @UploadedFiles 来取得一个包含多个 Express.Multer.File 型别的阵列。

注意:这里是使用复数 Files 而不是单一档案上传所使用的 FileInterceptor@UploadedFile

FilesInterceptor 有三个参数可以带入,分别是:

  1. fieldName:档案在表单上对应的名称。
  2. maxCount:配置可接受档案数量的上限,可以选择性填入。
  3. options:对应到 MulterOption

同样以 app.controller.ts 为例来实作单一栏位多档上传:

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

@Controller()
export class AppController {

  @Post('/multiple')
  @UseInterceptors(FilesInterceptor('files'))
  uploadMultipleFiles(@UploadedFiles() files: Express.Multer.File[]) {
    return files.map(({ fieldname, originalname }) => ({ fieldname, originalname }));
  }

}

透过 Postman 进行测试,将档案名称为 nestjs_logo.svgnodejs_logo.png 的图片上传,会收到它们的栏位名称与档案名称:
https://ithelp.ithome.com.tw/upload/images/20210607/20119338WgyxizFwJU.png

多栏位之多个档案上传

假如表单有多个栏位并且有一个以上的栏位包含档案,要使用 FileFieldsInterceptor 并透过 @UploadedFiles 装饰器来取得一个以栏位名称作为 key 的物件,其值为 Express.Multer.File 型别的阵列。其中,FileFieldsInterceptor 有两个参数可以带入:

  1. uploadedFields:一个包含多个物件的阵列,物件需要拥有 name 属性来指定栏位的名称,亦可以给定 maxCount 来指定该栏位可接受的档案数量上限。
  2. options:对应到 MulterOption

同样以 app.controller.ts 为例来实作多栏位多档案上传:

import { Controller, Post, UploadedFiles, UseInterceptors } from '@nestjs/common';
import { FileFieldsInterceptor } from '@nestjs/platform-express';

@Controller()
export class AppController {

  @Post('/multiple')
  @UseInterceptors(FileFieldsInterceptor([
    { name: 'first' },
    { name: 'second' }
  ]))
  uploadMultipleFiles(@UploadedFiles() files: { [x: string]: Express.Multer.File[] }) {
    const { first, second } = files;
    const list = [...first, ...second];
    return list.map(({ fieldname, originalname }) => ({ fieldname, originalname }));
  }

}

透过 Postman 进行测试,将档案名称为 nestjs_logo.svgnodejs_logo.png 的图片上传,会收到它们的栏位名称与档案名称:
https://ithelp.ithome.com.tw/upload/images/20210608/20119338Nlo1xRRYdG.png

不分栏位之多个档案上传

假如表单有多个栏位并且有一个以上的栏位包含档案,但不需要依照栏位名称做分类的话,可以直接使用 AnyFilesInterceptor 并透过 @UploadedFiles 装饰器来取得一个包含多个 Express.Multer.File 型别的阵列。其中,AnyFilesInterceptor 可以带入一个参数,即 options

同样以 app.controller.ts 为例来实作不分栏位多档上传:

import { Controller, Post, UploadedFiles, UseInterceptors } from '@nestjs/common';
import { AnyFilesInterceptor } from '@nestjs/platform-express';

@Controller()
export class AppController {

  @Post('/multiple')
  @UseInterceptors(AnyFilesInterceptor())
  uploadMultipleFiles(@UploadedFiles() files: Express.Multer.File[]) {
    return files.map(({ fieldname, originalname }) => ({ fieldname, originalname }));
  }

}

透过 Postman 进行测试,将档案名称为 nestjs_logo.svgnodejs_logo.png 的图片上传,会收到它们的栏位名称与档案名称:
https://ithelp.ithome.com.tw/upload/images/20210608/20119338gTlUTkFwhx.png

预设 multer 设置

上面每个功能都可以指定 MulterOption 的配置,假如有个配置是多数上传档案都会用到的,那每次都要个别配置实在太麻烦了,所以 Nest 有提供一个预设值的方法,大幅减少这种重复的操作,那该如何使用呢?只要导入 MulterModule 并调用 register 方法即可,该方法可接受之参数正是 MulterOption

这里以 app.module.ts 为例,假如我们希望把上传的档案存到名为 upload 的资料夹里,那就在 register 里面给定 dest 属性,并指定其值为 ./upload

import { Module } from '@nestjs/common';
import { MulterModule } from '@nestjs/platform-express';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [
    MulterModule.register({
      dest: './upload'
    })
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

实作档案储存

我们沿用「预设 multer 设置」与「不分栏位之多个档案上传」的范例进行测试,透过 Postman 上传 nestjs_logo.svgnodejs_logo.png,会在专案目录下看到 upload 资料夹,里面含有以下内容:
https://ithelp.ithome.com.tw/upload/images/20210608/20119338nWuiXByVyp.png

奇怪,怎麽跟预期的不一样?是哪里出错了吗?其实这是因为 multer 不知道你储存的档案类型与名称要叫什麽,并没有给它一个明确的定义,所以才会看到没有副档名的随机名称档案,这里可以做个小实验,将这两个档案的档案名称与副档名改成预期的样子,就可以看到他们的原貌了:
https://ithelp.ithome.com.tw/upload/images/20210608/20119338fTsR6RBhPi.png

但这并不是解决问题的好方法,我们会希望能够自动化去处理这件事情,那该怎麽做呢?这时候可以用 multer 提供的 diskStorage 来辅助我们去处理档案名称的问题。

diskStorage 是一个函式,我们可以透过指定 destination 来配置档案的存放位置、指定 filename 去处理档案名称,这两个属性的值皆为 函式,透过函式去处理的弹性比较大,毕竟给特定值并不适用在每个场景。

我们透过撰写一个 Helper Class 来实作这两个函式,在 src 资料夹下新增 core/helpers 资料夹,并添加 multer.helper.ts,由於这两个函式有特定的参数,故我们的方法也需要遵循这些参数来设计,其包含了 RequestExpress/Multer.File 以及 (error: Error | null, destination: string) => void 的 Callback 函式,透过该 Callback 将处理好的结果返回给 multer:

import { Request } from 'express';
import { join } from 'path';

export class MulterHelper {

  public static destination(
    request: Request,
    file: Express.Multer.File,
    callback: (error: Error | null, destination: string) => void
  ): void {
    callback(null, join(__dirname, '../../../upload/'));
  }

  public static filenameHandler(
    request: Request,
    file: Express.Multer.File,
    callback: (error: Error | null, destination: string) => void
  ): void {
    const { originalname } = file;
    const timestamp = new Date().toISOString();
    callback(null, `${timestamp}-${originalname}`);
  }

}

接着,我们就来将这两个函式实装上去,修改 app.module.ts 的内容,将 register 物件参数中的 dest 换成 storage,并配置 destinationfilename

import { Module } from '@nestjs/common';
import { MulterModule } from '@nestjs/platform-express';

import { diskStorage } from 'multer';

import { MulterHelper } from './core/helpers/multer.helper';

import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [
    MulterModule.register({
      storage: diskStorage({
        destination: MulterHelper.destination,
        filename: MulterHelper.filenameHandler
      })
    })
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

最後,透过 Postman 进行测试,将 nestjs_logo.svgnodejs_logo.png 上传,会在专案目录下的 upload 资料夹看到这两个档案:
https://ithelp.ithome.com.tw/upload/images/20210608/20119338ZpSX8WTlg9.png

小结

multer 将档案上传功能简化成套用 Middleware 即可使用,Nest 更进一步进行包装,使其可以很轻易地在 Nest 中使用,让它的使用方式更符合 Nest 的设计原则,是非常好用且强大的套件。这里附上今天的懒人包:

  1. Nest 使用 multer 作为档案上传模组的基础。
  2. multer 仅接受格式为 multipart/form-data 的资料。
  3. 单一档案上传需使用 FileInterceptor 并透过 @UploadedFile 来取得档案资料。
  4. 单一栏位多个档案上传需使用 FilesInterceptor 并透过 @UploadedFiles 来取得档案资料。
  5. 多栏位多个档案上传需使用 FileFieldsInterceptor 并透过 @UploadedFiles 来取得档案资料。
  6. 不分栏位多个档案上传需使用 AnyFilesInterceptor 并透过 @UploadedFiles 来取得档案资料。
  7. 透过 MulterModule.register() 来配置 multer 预设值。
  8. 透过 storage 属性与 diskStorage 来实作档案储存。

<<:  Rust-并行&并发(一)

>>:  Day20 Plugin 从零开始到上架 - 取得授权码(iOS)

Golang - 使用docker部署专案

之前就有做过这件事情 当时搞定了之後想说,简单吗~~~就是搞个Dockerfile而已 结果好一阵子...

【心得】Google Fonts使用

练习刻板面时常常会遇到形形色色的字体 但若不是电脑本身有下载该字体的话,即便设定了还是会以预设字体呈...

JavaScript - 做个录音录影功能ㄅ

大家好!今天这篇主要是实作浏览器上的录音与录影功能,这边先列出几个会做到的目标 显示视讯画面与声音 ...

16. PHPer x Composer x PSR

今天是讲座笔记,内容来自 PHP也有Day #60 composer & vite ,建议...

DAY22-导览设计之Sidebar

前言: 今天我们要来完成前面提到的Sidebar,我会从Navbar接着开始接着讲,那就让我们开始...