Nest.js 系列——上传文件与大文件分片上传

前言

上传文件对平常业务中来说是一个不可避免的需求,本文将会介绍如何在 nest 中实现文件上传。而且有时候会遇到文件比较大的情况,那么直接上传文件就会出现时间很长,大的文件可能会传几十分钟在那等着肯定不行,这时候就要用分片上传的方式了,而且这个也是面试中经常问到的。

上传文件

nest 中上传文件一般使用 multer 中间件,multer 是一个 node.js 中间件,用于处理 multipart/form-data 类型的表单数据,它主要用于上传文件。首先我们需要安装 multer 中间件:

bash 复制代码
npm install --save @nestjs/platform-express multer

npm install -D @types/multer

然后在 nest 控制器中定义一个上传文件的接口:

typescript 复制代码
import { Controller, Post, UseInterceptors, UploadedFile } from '@nestjs/common'
import { FileInterceptor } from '@nestjs/platform-express'

@Controller('upload')
export class UploadController {
  @Post('upload')
  @UseInterceptors(
    FileInterceptor('file', {
      dest: 'uploads'
    })
  )
  uploadFile(@UploadedFile() file: Express.Multer.File, @Body() body) {
    console.log('body', body)
    console.log('file', file)
  }
}

上面只是一个单文件上传,如果是多个文件上传该怎么做呢?看下后端接口代码该如何编写

typescript 复制代码
import { Controller, Post, UseInterceptors, UploadedFiles } from '@nestjs/common'
import { FilesInterceptor } from '@nestjs/platform-express'

@Controller('upload')
export class UploadController {
  @Post('upload')
  @UseInterceptors(
    FilesInterceptor('files', 3, {
      dest: 'uploads'
    })
  )
  uploadFile(@UploadedFiles() files: Express.Multer.File[], @Body() body) {
    console.log('body', body)
    console.log('files', files)
  }
}

如果有多个字段来接收多个文件,那么我们可以这样写:

typescript 复制代码
import { Controller, Post, UseInterceptors, UploadedFiles } from '@nestjs/common'
import { FilesInterceptor } from '@nestjs/platform-express'

@Controller('upload')
export class UploadController {
  @Post('upload')
  @UseInterceptors(
    FilesInterceptor(
      [
        { name: 'a', maxCount: 2 },
        { name: 'b', maxCount: 3 }
      ],
      {
        dest: 'uploads'
      }
    )
  )
  uploadFile(
    @UploadedFiles() files: { a?: Express.Multer.File[]; b?: Express.Multer.File[] },
    @Body() body
  ) {
    console.log('body', body)
    console.log('files', files)
  }
}

当不知道文件字段名的时候,我们可以使用 AnyFilesInterceptor 来接收所有的文件:

typescript 复制代码
import { Controller, Post, UseInterceptors, UploadedFiles } from '@nestjs/common'
import { AnyFilesInterceptor } from '@nestjs/platform-express'

@Controller('upload')
export class UploadController {
  @Post('upload')
  @UseInterceptors(
    AnyFilesInterceptor({
      dest: 'uploads'
    })
  )
  uploadFile(@UploadedFiles() files: Express.Multer.File[], @Body() body) {
    console.log('body', body)
    console.log('files', files)
  }
}

但是这样上传到文件夹下的文件是一个随机的文件名,如果我们想要自定义文件名,可以这样写:

typescript 复制代码
import { Controller, Post, UseInterceptors, UploadedFiles } from '@nestjs/common'
import { AnyFilesInterceptor } from '@nestjs/platform-express'
import { diskStorage } from 'multer'

@Controller('upload')
export class UploadController {
  @Post('upload')
  @UseInterceptors(
    AnyFilesInterceptor({
      storage: diskStorage({
        destination: 'uploads',
        filename: (req, file, cb) => {
          const randomName = Array(32)
            .fill(null)
            .map(() => Math.round(Math.random() * 16).toString(16))
            .join('')
          return cb(null, `${randomName}${extname(file.originalname)}`)
        }
      })
    })
  )
  uploadFile(@UploadedFiles() files: Express.Multer.File[], @Body() body) {
    console.log('body', body)
    console.log('files', files)
  }
}

上传的文件也可以对其做大小和文件名的校验

typescript 复制代码
import { Controller, Post, UseInterceptors, UploadedFiles } from '@nestjs/common'
import { AnyFilesInterceptor } from '@nestjs/platform-express'

@Controller('upload')
export class UploadController {
  @Post('upload')
  @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
  ) {
    console.log('body', body)
    console.log('file', file)
  }
}

分片上传

大文件分片上传的原理是在前端文件上传的时候对上传的文件进行分割,把大的文件分割成一个一个的小文件。在大文件上传中也是用类型multipart/form-data来进行文件处理。

分片

在对大文件上传之前,就是要对文件进行分片,那么用什么方式对文件进行分片呢?浏览器对Blob文件有一个slice方法,这个方法可以对文件进行分片处理,因为file文件就是Blob的格式,可以进行分片。

合并

合并的原理就是用nodefs模块,把分片上传的每一个小片写到文件中,在写入的过程中需要注意每一个小片段的顺序,然后指定fs写文件的起始位置,这样写入的文件就按照顺序把小的片段文件串起来了,合成了上传的大文件。

实战

首先和上传文件一样,先用cli工具新建一个项目

shell 复制代码
nest new large-file-upload

app.controller.ts中添加一个上传文件的路由

typescript 复制代码
@Post('upload')
  @UseInterceptors(
    FilesInterceptor('files', 20, {
      dest: 'uploads',
    }),
  )
  uploadFiles(
    @UploadedFiles() files: Array<Express.Multer.File>,
    @Body() body,
  ) {
    console.log('body', body);
    console.log('files', files);
  }

会出现类型报错,也和文件上传上面一样,安装一下类型包

shell 复制代码
pnpm add @types/multer -D

开启接口跨域支持,在main.ts中添加app.enableCors()

ts 复制代码
import { NestFactory } from '@nestjs/core'
import { AppModule } from './app.module'

async function bootstrap() {
  const app = await NestFactory.create(AppModule)
  app.enableCors()
  await app.listen(3000)
}
bootstrap()

写一个简单的静态页用来上传文件和处理分片

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" />
    <script>
      const fileInput = document.querySelector('#fileInput')

      const chunkSize = 20 * 1024

      fileInput.onchange = async function () {
        const file = fileInput.files[0]

        console.log(file)

        const chunks = []
        let startPos = 0
        while (startPos < file.size) {
          chunks.push(file.slice(startPos, startPos + chunkSize))
          startPos += chunkSize
        }

        const randomStr = Math.random().toString().slice(2, 8)

        const tasks = []
        chunks.map((chunk, index) => {
          const data = new FormData()
          data.set('name', randomStr + '_' + file.name + '-' + index)
          data.append('files', chunk)
          tasks.push(axios.post('http://localhost:3000/upload', data))
        })
        await Promise.all(tasks)
        axios.get('http://localhost:3000/merge?name=' + randomStr + '_' + file.name)
      }
    </script>
  </body>
</html>

这时候上传的文件是直接在定义的uploads文件夹中的,我们给一个单文件的的所有分片归到一个子文件夹中

ts 复制代码
@Post('upload')
@UseInterceptors(FilesInterceptor('files', 20, {
  dest: 'uploads'
}))
uploadFiles(@UploadedFiles() files: Array<Express.Multer.File>, @Body() body: { name: string }) {
  console.log('body', body);
  console.log('files', files);

  const fileName = body.name.match(/(.+)\-\d+$/)[1];
  const chunkDir = 'uploads/chunks_'+ fileName;

  if(!fs.existsSync(chunkDir)){
    fs.mkdirSync(chunkDir);
  }
  fs.cpSync(files[0].path, chunkDir + '/' + body.name);
  fs.rmSync(files[0].path);
}

现在分片这个步骤已经完成了,然后是合并,定义一个合并的接口merge

ts 复制代码
@Get('merge')
merge(@Query('name') name: string) {
    const chunkDir = 'uploads/chunks_'+ name;

    const files = fs.readdirSync(chunkDir);

    let startPos = 0;
    files.map(file => {
      const filePath = chunkDir + '/' + file;
      const stream = fs.createReadStream(filePath);
      stream.pipe(fs.createWriteStream('uploads/' + name, {
        start: startPos
      })).on('finish',()=>{
        count ++;
        if(count === files.length){
          fs.rm(chunkDir,{ recursive: true},()=>{})
        }
      })

      startPos += fs.statSync(filePath).size;
    })
}

浏览器调用接口就可以把文件进行合并了

shell 复制代码
localhost:3000/merge?name=497791_02.jpg

小结

本文介绍了如何在 nest 中上传文件与大文件上传,主要是使用 multer 中间件,在接口方面使用@UploadedFile()@UploadedFiles()来接收文件,同时也介绍了如何对上传的文件做校验。如果是大文件的话,就采用分片上传,在前端使用blob的分割方法,然后在后端使用合并方法合并成文件,就完成了大文件上传。

相关推荐
蓝莓味柯基1 小时前
React——setState 新旧值复用问题
前端·javascript·react.js
沈健_算法小生1 小时前
Vue3实现类ChatGPT聊天式流式输出(vue-sse实现)
前端·vue.js·chatgpt
浏览器爱好者1 小时前
怎么使用Chrome与C++实现高效自动化测试
前端·c++·chrome
The_massif1 小时前
Flip动画+拖拽API实现一个拖拽排序公共组件
前端·vue.js·react.js
OEC小胖胖1 小时前
js进阶——this和对象原型
前端·javascript·web
点灯的人2 小时前
Camunda源码发布流程
后端
程序员大金2 小时前
基于SpringBoot+Vue+MySQL的特色旅游网站系统
java·前端·vue.js·spring boot·后端·mysql·tomcat
Face2 小时前
JavaScript常用数组、运算符,方法、技巧(长期更新)
前端·javascript
游仙好梦2 小时前
Vitepress 自定义主题开发教程
前端·开源·vitepress
码农小旋风3 小时前
Spring框架总体结构
后端