NestJS 系列教程十八文件上传与对象存储架构Multer S3/OSS 权限控制✨ 本篇目标本篇你将学会使用 Multer 在 NestJS 中实现单文件上传多文件上传上传前的校验策略类型 / 大小文件命名与目录规划企业级建议对象存储S3/OSS两种常用架构后端直传对象存储Server Upload前端直传Pre-signed URL / STS下载访问权限控制私有桶 临时 URL为了确保你复制就能跑本章提供本地存储版本可直接运行对象存储版本模板代码两套实现。 一、目录结构可运行最小闭环src/ ├── app.module.ts └── files/ ├── files.module.ts ├── files.controller.ts ├── files.service.ts └── dto/ └── upload.dto.ts 二、安装依赖NestJS 上传基于 Multernpmi nestjs/platform-express multer如果你用的是 Fastify那么需要额外适配本系列默认 Express。 三、FilesModulesrc/files/files.module.tsimport{Module}fromnestjs/common;import{FilesController}from./files.controller;import{FilesService}from./files.service;Module({controllers:[FilesController],providers:[FilesService],})exportclassFilesModule{}在app.module.ts注册import{Module}fromnestjs/common;import{FilesModule}from./files/files.module;Module({imports:[FilesModule],})exportclassAppModule{} 四、企业级文件命名与存储规划建议遵循目录按业务划分avatars/、attachments/、posts/文件名避免冲突时间戳 随机数 原始后缀不要信任前端传来的文件名上传后返回fileIdurlmimesize我们先实现本地存储版本。✅ 五、本地存储版本单文件上传可运行1FilesService生成文件名 返回访问 URLsrc/files/files.service.tsimport{Injectable}fromnestjs/common;import*aspathfrompath;import*asfsfromfs;Injectable()exportclassFilesService{privatereadonlyuploadRootpath.join(process.cwd(),uploads);ensureUploadDir(subDir:string){constdirpath.join(this.uploadRoot,subDir);if(!fs.existsSync(dir)){fs.mkdirSync(dir,{recursive:true});}returndir;}buildPublicPath(subDir:string,filename:string){// 这里返回一个“可访问路径”后面我们会在 main.ts 做静态托管return/static/${subDir}/${filename};}}2FilesController单文件上传接口src/files/files.controller.tsimport{Controller,Post,UseInterceptors,UploadedFile,BadRequestException,Query,}fromnestjs/common;import{FileInterceptor}fromnestjs/platform-express;import{diskStorage}frommulter;import*aspathfrompath;import{FilesService}from./files.service;functionsafeExt(originalName:string){returnpath.extname(originalName).toLowerCase();}functionrandomName(ext:string){constrMath.random().toString(16).slice(2);return${Date.now()}_${r}${ext};}Controller(files)exportclassFilesController{constructor(privatereadonlyfilesService:FilesService){}Post(upload)UseInterceptors(FileInterceptor(file,{storage:diskStorage({destination:(req,file,cb){// 支持按业务目录分类如 ?diravatarsconstdir(req.query.dirasstring)||misc;cb(null,path.join(process.cwd(),uploads,dir));},filename:(req,file,cb){constextsafeExt(file.originalname);cb(null,randomName(ext));},}),limits:{fileSize:5*1024*1024,// 5MB},fileFilter:(req,file,cb){// 简单示例只允许图片if(!file.mimetype.startsWith(image/)){returncb(newBadRequestException(仅允许上传图片文件),false);}cb(null,true);},}),)uploadSingle(UploadedFile()file:Express.Multer.File,Query(dir)dir?:string,){if(!file)thrownewBadRequestException(未上传文件);constsubDirdir||misc;consturl/static/${subDir}/${file.filename};return{fileId:file.filename,url,mime:file.mimetype,size:file.size,};}}注意上面 destination 里直接用uploads/dir但是正常使用的情况下需要确保目录存在此处为了简洁我先写成可读版本大家平时使用的时候要更严谨可以在destination里调用ensureUploadDir下面我会给你一版更稳妥的写法。3在 main.ts 托管静态文件让上传结果可访问src/main.tsimport{NestFactory}fromnestjs/core;import{AppModule}from./app.module;import*asexpressfromexpress;import*aspathfrompath;asyncfunctionbootstrap(){constappawaitNestFactory.create(AppModule);// 访问: /static/... 映射到项目根目录 uploadsapp.use(/static,express.static(path.join(process.cwd(),uploads)));awaitapp.listen(3000);}bootstrap();4测试用 Postman / Thunder ClientPOSThttp://localhost:3000/files/upload?diravatarsform-datakeyfilevalue选择一张图片返回{fileId:1719999999_abcd.png,url:/static/avatars/1719999999_abcd.png,mime:image/png,size:12345}浏览器访问http://localhost:3000/static/avatars/1719999999_abcd.png✅ 六、更严谨确保目录存在建议大家用这版把 storage.destination 改成destination:(req,file,cb){constdir(req.query.dirasstring)||misc;constfullDirthis.filesService.ensureUploadDir(dir);cb(null,fullDir);},这需要你把拦截器配置从装饰器里抽出来因为this用不到。企业级建议写成multer.config.ts或在构造函数里创建 options。我后面第 22 章会给最终模板目录。 七、多文件上传一次传多张在 Controller 增加接口import{FilesInterceptor}fromnestjs/platform-express;Post(upload-many)UseInterceptors(FilesInterceptor(files,10,{storage:diskStorage({destination:(req,file,cb){constdir(req.query.dirasstring)||misc;cb(null,path.join(process.cwd(),uploads,dir));},filename:(req,file,cb){constextsafeExt(file.originalname);cb(null,randomName(ext));},}),limits:{fileSize:5*1024*1024},}),)uploadMany(UploadedFiles()files:Express.Multer.File[],Query(dir)dir?:string){constsubDirdir||misc;returnfiles.map((f)({fileId:f.filename,url:/static/${subDir}/${f.filename},mime:f.mimetype,size:f.size,}));}☁️ 八、对象存储架构S3/OSS两种主流模式模式 A后端直传对象存储Server Upload流程前端上传到后端后端把文件上传到 S3/OSS后端返回最终文件 URL优点权限控制简单统一审计、统一校验缺点后端带宽压力大大文件不划算模式 B前端直传Pre-signed URL / STS流程推荐前端请求后端我要上传一个文件告诉文件名/类型后端生成临时上传凭证签名 URL 或 STS前端直接 PUT 到对象存储上传完成后回调后端登记文件信息优点后端压力最小大文件上传性能极佳缺点实现稍复杂需要更好的权限策略 九、私有资源访问控制临时下载 URL生产常用如果你用私有桶文件默认不可公开访问后端生成临时下载 URL5分钟有效前端拿到 URL 后直接下载你可以在FilesController里提供GET /files/presign-uploadGET /files/presign-download这部分因为涉及具体云厂商AWS S3 / 阿里 OSS / 腾讯 COS所以需要根据具体情况具体调整我会在第 22 章模板里给出抽象接口StorageProvider。✅ 十、本章小结本篇我们完成了NestJS 使用 Multer 实现单文件、多文件上传文件校验类型、大小企业级文件命名与目录规划建议静态托管访问本地版可运行对象存储两种架构后端直传 / 前端直传私有资源访问控制的设计思路临时 URL