NestJS - How to upload image to aws s3?
Asked Answered
A

3

18

I'm trying to perform an image upload to aws s3 using multer-s3 on NestJS API. I have also tried aws-sdk. I use FileInterceptor and UploadedFile decorator to capture the file request. So far what I have is:

// Controller
 @Post()
 @UseInterceptors(FileInterceptor('file', multerOptions))
    uploadImage(@UploadedFile() file) {
        console.log(file);
    }

// multerOptions. multer.ts file
const configService = new ConfigService();

export const accessParams = {
    accessKeyId: configService.get('AWS_ACCESS_KEY_ID'),
    secretAccessKey: configService.get('AWS_SECRET_ACCESS_KEY'),
    region: configService.get('AWS_REGION'),
};

const imageMimeTypes = [
    'image/jpg',
    'image/jpeg',
    'image/png',
    'image/bmp',
];

AWS.config.update(accessParams);
export const s3 = new AWS.S3();

export const multerOptions = {
    fileFilter: (req: any, file: any, cb: any) => {
        const mimeType = imageMimeTypes.find(im => im === file.mimetype);

        if (mimeType) {
            cb(null, true);
        } else {
            cb(new HttpException(`Unsupported file type ${extname(file.originalname)}`, HttpStatus.BAD_REQUEST), false);
        }
    },
    storage: multerS3({
        s3: s3,
        bucket: configService.get('S3_BUCKET_NAME'),
        acl: 'read-public',
        metadata: function (req, file, cb) {
            cb(null, { fieldName: file.fieldname })
        },
        key: (req: any, file: any, cb: any) => {
            cb(null, `${Date.now().toString()}/${file.originalname}`);
        },
        contentType: multerS3.AUTO_CONTENT_TYPE
    }),
};

which gives me the following error:

{
  "message": null,
  "code": "InvalidArgument",
  "region": null,
  "time": "2020-04-24T05:34:19.009Z",
  "requestId": "DH224C558HTDF8E3",
  "extendedRequestId": "JKHKJH6877-LKJALDNC765llLKAL=",
  "statusCode": 400,
  "retryable": false,
  "retryDelay": 6.790294010827713,
  "storageErrors": []
}

Any idea? Thank you.

Aggi answered 24/4, 2020 at 5:55 Comment(0)
A
51

You could create a controller like

import { Post, UseInterceptors, UploadedFile } from '@nestjs/common';

@Post('upload')
@UseInterceptors(FileInterceptor('file'))
async upload(@UploadedFile() file) {
  return await this.service.upload(file);
}

Your service should look like

import { S3 } from 'aws-sdk';
import { Logger, Injectable } from '@nestjs/common';

@Injectable()
export class FileUploadService {
    async upload(file) {
        const { originalname } = file;
        const bucketS3 = 'my-aws-bucket';
        await this.uploadS3(file.buffer, bucketS3, originalname);
    }

    async uploadS3(file, bucket, name) {
        const s3 = this.getS3();
        const params = {
            Bucket: bucket,
            Key: String(name),
            Body: file,
        };
        return new Promise((resolve, reject) => {
            s3.upload(params, (err, data) => {
            if (err) {
                Logger.error(err);
                reject(err.message);
            }
            resolve(data);
            });
        });
    }

    getS3() {
        return new S3({
            accessKeyId: process.env.AWS_ACCESS_KEY_ID,
            secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
        });
    }
}
Anatola answered 25/4, 2020 at 21:6 Comment(4)
Thanks for the help, I am going to vote for your answer because it is correct but finally I was able to solve it using multer-s3 a little more to my likingAggi
In the latest version when @UseInterceptors(FilesInterceptor("files")) is given it is showing Error: connect ECONNREFUSED 127.0.0.1:4001 if interceptor line is commented code get inside controller without files and error. Any idea on this for a solution ?Akbar
is there a way to define access to public for uploaded files?Rely
Thanks a lot! i spend 4 days in this task! you r js god!Winzler
Y
7

With typings and stream:

import { ReadStream } from 'fs';
import * as AWS from 'aws-sdk';
import { PromiseResult } from 'aws-sdk/lib/request';
import { Injectable, InternalServerErrorException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';

import { TConfig, TStorageConfig } from '../../config';

@Injectable()
export class StorageService {
    private S3: AWS.S3;
    private BUCKET: string;

    constructor(private configService: ConfigService<TConfig>) {
        this.S3 = new AWS.S3({
            // Your config options
            accessKeyId: this.configService.get<TStorageConfig>('storage').accessKeyId,
            secretAccessKey: this.configService.get<TStorageConfig>('storage').secretAccessKey,
            endpoint: this.configService.get<TStorageConfig>('storage').endpoint,
            s3ForcePathStyle: true,
            signatureVersion: 'v4',
        });
        this.BUCKET = this.configService.get<TStorageConfig>('storage').bucket;
    }

    async getBlob(key: string): Promise<PromiseResult<AWS.S3.GetObjectOutput, AWS.AWSError>> {
        const params = { Bucket: this.BUCKET, Key: key };
        const blob = await this.S3.getObject(params).promise();
        
        return blob;
    }

    async putBlob(blobName: string, blob: Buffer): Promise<PromiseResult<AWS.S3.PutObjectOutput, AWS.AWSError>> {
        const params = { Bucket: this.BUCKET, Key: blobName, Body: blob };
        const uploadedBlob = await this.S3.putObject(params).promise();

        return uploadedBlob;
    }

    // to get stream you can use file.createReadStream()
    async putStream(key: string, stream: ReadStream): Promise<AWS.S3.PutObjectOutput> {
        const file = await new Promise<AWS.S3.PutObjectOutput>((resolve, reject) => {
            const handleError = (error) => {
                reject(error);
            };
            const chunks: Buffer[] = [];

            stream.on('data', (chunk: Buffer) => {
                chunks.push(chunk);
            });

            stream.once('end', async () => {
                const fileBuffer = Buffer.concat(chunks);

                try {
                    const uploaded = await this.putBlob(key, fileBuffer);

                    resolve(uploaded);
                } catch (error) {
                    handleError(new InternalServerErrorException(error));
                }
            });

            stream.on('error', (error) => handleError(new InternalServerErrorException(error)));
        });

        return file;
    }
}
Yulma answered 25/1, 2022 at 10:9 Comment(0)
C
2

Create a class called AWSS3Utils as the following:

import * as multerS3 from 'multer-s3-transform'
import * as sharp from 'sharp'
import * as AWS from 'aws-sdk' 
export default class AWSS3Utils {


 static uploadFile(bucket:string,transform:boolean,acl:string)
  {
    return multerS3({
      s3: new AWS.S3({
        accessKeyId: 'accessKeyId'  ,
        secretAccessKey: 'secretAccessKey',
      }),
      bucket: bucket,
      shouldTransform: true,
      acl: acl,
      transforms: [
        {
          id: 'original',
          key: function (req, file, cb) {
            cb(null, `${file.originalname}` )
          },
          transform: function (req, file, cb) {
            cb(null, sharp().png())
          }
        },
        {
          id: 'large',
          key: (req, file, cb) => cb(null, new Date().getTime() + `_large_${file.originalname}`),
          transform: (req, file, cb) => cb(null, sharp().resize(1200, 900).png())
        },
        {
          id: 'small',
          key: (req, file, cb) => cb(null, new Date().getTime() + `_small_${file.originalname}`),
          transform: (req, file, cb) => cb(null, sharp().resize(400, 300).png())
        }
      ]
    })
  }
}

Now you are ready to integrate it with Nest FileInterceptor

 @Post('upload')
  @UseInterceptors(FileInterceptor('file', {  storage: AWSS3Utils.uploadFile('nest-upload-tutorial',true,'public-read') }))
  uploadFile(
    @UploadedFile(
      new ParseFilePipe({
        validators: [new FileTypeValidator({ fileType: 'png' })],
      }),
    )
    file: Express.Multer.File,
  ) {
    return file
  }

don't miss to create a bucket with allow ACL permission otherwise remove the acl flag from uploadSingle function

Coheman answered 21/9, 2022 at 21:29 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.