jimmywarting / StreamSaver.js

StreamSaver writes stream to the filesystem directly asynchronous
https://jimmywarting.github.io/StreamSaver.js/example.html
MIT License
3.98k stars 413 forks source link

Error in Safari #178

Closed stephanoshadjipetrou closed 4 years ago

stephanoshadjipetrou commented 4 years ago

Hi! Thanks for this library, really helpful :)

Got an issue with using it in Safari though. Tested in Chrome, Opera, Firefox and works as expected.

import { createWriteStream } from 'streamsaver';

export const downloadFile = (url, fileName) => {
  return fetch(url)
    .then((res) => {
      const fileStream = createWriteStream(fileName);
      const writer = fileStream.getWriter();
      if (res.body.pipeTo) {
        writer.releaseLock();
        try {
          return res.body.pipeTo(fileStream); // getting: TypeError: |this| is not a Promise
        } catch (e) {
          console.log(e); 
        }
      }

      const reader = res.body.getReader(); // getting: TypeError: ReadableStream is locked
      const pump = () =>
        reader
          .read()
          .then(({ value, done }) =>
            done ? writer.close() : writer.write(value).then(pump)
          );

      return pump();
    })
    .catch((error) => console.error(error));
};

OS: MacOS Catalina 10.15.6

jimmywarting commented 4 years ago

Safari has pipeTo but it's useless without any WritableStream support so you probably have to use this if statement

      if (res.body.pipeTo && globalThis.WritableStream) { // must check for native WritableStream support

when you use pipeTo the readablestream will be locked, so the 2nd error will also go away when you don't call pipeTo

stephanoshadjipetrou commented 4 years ago

@jimmywarting thanks! works perfectly!

jimmywarting commented 4 years ago

The 2nd option is to convert the native readableStream you get from response.body to a polyfilled readableStream

jimmywarting commented 4 years ago

the 3th option is to use <a href="url" download="filename.txt"> works best if it's in the same origin. even better is if you have content-disposition attachment response header when you want to download something - then you don't have to emulate what the server is doing with a service worker

wawezz commented 2 years ago

Please help with safari I've no errors but download starts after fetch get all data from server

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

function streamDownload({ data, name, ext }: { data: ReadableStream, name?: string, ext?: string }) {
  if (!window.WritableStream) {
    streamSaver.WritableStream = WritableStream;
    window.WritableStream = WritableStream;
  }

  const fileStream = streamSaver.createWriteStream(`${name || 'download'}${ext ? `.${ext}` : ''}`);

  try {
    if (data.pipeTo) {
      data.pipeTo(fileStream);
    } else {
      const writer = fileStream.getWriter();
      const reader = data.getReader();
      const pump = (): Promise<void> => reader.read()
        .then((res: any) => (res.done
          ? writer.close()
          : writer.write(res.value).then(pump)));

      pump();
    }
  } catch (error) {
    throw error;
  }
}

export default streamDownload;
jimmywarting commented 2 years ago

@wawezz This is intentional - as safari don't support saving data generated by service workers it reads the stream and builds a blob, blob-url and uses a download link

it's the best we can do for now. see #69

if possible, try using a server to save it instead of emulating what it dose using a service worker. StreamSaver.js targeted audience is for those who generate huge amount of data on the client side.

wawezz commented 2 years ago

It's sad. Tnx for answer!