跳转到内容

文件上传模块

基于 NestJS + Fastify Multipart 的文件上传方案,支持 Zod Schema 验证、文件安全检查。

安装配置

1. 注册 Fastify Multipart 插件

typescript
// main.ts
import fastifyMultipart from '@fastify/multipart'

async function bootstrap() {
  const app = await NestFactory.create<NestFastifyApplication>(
    AppModule,
    new FastifyAdapter()
  )
  await app.register(fastifyMultipart)
  await app.listen(3000)
}

2. 导入 MultipartModule

typescript
import { MultipartModule } from '@maxtan/nest-core'

@Module({
  imports: [
    MultipartModule.forRoot({
      maxFileSize: 20 * 1024 * 1024,  // 20MB(默认)
      maxFiles: 100,                   // 最大文件数(默认)
      timeout: 30000                   // 超时(默认 30s)
    })
  ]
})
export class AppModule {}

基础用法

typescript
import { Controller, Post } from '@nestjs/common'
import { Multipart, MultipartData, ParsedMultipart } from '@maxtan/nest-core'

@Controller('upload')
export class UploadController {
  @Post('file')
  @Multipart()
  async uploadFile(@MultipartData() data: ParsedMultipart) {
    const { fields, files } = data

    const file = files[0]
    console.log(file.filename)  // 文件名
    console.log(file.size)      // 大小(字节)
    console.log(file.mimetype)  // MIME 类型
    console.log(file.buffer)    // Buffer 内容

    return { success: true }
  }
}

使用 Zod Schema 验证表单字段

typescript
import { z } from 'zod'

const UploadSchema = z.object({
  title: z.string().min(1, '标题不能为空'),
  description: z.string().optional()
})

@Post('avatar')
@Multipart({ schema: UploadSchema, maxFileSize: 5 * 1024 * 1024 })
async uploadAvatar(@MultipartData() data: ParsedMultipart<z.infer<typeof UploadSchema>>) {
  // data.fields — 已验证的表单数据
  // data.files — 文件列表
  return { title: data.fields.title }
}

只获取文件或字段

typescript
// 只获取文件
@Post('files-only')
@Multipart()
async uploadFiles(@MultipartData('files') files: UploadedFile[]) {
  return files.map(f => ({ name: f.filename, size: f.size }))
}

// 只获取字段
@Post('fields-only')
@Multipart({ schema: CreateSchema })
async createWithFields(@MultipartData('fields') fields: Create) {
  return fields
}

文件类型安全

MIME 类型白名单

typescript
import { UPLOAD_CONFIG } from '@maxtan/nest-core'

// 只允许图片
@Post('image')
@Multipart({
  allowedMimeTypes: UPLOAD_CONFIG.ALLOWED_IMAGE_TYPES,
  maxFileSize: 5 * 1024 * 1024
})
async uploadImage(@MultipartData() data: ParsedMultipart) {}

// 只允许文档
@Post('doc')
@Multipart({
  allowedMimeTypes: UPLOAD_CONFIG.ALLOWED_DOCUMENT_TYPES,
  maxFiles: 3
})
async uploadDoc(@MultipartData() data: ParsedMultipart) {}

// 自定义
@Post('custom')
@Multipart({
  allowedMimeTypes: ['image/jpeg', 'application/pdf'],
  forbiddenExtensions: ['.exe', '.bat']
})
async uploadCustom(@MultipartData() data: ParsedMultipart) {}

内置常量

typescript
import { UPLOAD_CONFIG } from '@maxtan/nest-core'

UPLOAD_CONFIG.ALLOWED_IMAGE_TYPES
// ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml', 'image/bmp']

UPLOAD_CONFIG.ALLOWED_DOCUMENT_TYPES
// ['application/pdf', 'application/msword', ...]

UPLOAD_CONFIG.ALLOWED_ARCHIVE_TYPES
// ['application/zip', ...]

UPLOAD_CONFIG.FORBIDDEN_EXTENSIONS
// ['.exe', '.bat', '.cmd', '.sh', '.ps1', '.vbs', '.scr', '.dll', '.so', '.dylib', '.app']

类型定义

UploadedFile

typescript
interface UploadedFile {
  filename: string    // 原始文件名
  size: number        // 大小(字节)
  mimetype: string    // MIME 类型
  buffer: Buffer      // 文件内容
  fieldname: string   // 表单字段名
  encoding: string    // 编码类型
}

ParsedMultipart

typescript
interface ParsedMultipart<T = Record<string, any>> {
  fields: T               // 表单字段(验证后)
  files: UploadedFile[]   // 文件列表
}

MultipartPipeOptions

typescript
interface MultipartPipeOptions {
  maxFileSize?: number        // 最大文件大小(字节),默认 20MB
  maxFiles?: number           // 最大文件数量,默认 100
  timeout?: number            // 超时(毫秒),默认 30000
  allowedMimeTypes?: string[] // MIME 类型白名单
  forbiddenExtensions?: string[] // 扩展名黑名单
}

错误处理

模块自动抛出 BadRequestException

场景错误信息示例
文件大小超限文件 example.jpg 超过大小限制, 最大 20.00MB
文件数量超限最多只能上传 100 个文件
MIME 类型不允许不支持的文件类型: application/x-msdownload
扩展名禁止禁止上传的文件类型: .exe
Schema 验证失败title: 标题不能为空
上传超时文件上传超时, 最大 30 秒
非 multipart 请求请求必须为 multipart/form-data 格式

注意事项

  1. 必须先注册 @fastify/multipart 插件
  2. Buffer 模式:文件自动转为 Buffer,适合中小文件
  3. 内存限制:大文件建议设置合理的 maxFileSize
  4. 全局拦截器:只处理带 @Multipart() 装饰器的路由

最佳实践

  1. 始终配置 MIME 白名单:使用 allowedMimeTypes 限制上传文件类型,不要允许任意文件
  2. 合理设置大小限制:头像 5MB、文档 20MB、视频 500MB,按场景区分
  3. 表单字段必须 Zod 校验:通过 schema 选项传入 Zod Schema,确保字段安全
  4. 扩展名黑名单保留默认值UPLOAD_CONFIG.FORBIDDEN_EXTENSIONS 已包含常见危险扩展名

相关文档