使用Node.js+Multer将文件哈希设为文件名时文件为空的解决方法
问题解决:Multer上传文件用哈希命名导致文件为空的高效方案
你的问题核心在于:Node.js的可读流是单向消费的,当你在filename回调里用getHashFromStream读取流计算哈希时,流已经被耗尽,Multer后续无法再读取流内容写入文件。
由于Multer要求在开始写入文件前确定文件名,而哈希值必须等整个流读取完成才能生成,因此无法直接在filename回调里完成哈希计算并保留流供Multer使用。下面是最优的高效解决方案:
自定义Multer存储引擎(推荐)
通过自定义存储逻辑,在读取上传流的同时完成哈希计算和临时文件写入,流只被读取一次,避免二次IO开销,最后将临时文件重命名为哈希值。
实现代码
import { StorageEngine } from 'multer'; import * as fs from 'fs'; import * as path from 'path'; import * as crypto from 'crypto'; import { randomUUID } from 'crypto'; // 自定义存储引擎:同时计算哈希并写入文件 export const hashStorage: StorageEngine = { _handleFile: async (req, file, cb) => { const uploadDir = './uploads'; const tempDir = './uploads/temp'; // 确保目录存在 await Promise.all([ fs.promises.mkdir(uploadDir, { recursive: true }), fs.promises.mkdir(tempDir, { recursive: true }) ]); // 生成临时文件名 const tempFilename = randomUUID(); const tempPath = path.join(tempDir, tempFilename); const hash = crypto.createHash('md5'); const writeStream = fs.createWriteStream(tempPath); // 监听流的data事件,实时更新哈希 file.stream.on('data', (chunk) => hash.update(chunk)); // 将上传流导向写入流 file.stream.pipe(writeStream); // 写入完成后处理重命名 writeStream.on('finish', async () => { try { const fileHash = hash.digest('hex'); const finalPath = path.join(uploadDir, fileHash); // 重命名临时文件为哈希值 await fs.promises.rename(tempPath, finalPath); // 返回文件信息给Multer cb(null, { path: finalPath, filename: fileHash, size: (await fs.promises.stat(finalPath)).size, originalname: file.originalname }); } catch (err) { cb(err as Error); } }); // 处理错误 writeStream.on('error', (err) => cb(err)); file.stream.on('error', (err) => cb(err)); }, _removeFile: (req, file, cb) => { fs.unlink(file.path, cb); } };
在NestJS控制器中使用
import { Controller, Post, UseInterceptors, UploadedFile } from '@nestjs/common'; import { FileInterceptor } from '@nestjs/platform-express'; import { hashStorage } from './hash-storage'; @Controller('upload') export class UploadController { @Post() @UseInterceptors(FileInterceptor('file', { storage: hashStorage })) async uploadFile(@UploadedFile() file: Express.Multer.File) { return { filename: file.filename, originalName: file.originalname, size: file.size }; } }
小文件替代方案(内存存储)
如果你的上传文件都是小体积(比如几MB以内),可以直接用Multer的内存存储,将文件内容存在Buffer中,计算哈希后再写入磁盘:
import { Controller, Post, UseInterceptors, UploadedFile } from '@nestjs/common'; import { FileInterceptor } from '@nestjs/platform-express'; import * as multer from 'multer'; import * as fs from 'fs'; import * as path from 'path'; import * as crypto from 'crypto'; const memoryStorage = multer.memoryStorage(); @Controller('upload') export class UploadController { @Post() @UseInterceptors(FileInterceptor('file', { storage: memoryStorage })) async uploadFile(@UploadedFile() file: Express.Multer.File) { const uploadDir = './uploads'; await fs.promises.mkdir(uploadDir, { recursive: true }); // 直接用Buffer计算哈希 const fileHash = crypto.createHash('md5').update(file.buffer).digest('hex'); const finalPath = path.join(uploadDir, fileHash); await fs.promises.writeFile(finalPath, file.buffer); return { filename: fileHash, originalName: file.originalname, size: file.size }; } }
方案对比
- 自定义存储引擎:适合所有文件大小,仅读取一次流,IO效率最高,无内存压力。
- 内存存储:实现简单,但大文件会占用大量内存,可能导致内存溢出,仅适合小文件场景。
内容的提问来源于stack exchange,提问作者andymel
相关产品推荐
相关产品推荐

