DanilMakarov
DanilMakarov

Reputation: 63

Nest js / node js file upload with multer: Pass a file stream to function handler

Multer gets a file from the request in a readable stream, and with default engines, we can save the file on disk or in RAM. After that, it's accessible in a file object that goes to the route handler. In my case, I want to upload the file to S3, so I need access to the stream, but there is no way of getting that in the handler function.

This issue describes how to solve it by creating a custom engine or using a library: https://github.com/aws/aws-sdk-js-v3/issues/5479.

And this is a working solution, but I don't like that I have to pass a custom engine in the decorator. I'm working on a Nest.js app, and I have a separate module and class to work with AWS S3, which has its own dependencies. As I'm having to pass a custom engine in the decorator, it means I have to recreate all dependencies, which is not the Nest way of doing things.

I have tried to develop a custom engine and was looking for a configuration options - nothing.

I'm looking for a way to pass a readable stream to the handler. Answering ahead - saving on disk, creating a readable stream, and deleting the file is not an option.

Upvotes: 1

Views: 1069

Answers (1)

DanilMakarov
DanilMakarov

Reputation: 63

Did not find a way to do that with multer, its just how multer works. It will not pass the control to handler until the file stream is not red. I ended up with custom solution, custom decorator. Under the hood multer uses busboy to handle multipart/form-data, so i did the same thing:

@Injectable()
export class UploadSingleFileInterceptor implements NestInterceptor {
  constructor(private busboyService: BusboyService) {}

  intercept(
    context: ExecutionContext,
    next: CallHandler,
  ): Promise<Observable<unknown>> {
    const ctx = context.switchToHttp();
    const req = ctx.getRequest<Request>();

    return new Promise((resolve, reject) => {
      this.busboyService.create({
        request: req,
        headers: req.headers,
        defCharset: FileUploadConstants.DEFAULT_CHARSET,
        limits: {
          parts: FileUploadConstants.MAX_FORM_FIELDS,
          files: FileUploadConstants.MAX_FORM_FILE_FIELDS,
          fields: FileUploadConstants.MAX_FORM_TEXT_FIELDS,
        },
        handleEvents: {
          file(fieldName, stream, { filename, encoding, mimeType }) {
            if (!req.user) {
              req.user = {};
            }
            const [type] = mimeType.split("/");
            const [format] = filename.split(".").slice(-1);
            const size = Number(req.header(HttpHeaders.CONTENT_LENGTH));

            if (!size) {
              return reject(
                new BadRequestException(
                  FilesErrors.INVALID_CONTENT_SIZE_LENGTH,
                ),
              );
            }

            req.user.uploadedFile = {
              fieldName,
              stream,
              encoding,
              mimeType,
              type,
              format,
              size,
              name: filename,
            };

            resolve(next.handle());
          },
          partsLimit() {
            reject(
              new BadRequestException(FilesErrors.MULTIPART_FORM_PARTS_LIMIT),
            );
          },
        },
      });
    });
  }
}

And the busboy service:

interface IBusboyConfigExtend extends busboy.BusboyConfig {
  request?: Request;
  handleEvents?: Partial<busboy.BusboyEvents>;
}

@Injectable()
export class BusboyService {
  private busboy = busboy;

  create(options?: IBusboyConfigExtend) {
    const busboy = this.busboy(options);

    if (options?.request) {
      options.request.pipe(busboy);
    }

    if (options?.handleEvents) {
      for (const event in options.handleEvents) {
        busboy.on(event, options.handleEvents[event]);
      }
    }

    return busboy;
  }
}

It works only with 1 file per request. After applying that interceptor on route I used an parameter decorator to get the file data from the request user object and pass it to the handler, which is what i wanted in the first place.

Upvotes: -1

Related Questions