初始化nest项目
nest new nestUploadFile
使用nest 命令生成模块
nest generate resource file
该命令生成的是一个完整的模块代码,如果只用其中一些可以用单独生成的命令或者自己手动创建文件。通过命令生成的模块会自动注入到app.module.ts 文件中,自己的生成的话别忘了手动注入。
耽误生成文件的命令:
nest generate controller file // 单独生成controller层
nest generate service file // 单独生成service层
nest generate module file // 单独生成module层
文件上传依赖 @types/multer ****需提前安装
文件上传的多种情况
项目生成后使用命令 pnpm run start:dev 启动项目
@Post('/upload')
@UseInterceptors(FileIntÏerceptor('file')) // 获取请求中file字段
async
uploadFile(@UploadedFile() file: Express.Multer.File) {
console.log(file)
}
获取到文件手动保存
通过buffer字段就可以拿到文件内容,使用fs模块读取buffer内容报存到指定位置。
export const saveFile = (
file: Express.Multer.File,
): { fileName: string; fullPath: string; size: string } => {
// 生成随机字符串
let randomString = Math.random().toString(36);
randomString = randomString.substr(2);
const lastName = file.originalname.split('.').pop();
const _fileName = randomString + '.' + lastName;
const filePath = path.join(root, '/uploads', _fileName);
fs.writeFileSync(filePath, file.buffer);
};
获取到文件自动保存
当然拦截器 FileInterceptor 中第二个参数可以接受多个值,可以不用自己写保存方法。参数内容multerOptions
| Key | Description |
|---|---|
destor storage | Where to store the files 文件存储位置 |
fileFilter | Function to control which files are accepted |
limits | Limits of the uploaded data |
preservePath | Keep the full path of files instead of just the base name |
当你设置了 dest 就会自动保存到指定的存储位置,此值无法保存文件后缀。
@Post('/upload1')
@UseInterceptors(FileInterceptor('file',{
dest:'uploads'
}))
async uploadFile1(@UploadedFile() file: Express.Multer.File) {
console.log(file)
}
但是可以设置storage 该key可以指定存储位置,而且可以自定义存储的文件名称
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, '/tmp/my-uploads')
},
filename: function (req, file, cb) {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9)
cb(null, file.fieldname + '-' + uniqueSuffix)
}
})
@Post('/upload2')
@UseInterceptors(FileInterceptor('file', {
storage: storage
}))
async uploadFile2(@UploadedFile() file: Express.Multer.File) {
console.log(file)
}
处理多个文件
文件名相同(数组)情况
拦截器 FilesInterceptor 装饰器 UploadedFiles 是的 都加了s
FilesInterceptor 可以传三个参数 (filename、maxCount、multerOptions)
@Post('/upload/samename')
@UseInterceptors(FilesInterceptor('file', 10, {
storage
}))
async uploadFile3(@UploadedFiles() file: Array<Express.Multer.File>) {
console.log(file)
}
多个不同文件名情况
文件名已知
@Post('upload')
@UseInterceptors(FileFieldsInterceptor([
{ name: 'file1', maxCount: 1 },
{ name: 'file2', maxCount: 1 },
],{
storage
}))
async uploadFile(@UploadedFiles() files: { file1?: Express.Multer.File[], file2?: Express.Multer.File[] }) {
console.log(file1);
console.log(file2);
}
文件名未知
在不知道有哪些字段是文件的时候,可以使用AnyFilesInterceptor 并且配合 UploadedFiles 装饰器使用。
@Post('upload/notknow')
@UseInterceptors(AnyFilesInterceptor({
storage
}))
async uploadFile(@UploadedFiles() files: Array<Express.Multer.File>) {
console.log(files);
}
@Post('upload/notknow') 一定要写到 @Post('upload/')前面否则匹配到updload就不会匹配notknow了
大文件上传
遇到文件size比较大的情况,如果还用上面的方式上传的话就会等待的时间比较长,体验也会变得很差,这就要用到文件分片上传了。大概的原理就是利用File (继承自Blob对象)通过slice方法对文件进行切片,每一片作为一个file上传到服务器,前端全部上传完之后,再调用后端提供的一个合并文件的接口。就可以将大文件上传到服务器。
这其中涉及到文件分片、上传、后端合并等一系列问题。
前端文件分片
input type=“file”类型的可以通过 元素名.files 获取到是一个文件数组。这里是单文件上传。
循环切片file文件(代码每片200kb切分),
const uploadBigFile = async (formD) => {
const resp = await fetch('http://localhost:3000/file/upload/bigFile', {
method: 'POST',
body: formD
})
await resp.json()
}
const mergeBigFile = async (form) => {
const resp = await fetch('http://localhost:3000/file/upload/merge', {
method: 'POST',
body: form,
})
await resp.json()
}
btn3.onclick = function () {
const file = file3.files[0]
handleUploadFile(file)
}
const handleUploadFile = async (file) => {
const totalSize = file.size
const size = 200 * 1024 // * 1024
let start = 0
let num = 0
const blobArr = []
let end = null
// 统一生成随机值,在后端作为文件夹名称保存所有分片;
// 合并时也需要使用该随机值读取所有内容合并
const randomString = Math.random().toString(36).substring(7)
while (start <= totalSize) {
if (start + size >= totalSize) {
end = totalSize
} else {
end = start + size
}
const blob = file.slice(start, end)
const formD = new FormData()
blobArr.push(formD)
formD.append(`file`, blob)
formD.append('name', randomString)
// 合并时需要按顺序合并 index作为切分顺序
formD.append('index', num)
start += size
num++
}
// 所有分片都上传完成后调用合并接口
Promise.all(
blobArr.map(async (item) => await uploadBigFile(item))
).then(() => {
const form = new FormData()
form.append('name', randomString)
// 拿到文件后缀名称
form.append('ext', file.name.split('.')[1])
mergeBigFile(form).then((res) => {
console.log('上传完成')
})
})
}
后端上传文件
首先会查看有没有存该文件的文件夹
然后读取文件buffer,存储到相应文件夹
@Post('upload/bigFile')
@UseInterceptors(FileInterceptor('file'))
async uploadFile6(@UploadedFile() file: Express.Multer.File, @Body() body: any) {
return createDirectarySaveFile(file, body.name, body.index)
}
export const createDirectarySaveFile = (
file: Express.Multer.File,
filePath: string,
fileName: string,
)=> {
const newFilePath = path.join(root, '/uploads', filePath)
if (!fs.existsSync(newFilePath)) {
fs.mkdirSync(newFilePath, { recursive: true });
}
fs.writeFileSync(path.join(newFilePath, fileName), file.buffer);
return{
fileName: fileName,
fullPath: path.join(newFilePath, fileName)
}
};
切片文件合并
使用buffer.concat 合并所有切片;最终保存到相应文件夹中生成随机文件名+后缀。
@Post('upload/merge')
@UseInterceptors(FileInterceptor('file'))
async uploadFilMerge(@Body() body:any) {
return mergeFileDelDirectory(body.name, body.ext)
}
export const mergeFileDelDirectory = (dirName: string, lastname: string) => {
const path = require('path');
const root = process.cwd();
const randomString = Math.random().toString(36).split('.')[1];
const uploadsPath = path.join(root, '/uploads', dirName);
let files = fs.readdirSync(uploadsPath);
// 排序 后面按顺序合并
files = files.sort((a, b) => {
console.log(a, b)
return parseInt(a) - parseInt(b)
})
// 合并
let mergedFileBuffer = Buffer.from('');
for (const file of files) {
const filePath = path.join(uploadsPath, file);
const fileBuffer = fs.readFileSync(filePath);
mergedFileBuffer = Buffer.concat([mergedFileBuffer, fileBuffer]);
}
const mergedFileName = `merged_${dirName}.${lastname}`;
const mergedFilePath = path.join(uploadsPath, mergedFileName);
const savePath = path.join(root, '/uploads', `${randomString}.${lastname}`);
fs.writeFileSync(savePath, mergedFileBuffer);
for (const file of files) {
const filePath = path.join(uploadsPath, file);
fs.unlinkSync(filePath);
}
const finalFilePath = path.join(root, '/uploads');
fs.rmdirSync(uploadsPath);
return {
path: savePath,
size: mergedFileBuffer.length.toString(),
}
}