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

相关推荐
F-2H1 小时前
C语言:指针4(常量指针和指针常量及动态内存分配)
java·linux·c语言·开发语言·前端·c++
gqkmiss2 小时前
Chrome 浏览器插件获取网页 iframe 中的 window 对象
前端·chrome·iframe·postmessage·chrome 插件
m0_748247554 小时前
Web 应用项目开发全流程解析与实战经验分享
开发语言·前端·php
刘大辉在路上4 小时前
突发!!!GitLab停止为中国大陆、港澳地区提供服务,60天内需迁移账号否则将被删除
git·后端·gitlab·版本管理·源代码管理
m0_748255024 小时前
前端常用算法集合
前端·算法
真的很上进5 小时前
如何借助 Babel+TS+ESLint 构建现代 JS 工程环境?
java·前端·javascript·css·react.js·vue·html
web130933203985 小时前
vue elementUI form组件动态添加el-form-item并且动态添加rules必填项校验方法
前端·vue.js·elementui
NiNg_1_2345 小时前
Echarts连接数据库,实时绘制图表详解
前端·数据库·echarts
如若1236 小时前
对文件内的文件名生成目录,方便查阅
java·前端·python
追逐时光者6 小时前
免费、简单、直观的数据库设计工具和 SQL 生成器
后端·mysql