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

如何从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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.17 22:55:18