Victor Ivens
Victor Ivens

Reputation: 2279

Upload file using nestjs and multer

Since nestjs is an express app, it's possible to use any library to handle upload using nest, and since it provides Midlewares, it's also possible to use multer. My question is: What's the best way to handle file uploads using nestjs?

Upvotes: 12

Views: 96418

Answers (9)

Rene Arias
Rene Arias

Reputation: 308

Use custom Decorator here an example

import { createParamDecorator, ExecutionContext, InternalServerErrorException } from '@nestjs/common';
import Busboy from 'busboy';
import { Request } from 'express';

export const FileUpload = createParamDecorator(
  (data: unknown, ctx: ExecutionContext): Promise<Express.Multer.File> => {
    const req = ctx.switchToHttp().getRequest<Request>();

    return new Promise((resolve, reject) => {
      const fileContent: any[] = [];
      const bb = Busboy({ headers: req.headers as any });
      let multerFile: Partial<Express.Multer.File> = {};

      bb.on('file', (fieldname, file, info) => {
        const { filename, encoding, mimeType } = info;

        file.on('data', (data) => {
          fileContent.push(data);
        });

        file.on('end', () => {
          const buffer = Buffer.concat(fileContent);

          // Simular el objeto Express.Multer.File
          multerFile = {
            fieldname,
            originalname: filename,
            encoding,
            mimetype: mimeType,
            buffer,
            size: buffer.length,
            destination: '',
            filename,
            path: '',
          };
        });
      });

      bb.on('finish', () => {
        resolve(multerFile as Express.Multer.File);
      });

      bb.on('error', (error: any) => {
        reject(new InternalServerErrorException('Failed to process file upload', error.message));
      });

      bb.end(req.body);
    });
  },
);

Upvotes: 0

Victor Ivens
Victor Ivens

Reputation: 2279

As informed by @Kamyl on issue https://github.com/nestjs/nest/issues/262, since v4.6.0 is possible to upload files using multer to nestjs using a common file interceptor.

import { ... , UseInterceptors, FileInterceptor, UploadedFile } from '@nestjs/common'

... 
 
@UseInterceptors(FileInterceptor('file'))
async upload( @UploadedFile() file) {
  console.log(file)
}

This way the variable file will have a buffer


Using Multer options

It's also needed the field name as the first param, then an array with Multer Options

import { ... , UseInterceptors, FileInterceptor, UploadedFile } from '@nestjs/common'
import { diskStorage } from 'multer'
import { extname } from 'path'

...

@UseInterceptors(FileInterceptor('file', {
  storage: diskStorage({
    destination: './uploads'
    , filename: (req, file, cb) => {
      // Generating a 32 random chars long string
      const randomName = Array(32).fill(null).map(() => (Math.round(Math.random() * 16)).toString(16)).join('')
      //Calling the callback passing the random name generated with the original extension name
      cb(null, `${randomName}${extname(file.originalname)}`)
    }
  })
}))
async upload( @UploadedFile() file) {
  console.log(file)
}

This way the variable file will have a filename, destination and path.

The destination param from the diskStorage can also be a function, with the parameters and expecting the callback the same as filename. By passing a diskStorage the file will be automatically saved to the destination informed with the filename given.

It's also possible to handle multiple files by using @UploadedFiles and FilesInterceptor (plural)

Upvotes: 40

Shubham Verma
Shubham Verma

Reputation: 9933

I have written the code to upload single and multiple files using NEST.JS:

app.controller.ts:

import { Controller, Post, UploadedFiles, UploadedFile, UseInterceptors } from '@nestjs/common';
import { FileInterceptor, FilesInterceptor } from '@nestjs/platform-express';
import { AppService } from './app.service';
import { Express } from 'express';


@Controller('api/portal/file')


export class AppController {
  constructor(private appService: AppService) {}


  @Post('/multiple')
  @UseInterceptors(FilesInterceptor('files'))
  async uploadFiles(@UploadedFiles() files: Array<Express.Multer.File>) {
    const req = {
      files,
      prospectId: 1234,
    };
    return await this.appService.getUrls(req);
  }


  @Post('/single')
  @UseInterceptors(FileInterceptor('file'))
  async uploadFile(@UploadedFile() file: Express.Multer.File) {
    const req = {
      files: [file],
      prospectId: 1234,
    };
    return await this.appService.getUrls(req);
  }
}

app.service.ts:

import { Injectable } from '@nestjs/common';
import { FileDataReq, FileDataRes } from './dto/app.dto';

@Injectable()
export class AppService {

  async getUrls(uploadData: FileDataReq): Promise<FileDataRes> {
    const { prospectId } = uploadData;
    const response = { urls: [], prospectId };
    const { files } = uploadData;
    for (const file of files) {
      const { originalname } = file;
      let url = `/${prospectId}/${new Date().getTime()}_${originalname.trim().split(' ').join('_')}`;
      response.urls.push(url);
    }
    return response;
  }
}

app.dto.ts:

export class FileDataReq {
  files: any[];
  prospectId: number;
}

export class FileDataRes {
  urls: string[];
  prospectId: number;
}

Postman:

Multiple Upload: enter image description here

SingleUpload: enter image description here

Upvotes: 0

Sandeep K Nair
Sandeep K Nair

Reputation: 2932

A cleaner way would be to extract the configurations to a separate file and then call it inside the interceptor method

import { extname } from 'path';
import { existsSync, mkdirSync } from 'fs';
import { diskStorage } from 'multer';
import { v4 as uuid } from 'uuid';
import { HttpException, HttpStatus } from '@nestjs/common';

// Multer configuration
export const multerConfig = {
    dest: process.env.UPLOAD_LOCATION,
};

// Multer upload options
export const multerOptions = {
    // Enable file size limits
    limits: {
        fileSize: +process.env.MAX_FILE_SIZE,
    },
    // Check the mimetypes to allow for upload
    fileFilter: (req: any, file: any, cb: any) => {
        if (file.mimetype.match(/\/(jpg|jpeg|png|gif)$/)) {
            // Allow storage of file
            cb(null, true);
        } else {
            // Reject file
            cb(new HttpException(`Unsupported file type ${extname(file.originalname)}`, HttpStatus.BAD_REQUEST), false);
        }
    },
    // Storage properties
    storage: diskStorage({
        // Destination storage path details
        destination: (req: any, file: any, cb: any) => {
            const uploadPath = multerConfig.dest;
            // Create folder if doesn't exist
            if (!existsSync(uploadPath)) {
                mkdirSync(uploadPath);
            }
            cb(null, uploadPath);
        },
        // File modification details
        filename: (req: any, file: any, cb: any) => {
            // Calling the callback passing the random name generated with the original extension name
            cb(null, `${uuid()}${extname(file.originalname)}`);
        },
    }),
};

and then call it under the interceptor like so

import { ... , UseInterceptors, FileInterceptor, UploadedFile } from '@nestjs/common'
import { diskStorage } from 'multer'
import { extname } from 'path'
import { multerOptions } from 'src/config/multer.config';
...

@Post('/action/upload')
@UseInterceptors(FileInterceptor('file', multerOptions))
async upload( @UploadedFile() file) {
  console.log(file)
}

Upvotes: 35

vishal sharma
vishal sharma

Reputation: 190

    Create a helper.ts file that rename your file and contains path
        
    export class Helper {
            static customFileName(req, file, cb) {
              const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
              let fileExtension = "";
              if(file.mimetype.indexOf("jpeg") > -1){
                  fileExtension = "jpg"
              }else if(file.mimetype.indexOf("png") > -1){
                  fileExtension = "png";
              }
              const originalName = file.originalname.split(".")[0];
              cb(null, originalName + '-' + uniqueSuffix+"."+fileExtension);
            }
           
            static destinationPath(req, file, cb) {
              cb(null, 'uploads/')
            }
          }

code for controller

import { Helper } from '../service/Helper';
import { diskStorage } from 'multer';
import {FileInterceptor} from '@nestjs/platform-express'
import {Controller, Post, Body, UseInterceptors, UploadedFile} from '@nestjs/common'

    @Post('upload')
    @UseInterceptors(
        FileInterceptor('picture', {
            storage: diskStorage({
                destination: Helper.destinationPath,
                filename: Helper.customFileName,
            }),
        }),
    )

    uploadFile(@UploadedFile() file: Express.Multer.File) {
    console.log(file);
    }

Upvotes: 0

Nechar Joshi
Nechar Joshi

Reputation: 670

Cleanest implementation using Multer options

Thank you @VictorIvens for the best answer out of the bunch.

However, I found the following problems in the code.

  • the import called FileInterceptor does not exist withing @nestjs/common package int the latest version of NestJS.
  • the code looks a bit too cluttered to my eyes.

So, to simplify the things up, I have come up with the following solution.

storage.config.ts

export const storage = diskStorage({
  destination: "./uploads",
  filename: (req, file, callback) => {
    callback(null, generateFilename(file));
  }
});

function generateFilename(file) {
  return `${Date.now()}.${extname(file.originalname)}`;
}

your-controller.controller.ts

import {
  Controller,
  Post,
  UseInterceptors,
  UploadedFile
} from "@nestjs/common";
import { FileInterceptor } from "@nestjs/platform-express";

import { diskStorage } from "multer";
import { extname } from "path";
import { storage } from "./storage.config"


@Controller()
export class YourController {
  @Post("upload") // API path
  @UseInterceptors(
    FileInterceptor(
      "file", // name of the field being passed
      { storage }
    )
  )
  async upload(@UploadedFile() file) {
    return file;
  }
}

Upvotes: 7

Chris Hawkes
Chris Hawkes

Reputation: 12410

**

2021 Update

**

To do this now, you need to import FileInterceptor like this...

import { FileInterceptor } from '@nestjs/platform-express';

Upvotes: 2

nitin aditya
nitin aditya

Reputation: 139

If you are getting the data from the user via API call, you can save the data as buffer and access the content using adm-zip. Below is the controller method implementation in nest.js.

@Post("/blackBoardUpload")
  @UseInterceptors(
    FileInterceptor('image', {
      storage: memoryStorage(),



      fileFilter: zipFileFilter,

    }),
  )
  async uploadedFile(@UploadedFile() file) {
    console.log(file)
    const response = {
      originalname: file.originalname,
      filename: file.filename,
    };
    var AdmZip = require('adm-zip');
    var zip = new AdmZip(file.buffer);

    var zipEntries = zip.getEntries();
    console.log(zipEntries.length);
    
    return {
      status: HttpStatus.OK,
      message: 'Received Zip file successfully!',
      data: response,
    };
  }

Upvotes: 0

Ali Tourani
Ali Tourani

Reputation: 1247

A simple way is to use controllers. You need to define an upload controller and add it in your app.module, this is an example of what a controller should be (back-end):

@Controller()
export class Uploader {
  @Post('sampleName')
  @UseInterceptors(FileInterceptor('file'))
  uploadFile(@UploadedFile() file) {
  // file name selection 
    const path = `desired path`;
    const writeStream = fs.createWriteStream(path);  
    writeStream.write(file.buffer);
    writeStream.end();
    return {
      result: [res],
    };
  }
}

And call your controller by fetch in the front-end:

    fetch('controller address', {
          method: 'POST',
          body: data,
        })
          .then((response) => response.json())
          .then((success) => {
            // What to do when succeed 
});
          })
          .catch((error) => console.log('Error in uploading file: ', error));

Upvotes: -3

Related Questions