前言
上传文件对平常业务中来说是一个不可避免的需求,本文将会介绍如何在 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
的格式,可以进行分片。
合并
合并的原理就是用node
的fs
模块,把分片上传的每一个小片写到文件中,在写入的过程中需要注意每一个小片段的顺序,然后指定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
的分割方法,然后在后端使用合并方法合并成文件,就完成了大文件上传。