如何从NestJS向AWS S3上传文件(附可复用实现方案)
支持多云平台的NestJS文件上传服务(AWS S3实现)
我正在开发一套可复用的文件上传服务,目前已经完成了AWS S3的上传与文件获取功能。代码里针对cloudPlatform变量的判断是为后续扩展预留的——这个服务最终会支持AWS、GCP、Azure以及本地文件系统等多种存储平台。
核心功能
- 批量处理多字段文件上传,返回按字段分组的结构化结果
- 通过配置动态切换存储平台,当前已实现AWS S3适配
- 包含完整的错误处理逻辑,覆盖上传失败、文件不存在等场景
- 自动生成唯一文件名,避免存储冲突
实现代码
import { Injectable, Logger, NotFoundException, InternalServerErrorException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { S3 } from 'aws-sdk'; import * as moment from 'moment'; import { v4 as uuid } from 'uuid'; import { IAWS_S3 } from './interfaces/aws-s3.interface'; import { Response } from 'express'; @Injectable() export class FileService { /** * @variable cloudPlatform 指定文件存储的云平台 * @variable awsConfig AWS S3的配置参数 * @S3 初始化后的S3客户端实例 */ private logger = new Logger('FileService'); private cloudPlatform: string = this.configService.get('uploadFilesSettings').fileUploadPlatform; private awsConfig: IAWS_S3; private s3: S3; constructor( private readonly configService: ConfigService, ) { if ( this.cloudPlatform === 'AWS_S3' ) { this.awsConfig = this.configService.get<IAWS_S3>('aws'); this.s3 = new S3( this.awsConfig ); } } /** * 批量上传请求体中的文件 * @param files 请求体中的文件对象,结构为 { 字段名: 文件数组 } * @returns 按字段分组的上传结果,格式示例:{ avatar: ['a.jpg', 'b.png'], background: ['bg.gif'] } */ async uploadFiles<T, U>( files: T ): Promise<U> { let props = Object.keys(files); let arrPromisesPending = await this.getArrayToUpload<T>( props, files ); let arrPromisesDone = await Promise.all( arrPromisesPending.map( async( file: Express.Multer.File ) => { return { fieldname: file.fieldname, ...( await this.uploadFileTo( file, this.cloudPlatform )), } }) ) return await this.getUploadedFiles<U>( arrPromisesDone, props ); } /** * 将按字段分组的文件对象转换为扁平数组,用于Promise.all批量处理 * @param props 文件对象的所有字段名 * @param files 请求体中的文件对象 * @returns 扁平的文件对象数组 */ async getArrayToUpload<T>( props: string[], files: T ): Promise<Object[]> { let promises: Object[] = []; props.map(( prop: string ) => { files[prop].map(( file: Express.Multer.File ) => { promises.push( file ); }); }) return promises; } /** * 将单个文件上传到指定云平台 * @param file 待上传的文件对象 * @param cloudPlatform 目标存储平台 * @returns 云平台返回的上传结果 */ async uploadFileTo( file: Express.Multer.File, cloudPlatform: string ): Promise<any> { let type: string = file.originalname.split(".").pop(); let genericFilename: string = `${ uuid() }-${ moment().format().split('T')[0] }`; if ( cloudPlatform === 'AWS_S3' ) { try { return await this.s3.upload({ Bucket: this.awsConfig.bucketName, Key: `${ genericFilename }.${ type }`, Body: file.buffer, ContentType: file.mimetype, }).promise(); } catch ( error ) { this.logger.error( error ); throw new InternalServerErrorException('出现意外错误,请查看服务器日志'); } } this.logger.error('请设置环境变量 "FILE_UPLOAD_PLATFORM"'); throw new InternalServerErrorException('出现意外错误,请查看服务器日志'); } /** * 将批量上传的结果重新整理为按字段分组的结构 * @param arrPromisesDone 所有文件的上传结果数组 * @param props 文件对象的所有字段名 * @returns 按字段分组的上传结果,格式示例: * { * avatar: ['image1.png','image2.jpg'], * background: ['bg1.png','bg2.jpg'] * } */ async getUploadedFiles<T>( arrPromisesDone: Object[], props: string[] ): Promise<T> { let uploadedFiles: any = {}; props.map(( prop: string ) => { uploadedFiles[prop] = []; arrPromisesDone.map(( file: any ) => { if ( file.fieldname === prop ) { uploadedFiles[prop].push( file.key ); } }); }); return uploadedFiles; } /** * 从云平台获取指定文件并返回给客户端 * @param imageName 存储在云平台的文件名 * @param res Express的Response对象 * @returns 返回文件内容给客户端 */ async getFile( imageName: string, res: Response ) { const params = { Bucket: this.awsConfig.bucketName, Key: imageName }; if ( this.cloudPlatform === 'AWS_S3' ) { try { const data = await this.s3.getObject( params ).promise(); res.contentType( data.ContentType ); return res.send( data.Body ); } catch ( error ) { if ( error.statusCode === 404 ) throw new NotFoundException(`文件 ${ imageName } 不存在`); this.logger.error( error ); throw new InternalServerErrorException('出现意外错误,请查看服务器日志'); } } this.logger.error('请设置环境变量 "FILE_UPLOAD_PLATFORM"'); throw new InternalServerErrorException('出现意外错误,请查看服务器日志'); } }
内容的提问来源于stack exchange,提问作者Erick Cinco
相关产品推荐
相关产品推荐

