JS - StreamSaver not writing to file

896 Views Asked by At

I am trying to stream the response of fetch(very large tar file) to a file using streamsaver library. But the file is not created/written, even though "done writing" is printed in the console.

Here is my code.

      const fileStream = streamSaver.createWriteStream('cat.tar')
      fetch(requestUrl).then(res => {
        const readableStream = res.body

        // more optimized
        if (window.WritableStream && readableStream.pipeTo) {
          return readableStream.pipeTo(fileStream)
            .then(() => console.log('done writing'))
        }

        window.writer = fileStream.getWriter()

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

        pump()
      })

I can not find the file in my system. Is there any piece that I am missing?

1

There are 1 best solutions below

0
On

You mentioned that you're using the latest version of Chrome. In that environment, it's possible to stream directly to disk from a fetch response using the File System Access API by piping the response's ReadableStream to the FileSystemWritableFileStream of a FileSystemFileHandle.

Here's the description of how this works from the page FileSystemWritableFileStream.write:

No changes are written to the actual file on disk until the stream has been closed. Changes are typically written to a temporary file instead.

My interpretation of this is that once the temporary file is completely written, it is moved into the place of the selected file handle, overwriting it. (Perhaps someone can clarify this technicality in a comment.) In any case, it seems to satisfy your criteria of "not buffering the entire download stream contents in memory before writing".

Using this method, you'll be responsible for managing all UI indications (start, error, progress, end, etc.) as it won't use the browser's native file download UI: you can hook into stream events using a custom TransformStream for progress monitoring.

The MDN docs that I've linked to have all the information you need to understand how to use these APIs. However, I also prepared a basic and contrived, but self-contained example that you can run using a static file server on localhost to see it all working:

streaming-file-download.html:

<!doctype html>
<html lang="en">

<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />

  <title>Streaming file download simulation</title>

  <style>
    /* Just some styles for this demo */
    body { font-family: sans-serif; } div { display: flex; flex-direction: column; align-items: flex-start; gap: 0.5rem; } textarea { font-size: 1rem; padding: 0.5rem; height: 10rem; width: 20rem; } button { font-size: 1rem; }
  </style>

  <script type="module">
    // Obtain a file handle
    async function getFileHandle () {
      const opts = {
        suggestedName: 'input.txt',
        types: [{
          description: 'Text file',
          accept: {'text/plain': ['.txt']},
        }],
      };
      return window.showSaveFilePicker(opts);
    }

    // Determine whether or not permission to write is granted
    async function getWritePermission (fsFileHandle) {
      const writeMode = {mode: 'readwrite'};
      if (await fsFileHandle.queryPermission(writeMode) === 'granted') return true;
      if (await fsFileHandle.requestPermission(writeMode) === 'granted') return true;
      return false;
    }

    // Mimic fetching a remote resource
    // by creating a response from the text input data
    function fetchResponse () {
      const text = document.querySelector('textarea').value;
      return Promise.resolve(new Response(text));
    }

    // Monitor and react to the progress of a stream using callbacks:
    class ProgressStream extends TransformStream {
      constructor ({start, progress, end} = {}) {
        super({
          start () { start?.(); },
          transform (chunk, controller) {
            const {byteLength} = chunk;
            // Critical: this pipes the stream data forward
            controller.enqueue(chunk);
            progress?.(byteLength);
          },
          flush () { end?.(); },
        });
      }
    }

    let fsFileHandle;

    async function saveFile () {
      if (!fsFileHandle) {
        try { fsFileHandle = await getFileHandle(); }
        catch (exception) {
          // Handle exception: User cancelled, etc.
          // In this demo, we just throw:
          throw exception;
        }
      }

      if (!await getWritePermission(fsFileHandle)) {
        // Handle condition: User revoked/declined permission
        // In this demo, we just throw:
        throw new Error('File write permission not granted');
      }

      const fsWritableStream = await fsFileHandle.createWritable();
      const response = await fetchResponse();

      await response.body
        .pipeThrough(new ProgressStream({
          start: () => console.log('Download starting'),
          progress: (byteLength) => console.log(`Downloaded ${byteLength} bytes`),
          end: () => console.log('Download finished'),
        }))
        .pipeTo(fsWritableStream); // Will automatically close when stream ends

      console.log('Done');
    }

    const saveButton = document.querySelector('button');
    saveButton.addEventListener('click', saveFile);
  </script>
</head>

<body>
  <h1>See console messages</h1>
  <div>
    <textarea>Hello world!</textarea>
    <button>Save</button>
  </div>
</body>

</html>