-1

How do I transfer a zip archive generated on the server back to the client? I'm using AngularJS and SailsJS. Currently I set the HTML headers to match the content type, generate the archive using archiver and pipe the data into the res obejct before calling res.end().

The file-data is succesfully placed inside the XHR response, but the file is never downloaded on the clients side - unless I make an API call to zipFiles (see the code below).

How do I fix this?

  zipFiles: async function (req, res) {
    var archiver = require('archiver');

    var year = req.allParams().year;
    var quarter = req.allParams().quarter;

    /*
     * FIXME: This is dangerous, the same code is present in api/controllers/sirka/SirkaShStatController.js
     * FIXME: A globally-available file should contain all relevant paths
     */

    var src_path = __some__path__

    var file_name = `download.zip`;

    // Set HTML headers to match the contents of the respone
    res.writeHead(200, {
      'Content-Type': 'application/zip',
      'Content-Disposition': `attachment; filename=${file_name}`,
    });

    var archive = archiver('zip');

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

    // Once the archive has been finished (by archive.finalize()) send the file
    archive.on('finish', function() {
      sails.log.info('Archive finished, sending...')
      res.end();
    });

    // Pipe the archive data into the respone object
    archive.pipe(res);

    // Append files found in src_path at the top level of the archive
    archive.directory(src_path, false);

    archive.finalize();
  }
pietrrrek
  • 33
  • 8
  • _“but the file is never downloaded on the clients side”_ - of course it isn’t; with XHR/AJAX, you are making a _background_ request, so this is not _supposed_ to hassle the user with any download dialogs automatically from there. If you want to trigger a download dialog from this environment, you need to perform additional steps, see duplicate for details. – CBroe Sep 08 '20 at 10:20
  • 1
    Does this answer your question? [download file using an ajax request](https://stackoverflow.com/questions/20830309/download-file-using-an-ajax-request) – CBroe Sep 08 '20 at 10:20
  • @CBroe Thank you, the answer you linked did help me a bit, if you're interested in my final solution check out the accepted comment where I described my solution. – pietrrrek Sep 11 '20 at 12:56

1 Answers1

0

After a lot of searching and tinkering I've finally managed to solve the issue. I'll try to explain the different approaches that I took and their results.

1st approach Generate the ZIP-file in-memory and transfer the binary data back to the user through the request.

This approach failed (see original question) since the call to zip the files was done through XHR/AJAX, even though it was possible to pipe the data into the response, it coulnd't be fetched on the client side.

2nd approach Create the zip-file on the server, then represent the binary data as a Buffer. With this approach, I could simply return the buffer back to the caller by calling res.ok(data) once the zip-file was fully generated:

var archiver = require('archiver');
var archive = archiver('zip');
var fs = require('fs');

var output = fs.createWriteStream(dst_path);

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

    // Once the archive has been finished (by archive.finalize()) send the file
archive.on('finish', function() {
   sails.log.info('Archive finished, sending...');
});

output.on('close', function () {
   var data = fs.readFileSync(dst_path);
   console.log(data);
   console.log(Buffer.byteLength(data));
   return res.ok(data);
})

// Pipe the archive data into the response object
archive.pipe(output);

// Append files found in src_path at the top level of the archive
archive.directory(src_path, false);

archive.finalize();

Then on the client-side I simply receive the data, convert it to Uint8Array and wrap it with another array. The wrapping is necessary since the data makes up one whole part of the Blob.

Then once the Blob is generated, I create an ObjectURL for it and attach the link to an invisible a element that is automatically clicked.

var dataBuffer = res["data"];
var binaryData = new Uint8Array(dataBuffer);
var blobParts =  [binaryData];

var blob = new Blob(blobParts, {type: 'application/zip'});
var downloadUrl = URL.createObjectURL(blob);

var a = document.createElement('a');
document.body.appendChild(a);
a.style = "display: none";
a.href = downloadUrl;
a.download = `Sygehusstatistik-${year}-K${quarter}.zip`;
a.click()

I've had issues with the generated zip-file getting placed into itself recursively, in order to avoid that ensure that src_path != dst_path

pietrrrek
  • 33
  • 8
  • What a waste to buffer up the zip in browsers memory, if the file comes from the server use content-disposition header and *go* to the link from ether navigation, submit or hidden iframe. or `a[href=url][download=name]` there is no need to involve client side download functionality – Endless Sep 11 '20 at 21:29
  • To be fair this pattern of using navigation/download URL has a few downsides: url has to be kept short, so if you have a lot of config for a file export you need a non standard "body" in your GET requests ; the code feels clumsy if you want to trigger the download without navigation (adding a link in DOM and cliking it)... But indeed it's better than a blob in browser RAM. – Eric Burel Aug 15 '22 at 09:30