第十九章 Nest multer 文件上传

上章我们了解了Express multer 文件上传的相关操作 本章将了解Nest中的文件上传。用 multer 包处理 multipart/form-data 类型的请求中的 file

新建个 nest 项目:

nest new nest-multer-upload 

1719665836394.png
安装 multer 的 ts 类型的包:

npm install -D @types/multer

1719667026173.png

1、单文件上传

接着创建一个接口,用于文件上传 下面的test1接口就是用于文件上传

import { Body, Controller, Get, Post, UploadedFile, UseInterceptors } from '@nestjs/common';
import { AppService } from './app.service';
import { FileInterceptor } from '@nestjs/platform-express';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) { }

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }

  @Post('test1')
  @UseInterceptors(FileInterceptor('file', {
    dest: 'uploads',
  }))
  uploadFile(@UploadedFile() file: Express.Multer.File, @Body() body: any) {
    console.log('body', body);
    console.log('file', file);

  }
}

使用 FileInterceptor 来提取 file 字段,然后通过 UploadedFile 装饰器把它作为参数传入
接着运行项目可以看到我们的项目根目录下创建了一个uploads名称的文件夹
1719668539032.png
在nestjs项目中设置支持跨域
修改main.ts:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule, {
    cors: true
  });
  await app.listen(3000);
}
bootstrap();

接着 我们来编写前端的代码,创建index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="https://unpkg.com/axios@0.24.0/dist/axios.min.js"></script>
</head>

<body>
    <input id="fileInput" type="file" multiple />
    <script>
        const fileInput = document.querySelector('#fileInput');

        async function formData() {
            const data = new FormData();
            data.set('name', '测试');
            data.set('age', 36);
            data.set('file', fileInput.files[0]);

            const res = await axios.post('http://localhost:3000/test1', data);
            console.log(res);
        }

        fileInput.onchange = formData;
    </script>
</body>

</html>

运行前端:

npx http-server

1719717782702.png
浏览器访问:
1719718137459.png
上传文件之后可以看控制台看到打印了上传的file 对象 和body数据,文件也保存到了 uploads 目录下
1719718212331.png

2、多文件上传

增加test2接口 注意哦 FilesInterceptor UploadedFiles 是加了s的 和之前的单文件上传不一样

@Post('test2')
  @UseInterceptors(FilesInterceptor('files', 3, {
    dest: 'uploads',
  }))
  uploadFiles(@UploadedFiles() files: Express.Multer.File[], @Body() body: any) {
    console.log('body', body);
    console.log('files', files);
  }

接着我们在前端代码新增一个函数 formData2:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="https://unpkg.com/axios@0.24.0/dist/axios.min.js"></script>
</head>

<body>
    <input id="fileInput" type="file" multiple />
    <script>
        const fileInput = document.querySelector('#fileInput');

        async function formData() {
            const data = new FormData();
            data.set('name', '测试');
            data.set('age', 36);
            data.set('file', fileInput.files[0]);

            const res = await axios.post('http://localhost:3000/test1', data);
            console.log(res);
        }

        async function formData2() {
            const data = new FormData();
            data.set('name', '测试');
            data.set('age', 36);
            [...fileInput.files].forEach(item => {
                data.append('file', item);
            })

            const res = await axios.post('http://localhost:3000/test2', data, {
                Headers: { 'content-type': 'multipart/form-data' }
            });

            console.log(res);
        }

        fileInput.onchange = formData2;
    </script>
</body>

</html>

重新运行前端:

npx http-server

接着上传多个文件之后 可以看到控制台打印了 files 文件数组
1719719733484.png

3、多字段上传

接下来我们测试一下多字段上传
新增加 test3接口 使用 FileFieldsInterceptor UploadedFiles

import { Body, Controller, Get, Post, UploadedFile, UploadedFiles, UseInterceptors } from '@nestjs/common';
import { AppService } from './app.service';
import { FileFieldsInterceptor, FileInterceptor, FilesInterceptor } from '@nestjs/platform-express';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) { }

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }

  @Post('test1')
  @UseInterceptors(FileInterceptor('file', {
    dest: 'uploads',
  }))
  uploadFile(@UploadedFile() file: Express.Multer.File, @Body() body: any) {
    console.log('body', body);
    console.log('file', file);
  }


  @Post('test2')
  @UseInterceptors(FilesInterceptor('files', 3, {
    dest: 'uploads',
  }))
  uploadFiles(@UploadedFiles() files: Express.Multer.File[], @Body() body: any) {
    console.log('body', body);
    console.log('files', files);
  }

  @Post('test3')
  @UseInterceptors(FileFieldsInterceptor([
    { name: 'a', maxCount: 2 },
    { name: 'b', maxCount: 3 },
  ]))
  uploadFileFields(@UploadedFiles() files: { aaa?: Express.Multer.File[], bbb?: Express.Multer.File[] }, @Body() body) {
    console.log('body', body);
    console.log('files', files);
  }
}

修改前端代码 增加 formData3 实现上传4个文件 前两个文件上传的时候在a字段 后两个文件上传的时候在b字段实现上传:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="https://unpkg.com/axios@0.24.0/dist/axios.min.js"></script>
</head>

<body>
    <input id="fileInput" type="file" multiple />
    <script>
        const fileInput = document.querySelector('#fileInput');

        async function formData() {
            const data = new FormData();
            data.set('name', '测试');
            data.set('age', 36);
            data.set('file', fileInput.files[0]);

            const res = await axios.post('http://localhost:3000/test1', data);
            console.log(res);
        }

        async function formData2() {
            const data = new FormData();
            data.set('name', '测试');
            data.set('age', 36);
            [...fileInput.files].forEach(item => {
                data.append('files', item);
            })

            const res = await axios.post('http://localhost:3000/test2', data, {
                Headers: { 'content-type': 'multipart/form-data' }
            });

            console.log(res);
        }

        async function formData3() {
            const data = new FormData();
            data.set('name', '测试');
            data.set('age', 36);
            data.append('a', fileInput.files[0])
            data.append('a', fileInput.files[1])
            data.append('b', fileInput.files[2])
            data.append('b', fileInput.files[3])

            const res = await axios.post('http://localhost:3000/test3', data);
            console.log(res);
        }

        fileInput.onchange = formData3;
    </script>
</body>

</html>

重新运行前端:

npx http-server

接着打开 http://127.0.0.1:8080/ 上传4个文件
1719723208742.png
可以看到 a 字段收到了2个文件 b字段收到了2个文件

4、任意字段上传

如果我们不知道有哪些字段是上传的 可以使用AnyFilesInterceptor,新建接口test4:

@Post('test4')
  @UseInterceptors(AnyFilesInterceptor({
    dest: 'uploads',
  }))
  uploadAnyFiles(@UploadedFiles() files: Array<Express.Multer.File>, @Body() body) {
    console.log('body', body);
    console.log('files', files);
  }

修改前端代码 增加 formData4 函数:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="https://unpkg.com/axios@0.24.0/dist/axios.min.js"></script>
</head>

<body>
    <input id="fileInput" type="file" multiple />
    <script>
        const fileInput = document.querySelector('#fileInput');

        async function formData() {
            const data = new FormData();
            data.set('name', '测试');
            data.set('age', 36);
            data.set('file', fileInput.files[0]);

            const res = await axios.post('http://localhost:3000/test1', data);
            console.log(res);
        }

        async function formData2() {
            const data = new FormData();
            data.set('name', '测试');
            data.set('age', 36);
            [...fileInput.files].forEach(item => {
                data.append('files', item);
            })

            const res = await axios.post('http://localhost:3000/test2', data, {
                Headers: { 'content-type': 'multipart/form-data' }
            });

            console.log(res);
        }

        async function formData3() {
            const data = new FormData();
            data.set('name', '测试');
            data.set('age', 36);
            data.append('a', fileInput.files[0])
            data.append('a', fileInput.files[1])
            data.append('b', fileInput.files[2])
            data.append('b', fileInput.files[3])

            const res = await axios.post('http://localhost:3000/test3', data);
            console.log(res);
        }

        async function formData4() {
            const data = new FormData();
            data.set('name', '测试');
            data.set('age', 36);
            data.set('aaa', fileInput.files[0]);
            data.set('bbb', fileInput.files[1]);
            data.set('ccc', fileInput.files[2]);
            data.set('ddd', fileInput.files[3]);

            const res = await axios.post('http://localhost:3000/test4', data);
            console.log(res);
        }

        fileInput.onchange = formData4;
    </script>
</body>

</html>

重新运行前端:

npx http-server

接着打开 http://127.0.0.1:8080/ 上传4个文件 可以看到识别了所有字段
1719724483848.png
同时也可以指定storage,转换上传的文件名称 创建 storage.ts

import * as multer from "multer";
import * as fs from 'fs';
import * as path from "path";

const storage = multer.diskStorage({
    destination: function (req, file, cb) {
        try {
            fs.mkdirSync(path.join(process.cwd(), 'my-uploads'));
        }catch(e) {}

        cb(null, path.join(process.cwd(), 'my-uploads'))
    },
    filename: function (req, file, cb) {
        const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9) + '-' + file.originalname
        cb(null, file.fieldname + '-' + uniqueSuffix)
    }
});

export { storage };

修改之前的test4接口:

import { Body, Controller, Get, Post, UploadedFile, UploadedFiles, UseInterceptors } from '@nestjs/common';
import { AppService } from './app.service';
import { AnyFilesInterceptor, FileFieldsInterceptor, FileInterceptor, FilesInterceptor } from '@nestjs/platform-express';
import { storage } from './storage ';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) { }

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }

  @Post('test1')
  @UseInterceptors(FileInterceptor('file', {
    dest: 'uploads',
  }))
  uploadFile(@UploadedFile() file: Express.Multer.File, @Body() body: any) {
    console.log('body', body);
    console.log('file', file);
  }


  @Post('test2')
  @UseInterceptors(FilesInterceptor('files', 3, {
    dest: 'uploads',
  }))
  uploadFiles(@UploadedFiles() files: Express.Multer.File[], @Body() body: any) {
    console.log('body', body);
    console.log('files', files);
  }

  @Post('test3')
  @UseInterceptors(FileFieldsInterceptor([
    { name: 'a', maxCount: 2 },
    { name: 'b', maxCount: 3 },
  ]))
  uploadFileFields(@UploadedFiles() files: { aaa?: Express.Multer.File[], bbb?: Express.Multer.File[] }, @Body() body) {
    console.log('body', body);
    console.log('files', files);
  }

  @Post('test4')
  @UseInterceptors(AnyFilesInterceptor({
    dest: 'uploads',
    storage: storage
  }))
  uploadAnyFiles(@UploadedFiles() files: Array<Express.Multer.File>, @Body() body) {
    console.log('body', body);
    console.log('files', files);
  }


}

再次打开游览器 http://127.0.0.1:8080/ 上传4个文件
1719730782749.png

5、文件上传限制

接下来我们对上传的文件进行显示 文件大小、类型等,这部分我们在pipe里实现

nest g pipe file-size-validation-pipe --no-spec --flat

1719730968608.png
修改ize-validation-pipe.pipe.ts 设置文件要小于20kb

import { PipeTransform, Injectable, ArgumentMetadata, HttpException, HttpStatus } from '@nestjs/common';

@Injectable()
export class FileSizeValidationPipe implements PipeTransform {
  transform(value: Express.Multer.File, metadata: ArgumentMetadata) {
    if(value.size > 20 * 1024) {
      throw new HttpException('文件大于 20k', HttpStatus.BAD_REQUEST);
    }
    return value;
  }
}

添加到接口里 FileSizeValidationPipe:

@Post('test1')
  @UseInterceptors(FileInterceptor('file', {
    dest: 'uploads',
  }))
  uploadFile(@UploadedFile(FileSizeValidationPipe) file: Express.Multer.File, @Body() body: any) {
    console.log('body', body);
    console.log('file', file);
  }

接着把前端代码修改一下

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="https://unpkg.com/axios@0.24.0/dist/axios.min.js"></script>
</head>

<body>
    <input id="fileInput" type="file" multiple />
    <script>
        const fileInput = document.querySelector('#fileInput');

        async function formData() {
            const data = new FormData();
            data.set('name', '测试');
            data.set('age', 36);
            data.set('file', fileInput.files[0]);

            const res = await axios.post('http://localhost:3000/test1', data);
            console.log(res);
        }

        async function formData2() {
            const data = new FormData();
            data.set('name', '测试');
            data.set('age', 36);
            [...fileInput.files].forEach(item => {
                data.append('files', item);
            })

            const res = await axios.post('http://localhost:3000/test2', data, {
                Headers: { 'content-type': 'multipart/form-data' }
            });

            console.log(res);
        }

        async function formData3() {
            const data = new FormData();
            data.set('name', '测试');
            data.set('age', 36);
            data.append('a', fileInput.files[0])
            data.append('a', fileInput.files[1])
            data.append('b', fileInput.files[2])
            data.append('b', fileInput.files[3])

            const res = await axios.post('http://localhost:3000/test3', data);
            console.log(res);
        }

        async function formData4() {
            const data = new FormData();
            data.set('name', '测试');
            data.set('age', 36);
            data.set('aaa', fileInput.files[0]);
            data.set('bbb', fileInput.files[1]);
            data.set('ccc', fileInput.files[2]);
            data.set('ddd', fileInput.files[3]);

            const res = await axios.post('http://localhost:3000/test4', data);
            console.log(res);
        }

        fileInput.onchange = formData;
    </script>
</body>

</html>

重新运行前端: 上传文件一张大于20kb的图片或文件

npx http-server

可以发现接口报错 这样就可以实现文件的校验
1719731591999.png
其实 Nest 内置了文件大小、类型的校验 接下来我们测试一下:
修改test1接口 使用Nest 内置的 ParseFilePipe,它的作用是调用传入的 validator 来对文件做校验。
比如 MaxFileSizeValidator 是校验文件大小、FileTypeValidator 是校验文件类型。

@Post('test1')
  @UseInterceptors(FileInterceptor('file', {
    dest: 'uploads',
  }))
  uploadFile(@UploadedFile(new ParseFilePipe({
    validators: [
      new MaxFileSizeValidator({ maxSize: 1000 }),
      new FileTypeValidator({ fileType: 'image/jpeg' })
    ]
  })) file: Express.Multer.File, @Body() body: any) {
    console.log('body', body);
    console.log('file', file);
  }

接下来我们再上传文件测试一下 可以看到返回了400和具体错误信息
1719731967044.png
接下来我们自定义一下返回的错误信息: 使用 exceptionFactory 自定义错误信息

@Post('test1')
  @UseInterceptors(FileInterceptor('file', {
    dest: 'uploads',
  }))
  uploadFile(@UploadedFile(new ParseFilePipe({
    validators: [
      new MaxFileSizeValidator({ maxSize: 1000 }),
      new FileTypeValidator({ fileType: 'image/jpeg' })
    ],
    exceptionFactory(error) {
      throw new HttpException('测试' + error, 400);
    },
  })) file: Express.Multer.File, @Body() body: any) {
    console.log('body', body);
    console.log('file', file);
  }

再次上传可以看到返回了自定义的错误信息
1719732165228.png
我们也可以自己实现Validator
创建 my-file-validator.ts

import { FileValidator } from "@nestjs/common";

export class MyFileValidator extends FileValidator{
    constructor(options) {
        super(options);
    }

    isValid(file: Express.Multer.File): boolean | Promise<boolean> {
        if(file.size > 10000) {
            return false;
        }
        return true;
    }
    buildErrorMessage(file: Express.Multer.File): string {
        return `文件 ${file.originalname} 大小超出 10k`;
    }
}

修改接口使用自定义的validator:

@Post('test1')
  @UseInterceptors(FileInterceptor('file', {
    dest: 'uploads',
  }))
  uploadFile(@UploadedFile(new ParseFilePipe({
    validators: [
      new MyFileValidator({}),
    ],
    exceptionFactory(error) {
      throw new HttpException('测试' + error, 400);
    },
  })) file: Express.Multer.File, @Body() body: any) {
    console.log('body', body);
    console.log('file', file);
  }

上传文件:
1719732568683.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

枫ゞ

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值