使用StreamSaver.js流式传输大文件blob

3

我正在尝试使用Angular组件中的StreamSaver.js直接将大型数据文件从服务器下载到文件系统。但是在下载约2GB后会出现错误。似乎数据首先被流式传输到浏览器内存中的blob中。并且可能存在2GB的限制。我的代码基本上是从StreamSaver示例中获取的。有什么想法,我做错了什么,为什么文件没有直接保存在文件系统中?

服务:

public transferData(url: string): Observable<Blob> {
    return this.http.get(url, { responseType: 'blob' });
}

组件:

download(url: string) {
    this.extractionService.transferData(url)
      .subscribe(blob => {
        const fileStream = streamSaver.createWriteStream('data.tel', {
          size: blob.size
        });
        const readableStream = blob.stream();
        if (window.WritableStream && readableStream.pipeTo) {
          return readableStream
            .pipeTo(fileStream)
            .then(() => console.log("done writing"));
        }
        const writer = fileStream.getWriter();
        const reader = readableStream.getReader();
        const pump = () =>
          reader.read()
            .then(res => res.done ? writer.close() : writer.write(res.value).then(pump));
        pump();
      });
}

所请求文件的头部:
"Content-Type: application/octet-stream\r\n"
"Content-Disposition: attachment; filename=data.tel\r\n"

3个回答

6

建议/背景

StreamSaver的目标用户是在客户端生成大量数据的人,例如长时间的摄像头录制。如果文件来自云端,并且您已经拥有Content-Disposition附件头,则您需要做的就是在浏览器中打开此URL。

有几种下载文件的方法:

  • location.href = url
  • <a href="url">download</a>
  • <iframe src="url" hidden>
  • 对于那些需要发布数据或使用其他HTTP方法的人,他们可以发布一个(隐藏的)<form>

只要浏览器不知道如何处理文件,它就会触发下载,这就是您已经使用Content-Type: application/octet-stream所做的。


由于您正在使用Ajax下载文件,浏览器知道如何处理数据(将其交给主JS线程),因此Content-TypeContent-Disposition没有任何作用。
StreamSaver试图模拟服务器使用ServiceWorkers和自定义响应保存文件的方式。
您已经在服务器上执行了这项操作!您需要做的唯一一件事就是停止使用AJAX下载文件。因此,我认为您根本不需要StreamSaver。

您的问题

...是您首先将整个数据作为Blob下载到内存中,然后再保存文件。这破坏了使用StreamSaver的全部目的,您可以使用更简单的FileSaver.js库或像FileSaver.js一样手动创建一个对象URL +链接从Blob中获取文件。

Object.assign(
  document.createElement('a'), 
  { href: URL.createObjectURL(blob), download: 'name.txt' }
).click()

此外,由于Angular使用旧的XMLHttpRequest,无法像fetchresponse.body中提供ReadableStream,因此您不能使用Angular的HTTP服务。因此,我的建议是只需简单地使用Fetch API。

https://github.com/angular/angular/issues/36246


1
可能值得一提的是,您是StreamSaver.js的作者。我认为如果不完全披露作者身份(也就是所谓的垃圾邮件o_O),没有人会将此视为自我推销。但最好还是保险起见,您可能会为这个“伟大的黑客”感到自豪,正如我几次称赞它一样;-P - Kaiido
不太喜欢吹嘘,但是是的,我是StreamSaver的作者。很高兴能得到认可。尝试通过解释工作原理来更明显地表明是我写的。 - Endless

4

我之前也遇到了类似的情况。由于 Angular 的 http 服务没有提供可读流(ReadableStream),所以在这种情况下无法使用它。我的解决方案是改用 fetch API。

但要注意,fetch流式响应体是一个实验性功能,并非所有浏览器都兼容。据我的测试,在 Google Chrome 中可以正常工作,但在 Firefox 或 Safari 中则不行。为了克服这个限制,我使用了一个名为 web-streams-polyfill 的 JavaScript 库与 fetch 一起使用。

代码大致如下:

import { WritableStream } from 'web-streams-polyfill/ponyfill';
import streamSaver from 'streamsaver';

fetch(url, {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json'
    },
    body: JSON.stringify(data)
})
.then(response => {

    let contentDisposition = response.headers.get('Content-Disposition');
    let fileName = contentDisposition.substring(contentDisposition.lastIndexOf('=') + 1);

    // These code section is adapted from an example of the StreamSaver.js
    // https://jimmywarting.github.io/StreamSaver.js/examples/fetch.html

    // If the WritableStream is not available (Firefox, Safari), take it from the ponyfill
    if (!window.WritableStream) {
        streamSaver.WritableStream = WritableStream;
        window.WritableStream = WritableStream;
    }

    const fileStream = streamSaver.createWriteStream(fileName);
    const readableStream = response.body;

    // More optimized
    if (readableStream.pipeTo) {
        return readableStream.pipeTo(fileStream);
    }

    window.writer = fileStream.getWriter();

    const reader = response.body.getReader();
    const pump = () => reader.read()
        .then(res => res.done
            ? writer.close()
            : writer.write(res.value).then(pump));

    pump();
})
.catch(error => {
    console.log(error);
});;

这个想法是检查当前浏览器是否可用 window.WritableStream。如果不可用,直接将 ponyfill 中的 WritableStream 赋值给 streamSaver.WritableStream 属性。

由于我曾经遇到过这个问题,我的解决方案仅在 Google Chrome 78、Firefox 70、Safari 13 和 web-streams-polyfill 2.0.5StreamSaver.js 2.0.3 上进行了测试。


我在使用Firefox时无法设置WritableStream。import streamSaver from 'streamsaver'; ->没有默认导出,import * as streamSaver from 'streamsaver'; ->无法分配给'WritableStream',因为它是只读属性。我正在使用StreamSaver.js 2.0.5。 - maersk
这只发生在Firefox上吗?Google Chrome呢? - Triet Doan
适用于 Chrome。 - maersk
现在WritableStream似乎与所有主要浏览器兼容:https://developer.mozilla.org/en-US/docs/Web/API/WritableStream#browser_compatibility - krummens

1
this.http.post(`url`, body, { responseType: 'blob'}).pipe(
  concatMap((response) => {
    const fileStream = streamSaver.createWriteStream('someFile');
    return response.body.stream().pipeTo(fileStream);
  })
);

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