javascriptnode.jszipkoaarchiverjs

Creating an in-memory .zip with archiver, and then sending this file to the client with koa on a node server


I (as a node server with Koa framework), need to take a JSON blob, turn it into a file with extension .json, then stick that in a zip archive, then send the archive as a file attachment in response to a request from the client.

It seems the way to do this is use the Archiver tool. Best I can understand, the way to do this is to create an archive, append a json blog to it as a .json file (it automatically creates the file within the archive?), then "pipe" that .zip to the response object. The "piping" paradigm is where my understanding fails, mostly due to not getting what these docs are saying.

Archiver docs, as well as some stackoverflow answers, use language that to me means "stream the data to the client by piping (the zip file) to the HTTP response object. The Koa docs say that the ctx.body can be set to a stream directly, so here's what I tried:

Attempt 1

    const archive = archiver.create('zip', {});
    ctx.append('Content-Type', 'application/zip');
    ctx.append('Content-Disposition', `attachment; filename=libraries.zip`);
    ctx.response.body = archive.pipe(ctx.body);
    archive.append(JSON.stringify(blob), { name: 'libraries.json'});
    archive.finalize();

Logic: the response body should be set to a stream, and that stream should be the archiver stream (pointing at ctx.body).

Result: .zip file downloads on client-side, however the zip is malformed somehow (can't open).

Attempt 2

    const archive = archiver.create('zip', {});
    ctx.append('Content-Type', 'application/zip');
    ctx.append('Content-Disposition', `attachment; filename=libraries.zip`);
    archive.pipe(ctx.body);
    archive.append(JSON.stringify(blob), { name: 'libraries.json'});
    archive.finalize();

Logic: Setting a body to be a stream after, uh, "pointing a stream at it" does seem silly, so instead copy other stackoverflow examples.

Result: Same as attempt 1.

Attempt 3

Based on https://github.com/koajs/koa/issues/944

    const archive = archiver.create('zip', {});
    ctx.append('Content-Type', 'application/zip');
    ctx.append('Content-Disposition', `attachment; filename=libraries.zip`);
    ctx.body = ctx.request.pipe(archive);
    archive.append(JSON.stringify(body), { name: 'libraries.json'});
    archive.finalize();

Result: ctx.request.pipe is not a function.

I'm probably not reading this right, but everything online seems to indicate that doing archive.pipe(some sort of client-sent stream) "magically just works." That's a quote of the archive tool example file, "streaming magic" is the words they use.

How do I in-memory turn a JSON blob into a .json, then append that .json to a .zip that then is sent to the client and downloaded, and can then be successfully unzipped to see the.json ?

EDIT: If I console.log the ctx.body after archive.finalize(), it shows a ReadableStream, which seems right. However, it has a "path" property that worries me - its the index.html, which I had wondered about - in the "response preview" on the client side, I'm seeing a stringified version of our index.html. The file was still downloading a .zip, so I wasn't too concerned, but now I'm wondering if this is related.

EDIT2: Looking deeper into the response on the client-side, it appears that the data sent back is straight up our index.html, so now I'm very confused.


Solution

  • Yes, you can directly set ctx.body to the stream. Koa will take care of the piping. No need to manually pipe anything (unless you also want to pipe to a log, for instance).

    const archive = archiver('zip');
    
    ctx.type = 'application/zip';
    ctx.response.attachment('test.zip');
    ctx.body = archive;
    
    archive.append(JSON.stringify(blob), { name: 'libraries.json' });
    archive.finalize();