nest 文件上传

268 阅读4分钟

初始化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

KeyDescription
destor storageWhere to store the files 文件存储位置
fileFilterFunction to control which files are accepted
limitsLimits of the uploaded data
preservePathKeep 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(),
  }
}

源码地址