42

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?

lostintranslation
  • 23,756
  • 50
  • 159
  • 262
  • It's not clear what you mean by "streamed to the zip file"...what file does the HTTP client receive in the end? the zip file? or a concatenation of the three text files? – Jeff Sisson Nov 21 '13 at 04:19
  • Sorry, formatting was messed up, hopefully that is more clear. I want to send back a zip. 2 main issues I am trying to wrap my head around. 1. how to stream the zip back, 2. how to stream my 3 files to a zip file that I am streaming back. I.E. I have seen examples of streaming a zip of files that already exist on the filesystem, but no example of how to do this if you are creating the files in the fly. – lostintranslation Nov 21 '13 at 14:07
  • did u able to find any workaround for the same.? – Manish Rawat Oct 03 '14 at 17:25

4 Answers4

67

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.

CuddleBunny
  • 1,941
  • 2
  • 23
  • 45
  • now what happens to the zip file created after appending all the files. I have a similar situation, where I want to allow my user to download multiple files at once. Since I cannot send them together with response. I have to create a temporary zip and move files in it and then serve it and then delete it so as to store space. So, does archiver create a temporary zip file or a permanent one? – Harshit Laddha Sep 16 '14 at 04:57
  • 1
    I don't think the zip file is "created" per say in that as the utility is building the zip file it is writing those bits to the stream which would be the response to the http request. I'm sure it lives on in the RAM somewhere (or swap file if it is rather large) for a minute until the file is sent off to the browser which will then save it to the end-user's temporary files until it is saved to a location. I've had this code running live for a few months now and my disk space usage has remained the same so there aren't any temporary zips you need to purge. – CuddleBunny Sep 16 '14 at 14:58
  • 1
    This example from the Archiver repo helped me. https://github.com/ctalkington/node-archiver/blob/master/examples/express.js – eephillip Jan 20 '15 at 16:52
  • The Archiver library doesn't seem to have an uncompress / extract feature right now, so I have to use another library for that. – steampowered Dec 15 '15 at 21:53
  • 1
    This is brilliant. – The Dembinski Mar 04 '17 at 21:22
  • Anyone else getting empty zip file? FFS. – The Dembinski Mar 04 '17 at 21:59
  • @TheDembinski can you place a breakpoint and confirm that the files/strings/streams you're placing are defined? – CuddleBunny Mar 06 '17 at 19:17
6

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');
});
Dan Kohn
  • 33,811
  • 9
  • 84
  • 100
  • 1
    Confusing part is I have no idea how to use adm-zip (or any other zip module) to write the zip to a stream and not a file on the file system. Not to mention stream file to the zip that is then streaming to the client. – lostintranslation Nov 21 '13 at 15:54
  • I added an example for you. – Dan Kohn Nov 21 '13 at 20:11
  • seen that example. Still cannot get what I need to work. I am creating file1 and file2 on the fly (they are not on the file system). As I create data for those files I want to add it to the stream. But I want it to be zipped as I add it. – lostintranslation Dec 09 '13 at 05:23
  • I would suggest a new question where you post test code and the results you're seeing. – Dan Kohn Dec 09 '13 at 11:45
6

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();

});
slava
  • 514
  • 1
  • 6
  • 8
2

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);
      }
    }
J0nh1dd3n
  • 161
  • 5