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的分割方法,然后在后端使用合并方法合并成文件,就完成了大文件上传。

相关推荐
柯南二号23 分钟前
HarmonyOS ArkTS 下拉列表组件
前端·javascript·数据库·harmonyos·arkts
wyy729325 分钟前
v-html 富文本中图片使用element-ui image-viewer组件实现预览,并且阻止滚动条
前端·ui·html
前端郭德纲37 分钟前
ES6的Iterator 和 for...of 循环
前端·ecmascript·es6
王解43 分钟前
【模块化大作战】Webpack如何搞定CommonJS与ES6混战(3)
前端·webpack·es6
欲游山河十万里44 分钟前
(02)ES6教程——Map、Set、Reflect、Proxy、字符串、数值、对象、数组、函数
前端·ecmascript·es6
明辉光焱44 分钟前
【ES6】ES6中,如何实现桥接模式?
前端·javascript·es6·桥接模式
源码12151 小时前
ASP.NET MVC宠物商城系统
后端·asp.net·宠物
PyAIGCMaster1 小时前
python环境中,敏感数据的存储与读取问题解决方案
服务器·前端·python
baozhengw1 小时前
UniAPP快速入门教程(一)
前端·uni-app
nameofworld1 小时前
前端面试笔试(二)
前端·javascript·面试·学习方法·数组去重