JavaScript:写入下载流

22

我想从我的服务器下载一个加密文件,解密并保存到本地。我希望能够在文件下载的同时对其进行解密并将其写入本地,而不必等待下载完成后再进行解密并将解密后的文件放入锚点标签中。我这样做的主要原因是,在处理大文件时,浏览器不必在内存中存储数百兆或几个千兆字节的数据。


1
@guest271314 - 这需要整个下载完成后才能将文件写入本地文件系统 - 我认为 OP 想要通过某些解密组件“管道”传输传入的数据,并将解密后的数据“管道”传输到文件系统 - 以便“浏览器不必在内存中存储数百兆字节或几个千兆字节”。 - Jaromanda X
一个文件如何可以“即时保存”? - guest271314
@Hephaestious - 对于您来说,插件或网页扩展是否是可行的选择(需要客户选择安装插件/扩展程序-这意味着当然不支持IE)? - Jaromanda X
应该可以使用nodejs实现。在这里,希望整个流程在提供下载文件之前完成并进行验证。如果一个100MB的文件最后一个字节损坏了怎么办? - guest271314
为什么这会有问题呢?攻击者仍然需要访问用户的浏览器或系统才能查看数据,是吧? - Hephaestious
显示剩余34条评论
3个回答

23

只有结合服务工作线程、fetch和stream才能实现这一点。一些浏览器已经支持worker和fetch,但支持流式传输的fetch更少(Blink)。

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

我已经构建了一个流文件保存库,以便与服务工作线程通信,拦截网络请求:StreamSaver.js

它与node的stream略有不同,以下是一个示例:

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')
    )
})

你也可以通过xhr获得流式响应的另外两种方式,但这不是标准的,如果你不使用它们(responseType = ms-stream || moz-chunked-arrayBuffer),也没有关系,因为StreamSaver无论如何都依赖于fetch+ReadableStream,不能以其他方式使用

稍后当WritableStream + Transform streams也被实现时,你将能够像这样做

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

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

值得一提的是,默认下载管理器通常与后台下载相关联,因此有些人在看到下载时会关闭选项卡。但这一切都发生在主线程中,所以当用户离开时需要警告用户。

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
}

嗨,我正在使用Angular并尝试相同的解决方案,但我无法获得下载弹出窗口,即使在控制台上也没有看到任何错误。 - Naveen Ramawat
我能够流式下载,这真是救命稻草,非常感谢。 - Naveen Ramawat
在客户端使用StreamSaver JS来流式传输数据。下载的文件是加密的,数据以base64格式存储。从fetch返回的流数据是Uint8Array格式的。有没有办法将其转换为字符串格式?我问这个问题的原因是一旦数据处于Unit8Array格式,当数据涉及多字节字符时,我发现很难从Unit8Array中获取字符串(bas64编码数据)。 - Ranganatha
嗨,我又遇到了一个问题。当我下载大约1GB的文件时,它会下载两次,但是当文件很小时,它只下载一次。在大文件上,它会在后台进行三次调用: 1)https://localhost:8443/api/v1.0/ui/appliance/download_logs?archived_file=8948060_logs.tar.gz 2)https://jimmywarting.github.io/StreamSaver.js/mitm.html?version=2.0.0 3)https://jimmywarting.github.io/StreamSaver.js/localhost:8443/965011/8948060_logs.tar.gz|但是在小文件上只有两个调用(1)和(3),有什么想法为什么会这样? - Naveen Ramawat
1
你能否在我的代码库中创建一个新问题或者发布一个新的 Stack Overflow 问题,附上一个示例以及出现的问题? - Endless
显示剩余3条评论

5
新的解决方案已经到来:showSaveFilePicker/FileSystemWritableFileStream,自2020年底起在Chrome和所有主要衍生产品(包括Edge和Opera)中得到支持,并且作者提供了一个适配器(另一个主要答案的作者编写!),可用于Firefox和Safari,这将使您能够直接执行以下操作:
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);

}

根据性能而定——例如,如果您要与MEGA竞争,您可能还要考虑修改DECRYPT(chunk)以允许您使用ReadableStreamBYOBReader

...从底层字节源进行零拷贝读取。它用于从底层源高效地复制数据,其中数据作为“匿名”字节序列传递,例如文件。


嗨James,我正在做类似的东西,并且无法弄清如何读取特定大小的块。在上传时,我使用slice将它们分成5MB大小的块,并加密每个块,然后通过多部分上传发送到S3。但是,在下载时,我无法定义块的大小。 - Samay
当我们处理特定大小的块时,由于从获取中生成的流速度与处理速度不匹配,这是否会引起问题。 - Samay
@Samay 如果磁盘和CPU无法跟上以全网速解密和存储文件,我相信浏览器会适当地限制fetch,就像正常下载文件时一样。 - JamesTheAwesomeDude
@Samay 在这种情况下,“reader”不过是由Fetch本身产生的“ReadableStream”(https://developer.mozilla.org/en-US/docs/Web/API/Response#response.body)而已。它以适合下载的最高效块大小输出数据,可能与网络有关,并由浏览器自行决定。如果您需要以32B或5MB或其他块大小处理数据,则需要从Fetch提供的流中[自己打包这些单元](https://codereview.stackexchange.com/a/58877/242503)。 - JamesTheAwesomeDude
谢谢@James。那很有用。我创建了一个在浏览器中工作的TransformStream包装器。https://gist.github.com/samaybhavsar/97d2674536c6f64de8b6d2c43085a347 - Samay

-5

出于安全原因,浏览器不允许将传入的可读流直接导入本地文件系统,因此您有两种解决方法:

  1. window.open(Resource_URL):在新窗口中下载资源,并将Content_Disposition设置为“attachment”;
  2. <a download href="path/to/resource"></a>:使用AnchorElement的“download”属性将流下载到硬盘中;

希望这些能帮到您 :)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接