2

I'm trying to use busboy to allow clients to upload files to my Express web server.

I have the following middleware function I'm running for Express.

module.exports = (req, res, next) => {
    req.files = {};

    let busboy;
    try {
        busboy = new Busboy({
            headers: req.headers
        });
    } catch (e) {
        return next();
    }

    busboy.on("file", (fieldname, file, filename, encoding, mimetype) => {
        req.files[fieldname] = {
            file,
            filename,
            encoding,
            mimetype
        };

        // Need to call `file.resume` to consume the stream somehow (https://stackoverflow.com/a/24588458/894067)
        file.resume();
    });
    busboy.on("finish", next);

    req.pipe(busboy);
};

As you can see, I had to add file.resume(); so that the "finish" event would be triggered, and call the next function for the middleware (https://stackoverflow.com/a/24588458/894067).

The problem is, later on, when I want to consume the stream, it says readable: false. So I'm assuming the file.resume(); discards the stream and doesn't allow it to be used in the future.

I basically want to get all the uploaded files and information associated with those files, store them on the req.files object, then consume the streams later, or not consume them if I don't want to use it. That way they remain streams and don't take up much memory, until I'm ready to consume the stream and actually do something with it (or choose to discard it).

What can I use in place of file.resume(); to ensure that the "finish" event get triggers, while allowing me to use the stream later on in the lifecycle of the request (the actual app.post routes, instead of middleware)?

The client might also upload multiple files. So I need any solution to handle multiple files.

Alf Eaton
  • 5,226
  • 4
  • 45
  • 50
Charlie Fish
  • 18,491
  • 19
  • 86
  • 179

2 Answers2

1

Would it make any sense to pipe the input stream into a PassThrough stream, like this?

const Busboy = require('busboy')
const { PassThrough } = require('stream')

const multipart = (req, res, next) => {
  req.files = new Map()
  req.fields = new Map()

  const busboy = new Busboy({ headers: req.headers })

  busboy.on('file', (fieldname, file, filename, encoding, mimetype) => {
    const stream = new PassThrough()
    file.pipe(stream)
    req.files.set(fieldname, { stream, filename, encoding, mimetype })
  })

  busboy.on(
    'field',
    (fieldname, val, fieldnameTruncated, valTruncated, encoding, mimetype) => {
      req.fields.set(fieldname, { val, encoding, mimetype })
    }
  )

  busboy.on('error', (error) => {
    next(error)
  })

  busboy.on('finish', () => {
    next()
  })

  busboy.end(req.rawBody)
}
Alf Eaton
  • 5,226
  • 4
  • 45
  • 50
  • nice answer a few modifications, 1- busboy.end(req.rawBody) does not seem to work but this does req.pipe(busboy); 2- need to increase highWaterMark on PassThrough() if the files are large and you uploading more than one file in the same request otherwise it just freezes – ericsicons Jun 30 '21 at 06:38
0

If you want to handle multiple files in a single request, the procedure is a bit tricky.

Busboy goes through a single stream and fires events whenever files arrive (in sequence). You cannot get separate streams for all files at the same time with Busboy. This is not a limitation from the library, this is how HTTP works.

Your best option would be to store all files in a temporary storage, and keep information for the next middlewares with res.locals :

const Busboy = require('busboy');
const path = require('path');
const fs = require('fs');

module.exports = (req, res, next) => {
  res.locals.files = {};
  // You need to ensure the directory exists
  res.locals.someTemporaryDirectory = '/some/temp/dir/with/randomString/in/it';

  let busboy;
  try {
    busboy = new Busboy({
      headers: req.headers
    });
  } catch (e) {
    return next(e);
  }

  busboy.on("file", (fieldname, file, filename, encoding, mimetype) => {
    res.locals.files[fieldname + '_' + filename] = {
      filename,
      encoding,
      mimetype
    };
    // I skipped error handling for the sake of simplicity. Cleanup phase will be required as well 
    const tempFilePath = path.join(res.locals.someTemporaryDirectory, fieldname + '_' + filename);
    file.pipe(fs.createWriteStream(tempFilePath));
  });

  busboy.on("finish", next);

  req.pipe(busboy);
};

The next middleware shall use res.locals.someTemporaryDirectory and res.locals.files to mind their businesses (that will require a clean-up phase).

This solution may seem sub-optimal, but HTTP is like it is. You may want to issue a separate HTTP request for each file instead, but I would not recommend it as you would encounter a bunch of other issues (such as synchronization of all requests + memory management).

Whatever the solution is, it requires to get your hands dirty.

debel27
  • 393
  • 1
  • 7
  • I need to be able to handle multiple files. Also that doesn’t seem like a very good solution. It could create a race condition. Even if I’m expecting only one file, but the client uploads multiple, that next callback function will get called multiple times, and the file will be overridden multiple times. Overall doesn’t seem like a sound solution that solves the true problem. – Charlie Fish Jan 29 '19 at 14:50
  • Also even in my first sentence I say “files” as opposed to “file”. Therefore, this answer doesn’t really answer the question. – Charlie Fish Jan 29 '19 at 15:01
  • Fair enough. This answer is indeed incorrect. I've investigated, and will write a correct answer shortly – debel27 Jan 29 '19 at 15:55