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

NestJS头像上传接口问题:无效文件仍被存储如何解决?

问题描述

我创建了一个用于上传头像的NestJS接口,控制器代码如下:

@Post('/avatar')
@UseInterceptors(
  FileInterceptor('file', {
    storage: diskStorage({
      destination: './public/avatars',
      filename: (req, file, cb) => {
        const { user } = req as unknown as { user: typeof ReqUser };
        const { id } = user as unknown as { id: string };

        const filename: string = id;
        const extension: string = path.parse(file.originalname).ext;
        cb(null, `${filename}${extension}`);
      },
    }),
  }),
)
async uploadAvatar(
  @UploadedFile(
    new ParseFilePipe({
      validators: [
        new MaxFileSizeValidator({ maxSize: 5000000 }),
        new FileTypeValidator({ fileType: /image\/(jpeg|png|jpg)/ }),
      ],
    }),
  )
  file: Express.Multer.File,
  @ReqUser() user,
) {
  return this.meService.setAvatar(file, user);
}

目前遇到的问题是:当文件不符合验证规则时,系统会抛出对应错误,但文件仍会被上传至服务器。例如,非.jpg、.png、.jpeg格式的文件依然会被存储,请问哪里操作有误?

解决方案

问题出在执行顺序:Multer的FileInterceptor会先完成文件的存储操作,之后才会触发ParseFilePipe的验证逻辑。因此验证不通过时,文件已经被写入服务器了。

有两种可靠的解决方式:

方式一:在Multer配置中提前拦截不符合规则的文件(推荐)

直接在FileInterceptor的配置里添加fileFilter和limits,在文件存储前就完成验证拦截,这样不符合规则的文件根本不会被保存到服务器。

修改后的代码:

import * as path from 'path';

// ...

@Post('/avatar')
@UseInterceptors(
  FileInterceptor('file', {
    storage: diskStorage({
      destination: './public/avatars',
      filename: (req, file, cb) => {
        const { user } = req as unknown as { user: typeof ReqUser };
        const { id } = user as unknown as { id: string };

        const filename: string = id;
        const extension: string = path.parse(file.originalname).ext;
        cb(null, `${filename}${extension}`);
      },
    }),
    // 新增:文件类型过滤
    fileFilter: (req, file, cb) => {
      const allowedMimeTypes = ['image/jpeg', 'image/png', 'image/jpg'];
      const allowedExtensions = ['.jpg', '.jpeg', '.png'];
      
      const isValidMimeType = allowedMimeTypes.includes(file.mimetype);
      const isValidExtension = allowedExtensions.includes(path.extname(file.originalname).toLowerCase());

      if (isValidMimeType && isValidExtension) {
        cb(null, true);
      } else {
        cb(new Error('仅支持JPEG、PNG格式的图片文件'), false);
      }
    },
    // 新增:文件大小限制
    limits: {
      fileSize: 5000000 // 5MB,和ParseFilePipe的限制保持一致
    }
  }),
)
async uploadAvatar(
  @UploadedFile()
  file: Express.Multer.File,
  @ReqUser() user,
) {
  return this.meService.setAvatar(file, user);
}

这种方式的好处是从源头拦截无效文件,避免不必要的磁盘IO操作。

方式二:在ParseFilePipe验证失败时删除已存储的文件

如果你希望保留ParseFilePipe的验证逻辑,可以通过自定义errorHandler在验证失败后删除已上传的文件:

修改后的@UploadedFile部分:

import * as fs from 'fs';

// ...

@UploadedFile(
  new ParseFilePipe({
    validators: [
      new MaxFileSizeValidator({ maxSize: 5000000 }),
      new FileTypeValidator({ fileType: /image\/(jpeg|png|jpg)/ }),
    ],
    errorHandler: (error, file) => {
      // 检查文件路径是否存在,存在则删除
      if (file?.path && fs.existsSync(file.path)) {
        fs.unlinkSync(file.path);
      }
      // 抛出原错误
      throw error;
    }
  }),
)
file: Express.Multer.File,

注意:这种方式依赖于文件已经被完整写入磁盘,若存储过程中出现异常可能无法触发删除,因此优先级低于方式一。

内容的提问来源于stack exchange,提问作者lumgaashi

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.06 09:15:18