You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 08:29:53