nestjs 大文件上传

296 阅读2分钟

参考文章: Nestjs 实现大文件分片上传在本文中,我们将探讨如何使用NestJS框架来实现大文件分片上传,并且将文件分片数据存储 - 掘金

文件上传一共分为这几步

  • 获取大文件上传签名
  • 上传大文件分片
  • 获取大文件分片上传状态
  • 取消大文件上传
  • 合并大文件

获取大文件上传签名

该步骤是创建一个长传会话, 用来保存文件的一些基本信息

interface ILargeFileSession {
    // 总的分片数据
    chunkCount: number;
    // 文件hash
    hash: string;
    // 文件名称
    fileName: string;
    // 文件大小
    fileSize: number;
    // 当前上传文件的用户id
    userId: number;
    // 文件的保存路径
    folderPath: string;
    // 保存到数据库的文件id
    fileId: number;
}

其中 chunkCount ,hash, fileSize, 都是用来最后合并的时候校验的, 看文件是否上传出错。

保存文件到动态文件夹

考虑到文件是分片上传,所以需要用一个文件夹来存放这些分片。文件夹的路径设置规则我设置的是年月日+uuid, 这个根据业务进行设置。 要注意的点是MulterModule 怎么将文件放到对应的文件夹里面去。

在上面的会话信息中, 我保存了一个folderPath 的字段, 这就是要存放的文件夹路径。我使用的是redis保存信息, 所以inject了一个Redis的service, 然后在 destination 可以读取到request请求。 可以从请求体里面获取到会话id,然后从redis 里面查找存放路径。

MulterModule.registerAsync({
            inject: [AppRedisService],
            async useFactory(redisService: AppRedisService) {
                return {
                    storage: multer.diskStorage({
                        destination: async (req, file, cb) => {
                            console.log('req', req);
                            const { session } = req.body as FilePartUploadRequest;
                            const id = `${req.user.type}-${req.user.userId}`;
                            const redisKey = mergeRedisKey(LARGE_FILE, `${req.user.type}`, `${req.user.userId}`, session);
                            console.log("🚀 ~ destination: ~ id:", id)
                            const folder = await redisService.client.hget(redisKey, 'folderPath');
                            cb(null, folder);
                        },
                        filename: (req, file, cb) => {
                            const { partIndex } = req.body as FilePartUploadRequest;
                            cb(null, `${partIndex}`);
                        }
                    })
                };
            },
        
        }),

合并文件

主要代码如下

这里使用了stream 进行文件合并, 不需要一次性的将文件全部加载到内存里面。 然后每次写入的时候都会包装成一个Promise,这样可以避免递归和回调的写法。

for (let currentPartIndex = 0; currentPartIndex < files.length; currentPartIndex++) {
                const readStream = fs.createReadStream(path.join(folderPath, files[currentPartIndex]));

                // 使用 pipe 将读取的流写入目标文件
                await new Promise<void>((resolve, reject) => {
                    readStream.pipe(ws);
                    readStream.on('end', resolve);
                    readStream.on('error', reject);
                });
            }