Dynamically create and stream zip to client

I am using NodeJs (w/express) and I am trying to stream a zip file back to the client. The files contained in the zip do not live on the file system, rather they are created dynamically. I would like to stream the file(s) content to the zip and stream the zip back to the client.

I.E. I want the client to receive:

tmp.zip
 --> 1.txt
 --> 2.txt
 --> 3.txt

Where 1,2,3.txt are created on the fly and streamed to the zip file. Is this possible?


Archiver has an append method that lets you save text as a file. To "stream" that data to the user you can simply pipe to the HTTP response object.

var Http = require('http');
var Archiver = require('archiver');

Http.createServer(function (request, response) {
    // Tell the browser that this is a zip file.
    response.writeHead(200, {
        'Content-Type': 'application/zip',
        'Content-disposition': 'attachment; filename=myFile.zip'
    });

    var zip = Archiver('zip');

    // Send the file to the page output.
    zip.pipe(response);

    // Create zip with some files. Two dynamic, one static. Put #2 in a sub folder.
    zip.append('Some text to go in file 1.', { name: '1.txt' })
        .append('Some text to go in file 2. I go in a folder!', { name: 'somefolder/2.txt' })
        .file('staticFiles/3.txt', { name: '3.txt' })
        .finalize();

}).listen(process.env.PORT);

This will create a zip file with the two text files. The user visiting this page will be presented with a file download prompt.


Yes, it's possible. I recommend taking a look at Streams Playground to get a feel for how Node Streams work.

The zip compression in the core zlib library doesn't seem to support multiple files. If you want to go with tar-gzip, you could tar it with node-tar. But if you want ZIP, adm-zip looks like the best option. Another possibility is node-archiver.

Update:

This example shows how to use Archiver, which supports streams. Just substitute fs.createReadStream with the streams you're creating dynamically, and have output stream to Express's res rather than to fs.createWriteStream.

var fs = require('fs');

var archiver = require('archiver');

var output = fs.createWriteStream(__dirname + '/example-output.zip');
var archive = archiver('zip');

output.on('close', function() {
  console.log('archiver has been finalized and the output file descriptor has closed.');
});

archive.on('error', function(err) {
  throw err;
});

archive.pipe(output);

var file1 = __dirname + '/fixtures/file1.txt';
var file2 = __dirname + '/fixtures/file2.txt';

archive
  .append(fs.createReadStream(file1), { name: 'file1.txt' })
  .append(fs.createReadStream(file2), { name: 'file2.txt' });

archive.finalize(function(err, bytes) {
  if (err) {
    throw err;
  }

  console.log(bytes + ' total bytes');
});

solution with: express.js, wait.for, zip-stream

app.get('/api/box/:box/:key/download', function (req, res) {

    var wait = require('wait.for');

    var items = wait.for(function (next) {
        BoxItem.find({box: req.Box}).exec(next)
    });

    res.set('Content-Type', 'application/zip');
    res.set('Content-Disposition', 'attachment; filename=' + req.Box.id + '.zip');

    var ZipStream = require('zip-stream');
    var zip = new ZipStream();

    zip.on('error', function (err) {
        throw err;
    });

    zip.pipe(res);

    items.forEach(function (item) {

        wait.for(function (next) {

            var path = storage.getItemPath(req.Box, item);
            var source = require('fs').createReadStream(path);

            zip.entry(source, { name: item.name }, next);
        })

    });

    zip.finalize();

});

Sending a zip file as binary data with expressjs and node-zip:

app.get("/multipleinzip", (req, res) => {
    var zip = new require('node-zip')();
    var csv1 = "a,b,c,d,e,f,g,h\n1,2,3,4,5,6,7,8\n1,2,3,4,5,6,7,8\n1,2,3,4,5,6,7,8\n1,2,3,4,5,6,7,8";
    zip.file('test1.file', csv1);
    var csv2 = "z,w,x,d,e,f,g,h\n1,2,3,4,5,6,7,8\n1,2,3,4,5,6,7,8\n1,2,3,4,5,6,7,8\n1,2,3,4,5,6,7,8";
    zip.file('test2.file', csv2);
    var csv3 = "q,w,e,d,e,f,g,h\n1,2,3,4,5,6,7,8\n1,2,3,4,5,6,7,8\n1,2,3,4,5,6,7,8\n1,2,3,4,5,6,7,8";
    zip.file('test3.file', csv3);
    var csv4 = "t,y,u,d,e,f,g,h\n1,2,3,4,5,6,7,8\n1,2,3,4,5,6,7,8\n1,2,3,4,5,6,7,8\n1,2,3,4,5,6,7,8";
    zip.file('test4.file', csv4);
    var data = zip.generate({base64:false,compression:'DEFLATE'});
    console.log(data); // ugly data
    res.type("zip")
    res.send(new Buffer(data, 'binary'));
})

Creating a download link for the zip file. Fetch data and convert the response to an arraybuffer with ->

    //get the response from fetch as arrayBuffer...
    var data = response.arrayBuffer();

    const blob = new Blob([data]);
    const fileName = `${filename}.${extension}`;
    
    if (navigator.msSaveBlob) {
      // IE 10+
      navigator.msSaveBlob(blob, fileName);
    } else {
      const link = document.createElement('a');
      // Browsers that support HTML5 download attribute
      if (link.download !== undefined) {
        const url = URL.createObjectURL(blob);
        link.setAttribute('href', url);
        link.setAttribute('download', fileName);
        link.style.visibility = 'hidden';
        document.body.appendChild(link);
        link.click();
        document.body.removeChild(link);
      }
    }