JavaScript: Writing to download stream
Asked Answered
N

2

23

I want to download an encrypted file from my server, decrypt it and save it locally. I want to decrypt the file and write it locally as it is being downloaded rather than waiting for the download to finish, decrypting it and then putting the decrypted file in an anchor tag. The main reason I want to do this is so that with large files the browser does not have to store hundreds of megabytes or several gigabytes in memory.

Nicholas answered 25/9, 2016 at 1:4 Comment(39)
Do you have decryption algorithm for the data?Faveolate
I'm planning to use AES.Nicholas
You can't write to files on a users computer in browser Javascript. So you can't do what you are trying to achieve unless you are working with, say, Electron or something similar.Aseity
@LukePark "You can't write to files on a users computer in browser Javascript." Technically, it is possible to write to user filesystem.Faveolate
What about with the method this post describes? Would it be possible to edit the content as it is being downloaded?Nicholas
@Faveolate No it isn't. Unless you consider cache, local storage etc to be "writing to the filesystem". Which it isn't.Aseity
@LukePark See How to Write in file (user directory) using JavaScript? .Faveolate
@Nicholas You can use ReadableStream, see JS Promise - instantly retrieve some data from a function that returns a PromiseFaveolate
Didnt know this existed. Still not sure if it will solve the problem though.Aseity
"Still not sure if it will solve the problem though" What is the problem?Faveolate
It looks like that allows you to read the stream as it is being downloaded but I don't see how to save the stream locally as it is being processedNicholas
What do you mean by "modify the data being saved"? Use the decryption method that you selectFaveolate
How would you then save that data locally as you process it?Nicholas
You can store the data as a Blob or ArrayBuffer, as the data is streaming, following any decryption processes; then use createObjectURL or data URI of data for user to download when stream is completeFaveolate
@Faveolate - that would require the whole download to complete prior to the file being written to the local filesystem - I think the OP wants to "pipe" the incoming data through some decryption component and "pipe" the decrypted data to filesystem - such that the browser does not have to store hundreds of megabytes or several gigabytes in memory.Immersionism
@JaromandaX Correct, I need to do the decryption and saving on the fly.Nicholas
How can a file be saved "on the fly"?Faveolate
By writing to a download stream or file, the latter of which I'm aware JavaScript doesn't really allow for.Nicholas
@Nicholas - is an addon or web extension a viable option for you (requires clients to choose to install the addon/extension - which means no support for IE at all of course)Immersionism
Should be possible using nodejs. Here, would want for entire stream to be completed and verified before offering a file for download. What if 100MB of file is ok, though last byte is corrupted?Faveolate
I'm going to have a C# client that can do the same thing but I'd really like to be able to have a web client that does not have external dependencies.Nicholas
@JaromandaX Why would it be a problem? An attacker would still need access to the user's browser or system to view the data, no?Nicholas
@Nicholas See stream-handbook . Note, you can, generally, utilize use browserify to use nodejs at browser.Faveolate
@Faveolate I don't see any way to download data from that stream to the client's filesystem.Nicholas
@JaromandaX I'm going to be using the Stanford Crypto Library most likely, so I don't think it would matter if people can see how the data is encrypted or decrypted when using a secure algorithm such as AES. The user will still need to enter a password to decrypt anything.Nicholas
@Nicholas - no problems - I'll remove the "noise" in the comments about this aspect :pImmersionism
@Nicholas Yes, nodejs is not, generally, intended to be used at browser. Was attempting to link to illustrations of using .pipe(). Process data at server, or even Worker, then offer download. Again, not certain how a file can be downloaded as a stream. Closest have viewed is a .zip file being populated as it downloads. What is the size of the file that will be downloaded?Faveolate
@Faveolate - What is the size of the file that will be downloaded? - it's in the questionImmersionism
@JaromandaX Ok. Got it.Faveolate
@Nicholas Have you tried without attempting to stream the download? Does browser freeze?Faveolate
Without streaming the download it would be very easy: Just do an ajax.get(url), decrypt the data and put it in an anchor tag or one of the other ways you can download a specified string with JavaScript. I want to decrypt it client side as if I decrypt it server side there's not really any point in using encryption at all.Nicholas
Not focusing, here, on decryption portion, only technical viability of streaming a download. You could probably stream to user filesystem using requestFileSystem, then use .toURL() to offer download. Though, admittedly, have not tried to append characters to a data URI set at a element href after user has clicked anchorFaveolate
Not all browsers support requestFileSystem.Nicholas
Yes, that is true. Just trying to offer possible options to meet requirement.Faveolate
All browsers have Blob and ArrayBuffer defined; though, again, have not tried appending bytes to a Blob or ArrayBuffer as the bytes are being downloaded to a local file.Faveolate
@Nicholas Actually just tried appending bytes to a data URI after click at <a> element having download attribute, and the bytes were appended to the saved file. So, this may be possible jsfiddle.net/6xazmmppFaveolate
Thanks you! I'm going to test this right now.Nicholas
@Nicholas Tried with i < 100000, tab crashedFaveolate
Let us continue this discussion in chat.Nicholas
T
24

This is only going to be possible with a combination of service worker + fetch + stream A few browser has worker and fetch but even fewer support fetch with streaming (Blink)

new Response(new ReadableStream({...}))

I have built a streaming file saver lib to communicate with a service worker in other to intercept network request: StreamSaver.js

It's a little bit different from node's stream here is an example

function unencrypt(){
    // should return Uint8Array
    return new Uint8Array()
}

// We use fetch instead of xhr that has streaming support
fetch(url).then(res => {
    // create a writable stream + intercept a network response
    const fileStream = streamSaver.createWriteStream('filename.txt')
    const writer = fileStream.getWriter()

    // stream the response
    const reader = res.body.getReader()
    const pump = () => reader.read()
        .then(({ value, done }) => {
            let chunk = unencrypt(value)

            // Write one chunk, then get the next one
            writer.write(chunk) // returns a promise

            // While the write stream can handle the watermark,
            // read more data
            return writer.ready.then(pump)
        )

    // Start the reader
    pump().then(() =>
        console.log('Closed the stream, Done writing')
    )
})

There are also two other way you can get streaming response with xhr, but it's not standard and doesn't mather if you use them (responseType = ms-stream || moz-chunked-arrayBuffer) cuz StreamSaver depends on fetch + ReadableStream any ways and can't be used in any other way

Later you will be able to do something like this when WritableStream + Transform streams gets implemented as well

fetch(url).then(res => {
    const fileStream = streamSaver.createWriteStream('filename.txt')

    res.body
        .pipeThrogh(unencrypt)
        .pipeTo(fileStream)
        .then(done)
})

It's also worth mentioning that the default download manager is commonly associated with background download so ppl sometimes close the tab when they see the download. But this is all happening in the main thread so you need to warn the user when they leave

window.onbeforeunload = function(e) {
  if( download_is_done() ) return

  var dialogText = 'Download is not finish, leaving the page will abort the download'
  e.returnValue = dialogText
  return dialogText
}
Thumbstall answered 25/9, 2016 at 9:33 Comment(8)
Is implementation of unencrypt methos missing?Singlestick
yes, cuz unencrypt can work diffrent, have a look at webcrypto do implement your own decryptionThumbstall
Hi, I am using angular and I tries the same solution, I am not able to get download popup even I have not seen any error on console.Singlestick
I could able to download in stream, Its lifesaver, Thanks a lotSinglestick
Using the StreamSaver JS on client side to stream the data. The file being download is encrypted and data in base64 format. The stream data returned from fetch is in Uint8Array. Is is there anyway way to change it string format ? The reason I am asking once the data is in Unit8Array format, I am finding it difficult to get the string (bas64 encoded data) from Unit8Array when the data involves the multi byte charatersWendy
HI I am facing another problem, When I download file around 1 GB its downloaded twice but when file is small it download once, At large file it makes three calls in background 1)localhost:8443/api/v1.0/ui/appliance/… 2)jimmywarting.github.io/StreamSaver.js/mitm.html?version=2.0.0 3)jimmywarting.github.io/StreamSaver.js/localhost:8443/965011/… But on small file only two calls made (1) and (3), Any idea why its happening?Singlestick
could you make a new issue in my repo or post a new SO question with a example and what goes wrong?Thumbstall
Thanks for quick response Jimmy Warting, There is one more issue I am having. My system running on self signed certificate. And I want to serve mitm.html from my server because at client setup outer internet might be unavailable. I tried but facing issue to register service worker. Is it possible to use mitm from local server which is running using self sign certificate ?Singlestick
T
8

New solution has arrived: showSaveFilePicker/FileSystemWritableFileStream, supported in Chrome and all major derivatives (including Edge and Opera) since the end of 2020, and with a shim (written by the author of the other major answer!) for Firefox and Safari, will allow you to do this directly:

async function streamDownloadDecryptToDisk(url, DECRYPT) {

    // create readable stream for ciphertext
    let rs_src = fetch(url).then(response => response.body);

    // create writable stream for file
    let ws_dest = window.showSaveFilePicker().then(handle => handle.createWritable());

    // create transform stream for decryption
    let ts_dec = new TransformStream({
        async transform(chunk, controller) {
            controller.enqueue(await DECRYPT(chunk));
        }
    });

    // stream cleartext to file
    let rs_clear = rs_src.then(s => s.pipeThrough(ts_dec));
    return (await rs_clear).pipeTo(await ws_dest);

}

Depending on performance—if you're trying to compete with MEGA, for instance—you might also consider modifying DECRYPT(chunk) to allow you to use ReadableStreamBYOBReader with it:

…zero-copy reading from an underlying byte source. It is used for efficient copying from underlying sources where the data is delivered as an "anonymous" sequence of bytes, such as files.

Timeserver answered 8/7, 2022 at 17:12 Comment(5)
Hi James, I am working on something similar and unable to figure out how to read chunk of a specific size. When uploading I make chunks of 5MB using slice and encrypt each of them and send it via multipart upload to S3. However when downing, I am unable to define the chunk size.Neisa
And when we are processing chunk of a specific size, would it not create issues as the speed of stream generate from fetch would not match the processing.Neisa
@Neisa If the disk and CPU cannot keep up with decrypting and storing the files at full network speed, I believe that the browser will throttle the fetch stream appropriately, just as it does when downloading files normally.Timeserver
@Neisa The reader in this case is nothing more than the ReadableStream produced by Fetch itself. It spits out data in whatever chunk size is efficient for downloading, probably to do with the network, at the browser's discretion. If you need to process the data in 32B or 5MB or whatever chunk sizes, you'll need to package such units up yourself, from the stream Fetch gives you.Timeserver
Thanks @James. That was useful. I create a wrapper around TransformStream that works in the browser. gist.github.com/samaybhavsar/97d2674536c6f64de8b6d2c43085a347Neisa

© 2022 - 2024 — McMap. All rights reserved.