How to successfully upload an image to AWS S3 using NestJS?

I'm currently working on implementing an image upload feature to AWS S3 using multer-s3 within my NestJS API. I have experimented with aws-sdk as well. I am utilizing FileInterceptor and UploadedFile decorator to handle the file request. This is what I have achieved so far:

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

// multerOptions in 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
    }),
};

However, this implementation results in the following error being triggered:

{
  "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": []
}

Would appreciate any insights or suggestions to resolve this issue. Thank you.

Answer №1

To implement file uploading functionality in NestJS, you can start by creating a controller:

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

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

Your FileUploadService should be structured as follows:

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.uploadToS3(file.buffer, bucketS3, originalname);
    }

    async uploadToS3(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,
        });
    }
}

Answer №2

Utilizing typings and stream functionality:

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 CloudStorageService {
    private S3: AWS.S3;
    private BUCKET: string;

    constructor(private configService: ConfigService<TConfig>) {
        this.S3 = new AWS.S3({
            // Customize your configuration options here
            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 upload a stream, you can utilize 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;
    }
}

Answer №3

To start, develop a class named AWSS3Utils with the following structure:

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 can seamlessly 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
  }

Ensure to create a bucket with proper ACL permissions or omit the acl flag from the uploadSingle function.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Utilizing Dependency Injection with TypeScript and Angular 5 in an Abstract Class

In my BaseComponent, I have some dependencies injected. The first dependency, EntityService, is essential and correctly implemented. However, the AnyOtherService is only utilized within the abstract BaseComponent. Instead of injecting it into the ChildCom ...

Can you explain the significance of declaring messages as a string array in an Angular class?

As a beginner in Angular and JavaScript, I am struggling to understand the significance of this particular statement. Can someone please explain its meaning? messages: string[] = []; ...

Retrieving the chosen option using a change event listener

Is there a way to retrieve the selected option using a change listener in TypeScript? I have come across JavaScript examples where the value is retrieved through event., but I am unable to locate any field that contains the selected option. <!DOCTYPE ...

Tips for successfully passing a closure as a parameter in a constructor

I encountered an issue while working with a third-party library where I needed to register my own control. The problem arose when I tried to add another dependency to the control and struggled with passing a closure as a parameter to fulfill the required c ...

Using JSON data in an ArrayBuffer with TypeScript

I am currently struggling with converting the received ArrayBuffer data from a server via Websocket into another format. Below is the WebSocket code snippet: let ws = new WebSocket('wss://api.example.com/websocket'); ws.binaryType = 'arrayb ...

Inversify: class-based contextual dependency injection

I am currently experimenting with injecting loggers into various classes using inversify. My goal is to pass the target class name to the logger for categorization. The challenge I'm facing is the inability to access the target name from where I am c ...

Angular 5's data display glitch

Whenever I scroll down a page with a large amount of data, there is a delay in rendering the data into HTML which results in a white screen for a few seconds. Is there a solution to fix this issue? Link to issue I am experiencing HTML binding code snippe ...

Utilizing AngularJS and TypeScript for seamless two-way data binding: a guide for synchronizing controller and directive interaction

Seeking to delegate my table with filtering and sorting functions as a directive. To incorporate two-way data binding, I have implemented the following: public bindToController = { cars: "=" }; This setup is necessary because when a car in the tabl ...

The behavior of the Ionic checkbox in version 5 seems to be quite delayed

I am facing an issue with binding the checked attribute value on an ion-checkbox, as the behavior seems to be delayed. In my .ts file, I have an array variable named user_id. In my checkbox list, I am trying to populate this array based on which checkboxe ...

Creating a dynamic link for a button based on the selected value from a dropdown menu

Here is an example related to my inquiry example.component.html <div class="center"> <div class="form-group" > <label>Choose a Country</label> <select class="form-control"> <option *ngFor="let option of options">{{op ...

Routing with nested modules in Angular 2 can be achieved by using the same

Encountering a common issue within a backend application. Various resources can be accessed through the following routes: reports/view/:id campains/view/:id suts/view/:id certifications/view/:id Note that all routes end with the same part: /view/:id. ...

Uploading multiple strings to an Amazon S3 bucket using Node.js by piping a string

Suppose I have a simple loop similar to the one shown below: for (const i=0; i<3; i++) { to(`This incrementer is ${i}`) } At the end of the loop, I expect my file to contain: This counter is 0 This counter is 1 This counter is 2 I at ...

Is there a specific method for conducting a production build using AngularCLI rc.1?

Just recently upgraded to angular-cli version 1.0.0-rc1 by following the guidelines provided on the wiki. The application functions properly when I execute ng serve. Similarly, the app works as expected when I run ng build. However, encountering an issu ...

Getting the local path of a file from an input file in Angular 7

Is there a way to retrieve the local file path from an input field in HTML? After running the code below, I obtained 'C:\fakepath\fileTest.txt' I am looking for a method to get the local path so that I can pass it on to my control ...

Can you use setValidators() in Angular to validate two patterns simultaneously?

Is there a way to validate both IP address and IP address range in a single control using Angular? I have tried using the following code snippet: controls["CapPoolVolExpolAldClientControl"].setValidators([Validators.required, Validators.pattern(/([0-9]){1 ...

Error: The property 'combine' of 'winston_1.default.format' cannot be destructured since it is not defined

Encountered an error while using Winston in Node.js, how can we resolve it? The version of Winston I am using is 3.3.3 and winston-daily-rotate-file version is 4.5.0 I attempted npm i winston@next --save, but the error persists. ** Here is the Error Mes ...

Angular - Is there a specific type for the @HostListener event that listens for scrolling on the window?

Encountering certain errors here: 'e.target' is possibly 'null'. Property 'scrollingElement' does not exist on type 'EventTarget'. What should be the designated type for the event parameter in the function onWindow ...

Limiting the combinations of types in TypeScript

I have a dilemma: type TypeLetter = "TypeA" | "TypeB" type TypeNumber = "Type1" | "Type2" I am trying to restrict the combinations of values from these types. Only "TypeA" and "Type1" can be paired together, and only "TypeB" and "Type2" can be paired tog ...

Updating the main window in Angular after the closure of a popup window

Is it possible in Angular typescript to detect the close event of a popup window and then refresh the parent window? I attempted to achieve this by including the following script in the component that will be loaded onto the popup window, but unfortunatel ...

I'm facing an issue with Angular2 where I am unable to include a component from

I recently downloaded an angular component from npmjs called ng2-easy-table (I actually created this component, so there may have been some mistakes in its development). package.json { "name": "untitled", "version": "1.0.0", "description": "", "m ...