iT邦幫忙

2021 iThome 鐵人賽

DAY 20
0
Modern Web

NestJS 帶你飛!系列 第 20

[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 來實作檔案儲存。

上一篇
[NestJS 帶你飛!] DAY19 - Module Reference
下一篇
[NestJS 帶你飛!] DAY21 - HTTP Module
系列文
NestJS 帶你飛!32

尚未有邦友留言

立即登入留言