Skip to content

文件上传拦截器使用文档

概述

MulterFileInterceptor 是一个基于 NestJS 和 Multer 的文件上传拦截器,提供了灵活的文件上传功能。它支持文件格式验证、文件大小限制、自动目录管理等功能,能够帮助开发者快速实现安全可靠的文件上传接口。

基础使用

在控制器中使用

typescript
import { Controller, Post, Body } from '@nestjs/common'
import { MulterFileInterceptor } from '@/common/interceptor/file.interceptor'
import { UploadedFile, UseInterceptors } from '@nestjs/common'

@Controller('upload')
export class UploadController {
  // 基本用法:上传图片,最大 5MB
  @Post('image')
  @MulterFileInterceptor('images', ['jpg', 'png', 'gif'], 5 * 1024 * 1024)
  async uploadImage(@UploadedFile() file: Express.Multer.File) {
    return {
      code: 200,
      message: '上传成功',
      data: {
        filename: file.filename,
        originalname: file.originalname,
        size: file.size,
        mimetype: file.mimetype,
        path: `/upload/${file.filename}`,
      },
    }
  }

  // 上传文档,最大 10MB
  @Post('document')
  @MulterFileInterceptor('docs', ['pdf', 'doc', 'docx'], 10 * 1024 * 1024)
  async uploadDocument(@UploadedFile() file: Express.Multer.File) {
    return {
      code: 200,
      data: {
        path: `/upload/${file.filename}`,
      },
    }
  }

  // 允许所有格式,默认 20MB
  @Post('any')
  @MulterFileInterceptor()
  async uploadAnyFile(@UploadedFile() file: Express.Multer.File) {
    return {
      code: 200,
      data: {
        path: `/upload/${file.filename}`,
      },
    }
  }
}

参数说明

MulterFileInterceptor(subDir?, formats?, fileMaxSize?)

参数类型默认值说明
subDirstring''子目录名称,可从请求体 module 字段获取
formatsstring[]['*']允许的文件扩展名数组,['*'] 表示不限制
fileMaxSizenumber20MB文件大小限制(字节)

API 参考

核心功能

1. 文件格式验证

拦截器会自动验证上传文件的格式是否符合要求:

typescript
// 只允许图片格式
@MulterFileInterceptor('', ['jpg', 'jpeg', 'png', 'gif', 'webp'])

// 允许多种文档格式
@MulterFileInterceptor('', ['pdf', 'doc', 'docx', 'xls', 'xlsx'])

// 不限制格式
@MulterFileInterceptor('', ['*'])

错误处理:

当文件格式不符合要求时,会返回错误:

json
{
  "statusCode": 400,
  "message": "文件格式不支持",
  "error": "Bad Request"
}

2. 文件大小限制

默认限制为 20MB,可根据需要自定义:

typescript
// 限制为 5MB
@MulterFileInterceptor('', ['*'], 5 * 1024 * 1024)

// 限制为 50MB
@MulterFileInterceptor('', ['*'], 50 * 1024 * 1024)

// 限制为 100KB
@MulterFileInterceptor('', ['*'], 100 * 1024)

3. 自动目录管理

文件会按照日期和业务模块自动分类存储:

目录结构:

upload/
├── default/           # 默认模块
│   ├── 2024-01-15/
│   │   └── 1705305600000-123456789.jpg
│   └── 2024-01-16/
│       └── 1705392000000-987654321.png
├── avatar/            # 头像模块
│   └── 2024-01-15/
│       └── 1705305600000-111222333.jpg
└── article/           # 文章模块
    └── 2024-01-15/
        └── 1705305600000-444555666.png

目录规则:

  • 一级目录:subDir 参数或请求体中的 module 字段,默认为 default
  • 二级目录:按日期分组 YYYY-MM-DD
  • 文件名:时间戳-随机数.扩展名

指定子目录:

typescript
// 方式一:通过参数指定
@MulterFileInterceptor('avatar')

// 方式二:通过请求体指定
@Post('upload')
@MulterFileInterceptor()
async upload(@Body() body: { module: string }, @UploadedFile() file) {
  // body.module = 'avatar' 会创建 upload/avatar/2024-01-15/ 目录
}

4. 文件命名策略

生成的文件名格式:{目录}/{时间戳}-{随机数}.{扩展名}

示例:

  • avatar/2024-01-15/1705305600000-123456789.jpg
  • article/2024-01-16/1705392000000-987654321.png

优势:

  • ✅ 避免文件名冲突
  • ✅ 按日期归档,便于管理
  • ✅ 防止中文文件名导致的编码问题
  • ✅ 保留原始文件扩展名

实际应用场景

1. 用户头像上传

typescript
import { Controller, Post, UploadedFile, Body } from '@nestjs/common'
import { MulterFileInterceptor } from '@/common/interceptor/file.interceptor'

@Controller('user')
export class UserController {
  @Post('avatar')
  @MulterFileInterceptor('avatar', ['jpg', 'jpeg', 'png'], 2 * 1024 * 1024)
  async uploadAvatar(@UploadedFile() file: Express.Multer.File, @Body('userId') userId: number) {
    // 更新用户头像
    await this.userService.updateAvatar(userId, `/upload/${file.filename}`)

    return {
      code: 200,
      message: '头像上传成功',
      data: {
        avatarUrl: `/upload/${file.filename}`,
      },
    }
  }
}

前端调用:

javascript
const formData = new FormData()
formData.append('file', avatarFile)
formData.append('userId', 123)

const response = await fetch('/api/user/avatar', {
  method: 'POST',
  body: formData,
})

2. 文章图片上传

typescript
@Controller('article')
export class ArticleController {
  @Post('image')
  @MulterFileInterceptor('article', ['jpg', 'jpeg', 'png', 'gif', 'webp'], 5 * 1024 * 1024)
  async uploadArticleImage(@UploadedFile() file: Express.Multer.File) {
    return {
      code: 200,
      data: {
        url: `/upload/${file.filename}`,
        filename: file.originalname,
        size: file.size,
      },
    }
  }
}

3. 多类型文件上传

typescript
@Controller('resource')
export class ResourceController {
  // 上传视频
  @Post('video')
  @MulterFileInterceptor('video', ['mp4', 'avi', 'mov'], 100 * 1024 * 1024)
  async uploadVideo(@UploadedFile() file: Express.Multer.File) {
    return {
      code: 200,
      data: {
        url: `/upload/${file.filename}`,
        duration: await this.getVideoDuration(file.path),
      },
    }
  }

  // 上传音频
  @Post('audio')
  @MulterFileInterceptor('audio', ['mp3', 'wav', 'ogg'], 20 * 1024 * 1024)
  async uploadAudio(@UploadedFile() file: Express.Multer.File) {
    return {
      code: 200,
      data: {
        url: `/upload/${file.filename}`,
      },
    }
  }

  // 上传压缩包
  @Post('archive')
  @MulterFileInterceptor('archive', ['zip', 'rar', '7z'], 50 * 1024 * 1024)
  async uploadArchive(@UploadedFile() file: Express.Multer.File) {
    return {
      code: 200,
      data: {
        url: `/upload/${file.filename}`,
      },
    }
  }
}

4. 通用文件上传(带业务逻辑)

typescript
import { Injectable } from '@nestjs/common'
import { InjectRepository } from '@nestjs/typeorm'
import { Repository } from 'typeorm'
import { Attachment } from './entities/attachment.entity'

@Injectable()
export class AttachmentService {
  constructor(
    @InjectRepository(Attachment)
    private attachmentRepo: Repository<Attachment>,
  ) {}

  async saveAttachment(fileInfo: any, userId: number) {
    const attachment = this.attachmentRepo.create({
      filename: fileInfo.filename,
      originalName: fileInfo.originalname,
      size: fileInfo.size,
      mimetype: fileInfo.mimetype,
      path: `/upload/${fileInfo.filename}`,
      userId,
      createdAt: new Date(),
    })

    return await this.attachmentRepo.save(attachment)
  }
}

@Controller('attachment')
export class AttachmentController {
  constructor(private attachmentService: AttachmentService) {}

  @Post()
  @MulterFileInterceptor()
  async upload(@UploadedFile() file: Express.Multer.File, @Body('userId') userId: number) {
    const attachment = await this.attachmentService.saveAttachment(file, userId)

    return {
      code: 200,
      message: '上传成功',
      data: attachment,
    }
  }
}

5. 批量上传(需要修改拦截器支持)

注意: 当前拦截器仅支持单文件上传,如需批量上传可使用 AnyFilesInterceptor

高级配置

1. 自定义文件过滤器

如果需要更复杂的文件验证逻辑,可以扩展拦截器:

typescript
import { UseInterceptors } from '@nestjs/common'
import { FileInterceptor } from '@nestjs/platform-express'
import { diskStorage } from 'multer'
import { extname, join } from 'path'
import dayjs from 'dayjs'
import { mkdir } from 'node:fs/promises'

export function AdvancedFileInterceptor(options: {
  subDir?: string
  formats?: string[]
  maxSize?: number
  minSize?: number
  checkContent?: boolean
}) {
  return UseInterceptors(
    FileInterceptor('file', {
      limits: {
        fileSize: options.maxSize || 20 * 1024 * 1024,
      },
      fileFilter(req, file, callback) {
        // 检查文件扩展名
        const ext = file.originalname.split('.').at(-1)
        if (options.formats && options.formats[0] !== '*' && !options.formats.includes(ext)) {
          callback(new Error('文件格式不支持'), false)
          return
        }

        // 检查 MIME 类型
        if (!file.mimetype.startsWith('image/') && options.formats?.some((f) => ['jpg', 'png'].includes(f))) {
          callback(new Error('只能上传图片文件'), false)
          return
        }

        // 检查文件大小
        if (file.size > (options.maxSize || 20 * 1024 * 1024)) {
          callback(new Error(`文件大小不能超过 ${options.maxSize / 1024 / 1024}MB`), false)
          return
        }

        // 检查最小文件大小
        if (options.minSize && file.size < options.minSize) {
          callback(new Error(`文件大小不能小于 ${options.minSize / 1024}KB`), false)
          return
        }

        callback(null, true)
      },
      storage: diskStorage({
        destination: async (req, file, cb) => {
          const dirGroup = dayjs().format('YYYY-MM-DD')
          const fileDir = join(options.subDir || req.body.module || 'default', dirGroup)

          try {
            await mkdir(join('upload', fileDir), { recursive: true })
            cb(null, join('upload', fileDir))
          } catch (error) {
            cb(error, null)
          }
        },
        filename: (req, file, cb) => {
          const ext = extname(file.originalname)
          const filename = `${Date.now()}-${Math.round(Math.random() * 1e9)}${ext}`
          cb(null, filename)
        },
      }),
    }),
  )
}

2. 集成云存储(如 OSS)

typescript
import { Injectable } from '@nestjs/common'
import * as OSS from 'ali-oss'

@Injectable()
export class OssService {
  private client: OSS

  constructor() {
    this.client = new OSS({
      region: 'oss-cn-hangzhou',
      accessKeyId: process.env.OSS_ACCESS_KEY_ID,
      accessKeySecret: process.env.OSS_ACCESS_KEY_SECRET,
      bucket: 'my-bucket',
    })
  }

  async uploadToOss(file: Express.Multer.File, path: string) {
    const result = await this.client.put(path, file.buffer)
    return result.url
  }
}

@Controller('oss-upload')
export class OssUploadController {
  constructor(private ossService: OssService) {}

  @Post()
  @MulterFileInterceptor()
  async upload(@UploadedFile() file: Express.Multer.File) {
    const ossPath = `uploads/${file.filename}`
    const url = await this.ossService.uploadToOss(file, ossPath)

    return {
      code: 200,
      data: {
        url,
        ossPath,
      },
    }
  }
}

3. 文件上传进度追踪

前端实现上传进度显示:

javascript
const xhr = new XMLHttpRequest()
const formData = new FormData()
formData.append('file', file)

xhr.open('POST', '/api/upload/image')

// 监听上传进度
xhr.upload.addEventListener('progress', (event) => {
  if (event.lengthComputable) {
    const percentComplete = (event.loaded / event.total) * 100
    console.log(`上传进度: ${percentComplete.toFixed(2)}%`)
  }
})

xhr.onload = () => {
  if (xhr.status === 200) {
    const response = JSON.parse(xhr.responseText)
    console.log('上传成功:', response)
  }
}

xhr.onerror = () => {
  console.error('上传失败')
}

xhr.send(formData)

注意事项

1. 静态资源服务配置

确保在 main.ts 中配置静态文件服务:

typescript
import { NestFactory } from '@nestjs/core'
import { AppModule } from './app.module'
import { NestExpressApplication } from '@nestjs/platform-express'
import { join } from 'path'

async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule)

  // 配置静态文件服务
  app.useStaticAssets(join(__dirname, '..', 'upload'), {
    prefix: '/upload/',
  })

  await app.listen(3000)
}
bootstrap()

2. 安全性考虑

防止恶意文件上传

typescript
// 建议的验证策略
@MulterFileInterceptor('secure', ['jpg', 'png'], 5 * 1024 * 1024)

安全措施:

  • ✅ 严格限制文件类型
  • ✅ 设置合理的文件大小限制
  • ✅ 使用随机文件名,避免路径遍历攻击
  • ✅ 不要直接使用用户上传的原始文件名
  • ✅ 对图片文件进行内容验证(检查是否为真实图片)

图片内容验证示例

typescript
import * as sharp from 'sharp'

async function validateImage(filePath: string): Promise<boolean> {
  try {
    const metadata = await sharp(filePath).metadata()
    // 验证是否为有效图片
    return !!metadata.width && !!metadata.height
  } catch (error) {
    return false
  }
}

3. 性能优化

大文件处理

对于大文件上传,建议:

typescript
// 增加超时时间
@UseInterceptors(TimeoutInterceptor(60000)) // 60秒超时

// 使用流式处理
import { createWriteStream } from 'fs'

const writeStream = createWriteStream(filePath)
file.stream.pipe(writeStream)

并发控制

typescript
// 使用限流中间件
import { Throttle } from '@nestjs/throttler'

@Post('upload')
@Throttle({ default: { limit: 10, ttl: 60000 } }) // 每分钟最多10次
@MulterFileInterceptor()
async upload(@UploadedFile() file) {
  // ...
}

4. 错误处理

全局异常过滤器中处理文件上传错误:

typescript
import { ExceptionFilter, Catch, ArgumentsHost } from '@nestjs/common'
import { MulterError } from 'multer'

@Catch(MulterError)
export class MulterExceptionFilter implements ExceptionFilter {
  catch(exception: MulterError, host: ArgumentsHost) {
    const ctx = host.switchToHttp()
    const response = ctx.getResponse()

    let message = '文件上传失败'

    switch (exception.code) {
      case 'LIMIT_FILE_SIZE':
        message = '文件大小超出限制'
        break
      case 'LIMIT_FILE_COUNT':
        message = '文件数量超出限制'
        break
      case 'LIMIT_UNEXPECTED_FILE':
        message = '意外的文件字段'
        break
    }

    response.status(400).json({
      statusCode: 400,
      message,
      error: exception.message,
    })
  }
}

5. 目录权限

确保 upload 目录有正确的读写权限:

bash
# Linux/Mac
mkdir -p upload
chmod 755 upload

# 或在代码中自动创建(已实现)
await mkdir(join('upload', fileDir), { recursive: true })

6. 文件清理策略

定期清理过期文件:

typescript
import { Cron, CronExpression } from '@nestjs/schedule'
import { readdir, stat, unlink } from 'fs/promises'
import { join } from 'path'

@Injectable()
export class FileCleanupService {
  @Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT) // 每天午夜执行
  async cleanupOldFiles() {
    const uploadDir = join(process.cwd(), 'upload')
    const thirtyDaysAgo = Date.now() - 30 * 24 * 60 * 60 * 1000

    // 递归清理30天前的文件
    await this.cleanDirectory(uploadDir, thirtyDaysAgo)
  }

  private async cleanDirectory(dir: string, cutoffTime: number) {
    const files = await readdir(dir, { withFileTypes: true })

    for (const file of files) {
      const fullPath = join(dir, file.name)

      if (file.isDirectory()) {
        await this.cleanDirectory(fullPath, cutoffTime)
      } else {
        const stats = await stat(fullPath)
        if (stats.mtimeMs < cutoffTime) {
          await unlink(fullPath)
          console.log(`Deleted old file: ${fullPath}`)
        }
      }
    }
  }
}

常见问题

Q1: 如何获取上传文件的完整路径?

typescript
@Post('upload')
@MulterFileInterceptor()
async upload(@UploadedFile() file: Express.Multer.File) {
  // 相对路径
  const relativePath = `/upload/${file.filename}`

  // 绝对路径
  const absolutePath = join(process.cwd(), 'upload', file.filename)

  return { relativePath, absolutePath }
}

Q2: 如何同时上传多个文件?

当前拦截器仅支持单文件。如需多文件上传,可以使用 AnyFilesInterceptor

typescript
import { AnyFilesInterceptor } from '@nestjs/platform-express'

@Post('multiple')
@UseInterceptors(AnyFilesInterceptor())
async uploadMultiple(@UploadedFiles() files: Express.Multer.File[]) {
  const results = files.map(file => ({
    filename: file.filename,
    originalname: file.originalname,
  }))

  return {
    code: 200,
    data: results,
  }
}

Q3: 如何在上传前验证文件?

可以在 Service 层进行额外验证:

typescript
@Injectable()
export class UploadService {
  async validateAndSave(file: Express.Multer.File) {
    // 1. 检查文件是否存在
    if (!existsSync(file.path)) {
      throw new Error('文件不存在')
    }

    // 2. 验证文件内容(如图片)
    if (file.mimetype.startsWith('image/')) {
      const isValid = await this.validateImage(file.path)
      if (!isValid) {
        throw new Error('无效的图片文件')
      }
    }

    // 3. 保存记录到数据库
    return await this.saveToFileRecord(file)
  }
}

Q4: 如何支持断点续传?

断点续传需要更复杂的实现,建议使用专门的库或服务:

typescript
// 推荐使用 tus-node-server 或其他专业方案
import { Server } from 'tus-node-server'

const server = new Server({
  path: '/files',
  datastore: new FileStore({ directory: './uploads' }),
})

Q5: 上传的文件如何访问?

配置静态资源服务后,可以通过 URL 直接访问:

http://localhost:3000/upload/avatar/2024-01-15/1705305600000-123456789.jpg

总结

MulterFileInterceptor 提供了一个简单易用的文件上传解决方案,具有以下特点:

核心优势:

  • ✅ 灵活的格式验证和大小限制
  • ✅ 自动化的目录管理和文件命名
  • ✅ 支持自定义子目录和业务模块分类
  • ✅ 良好的扩展性和可配置性

最佳实践:

  • 📌 根据业务需求合理设置文件大小限制
  • 📌 严格验证文件类型,避免安全风险
  • 📌 定期清理过期文件,释放存储空间
  • 📌 生产环境建议使用云存储服务(OSS、S3等)
  • 📌 做好错误处理和日志记录

适用场景:

  • 用户头像上传
  • 文章配图上传
  • 文档附件上传
  • 多媒体资源管理
  • 临时文件存储

通过合理使用该拦截器,可以快速构建安全、高效的文件上传功能。