Search code examples
javascriptnode.jsamazon-s3busboy

How do I recover from failure when uploading multiple files via my server to S3?


New to NodeJS and S3, I wrote the following exploratory code to upload files to S3 via my NodeJS server without saving the file to disk or memory:

var express = require('express');
var Busboy = require('busboy');
var S3 = require('../utils/s3Util');
var router = express.Router(); // mounted at /uploads

router.post("/", function (req, res, next) {
    let bb = new Busboy({ headers: req.headers });
    const uploads = [];
    bb.on('file', (fieldname, stream, filename, encoding, mimeType) => {
        console.log(`Uploaded fieldname: ${fieldname}; filename: ${filename}, mimeType: ${mimeType}`);
        uploads.push(S3.svc.upload({ Bucket: 'my-test-bucket', Key: filename, Body: stream }).promise());
    });
    bb.on('finish', () => {
        console.log("# of promises:", uploads.length);
        Promise.all(uploads).then(retVals => {
            for (let i = 0; retVals && i < retVals.length; i++) {
                console.log(`File ${i + 1}::`, retVals[i]); 
            }
            res.end();
        }).catch(err => {
            console.log("Error::", err);
            res.status(500).send(`${err.name}: ${err.message}`);
        });
    });
    req.pipe(bb);
});

module.exports = router;

In the general failure case, how do I handle the scenario where the upload of 1 or more of x files being uploaded fails? Some uploads would have succeeded, some would have failed. However, in the catch clause I wouldn't know which ones have failed...

It would be good to be able to make this upload process somewhat transactional (i.e., either all uploads succeed, or none do). When errors happen, ideally I would be able to "rollback" the subset of successful uploads.


Solution

  • You could do it like this:

    Push an object into uploads, with the data you need to retry, so:

    uploads.push({ 
      fieldname, 
      filename, 
      mimeType,
      uploaded: S3.svc.upload({ Bucket: 'my-test-bucket', Key: filename, Body: stream })
        .promise()
        .then(() => true)
        .catch(() => false)
    });
    
    ...
    
    const failed = await 
      (Promise.all(uploads.map(async upload => ({...upload, uploaded: await upload.uploaded})))).then(u => u.filter(upload => !upload.uploaded))
    
    const failedFiles = failed.join(', ')
    
    console.log(`The following files failed to upload: ${failedFiles}`);
    

    You need to make your event handlers async to use await inside them, so, for example:

    bb.on('file', async (fieldname, stream, filename, encoding, mimeType) => {