GoogleChromeLabs / browser-fs-access

File System Access API with legacy fallback in the browser
https://googlechromelabs.github.io/browser-fs-access/demo/
Apache License 2.0
1.38k stars 84 forks source link

Accept a ReadableStream for file writing? #100

Closed tmcw closed 2 years ago

tmcw commented 2 years ago

Bit of a doozy here…

Okay, so at least Chrome has a rule that calls to APIs like showSaveFilePicker have to be loosely connected to a "gesture" like a click. I don't know exactly what the cutoff is, 1 second works but 5 seconds shows an error that the file picker needs to be opened by a gesture.

So, presuming you have a save operation like an "export" and that export has to do some complex things, like converting files in a webworker or dynamically loading some extra modules for conversion. You can accidentally go over the invisible and only-barely-mentioned tripline and Chrome will consider your showSaveFilePicker call to be invalid. Such is life.

So browser-fs-access currently accepts Blob or Response as input, and it pipes either into a WritableStream.

You can, currently, use the Response support as a workaround by creating your own Response object, however this then triggers code that assumes the Response is real and the control of your mime types and extensions changes dramatically in this section of code

https://github.com/GoogleChromeLabs/browser-fs-access/blob/c99a348b14b0ea4cc3c1e84ded89c8bab19b7436/src/fs-access/file-save.mjs#L40

At least for me, this causes a lot of problems: Response.type is default, which means that we add a default type to the accept parameter of showSaveFilePicker, which prevents it from running.


So: q, could or should browser-fs-access allow users to provide a ReadableStream as a third option so that long-running file save operations are possible without having to worry about the semantics of wrapping that stream in a Response object?

tclangv commented 2 years ago

I ran into the problems you described which prompted me to open the PRs #101 and #102.

My current strategy, using the changes of both PRs, is to show the dialog as fast as possible, so no computations are taking place between the user interaction and showing the dialog. But with the new callback introduced in #102 I can trigger the long-running operations (which actually "generate" the data to be saved) right after the dialog was shown. An important part for me is to only start the data generation after the dialog was closed (and not cancelled).

But this only works in browsers supporting the new API. From what I understand of the old API, the whole data has to be assembled in a Blob before showing the dialog.

tomayac commented 2 years ago

Thanks for working on this. I'll take a look at the PRs soon (busy with urgent Google I/O stuff at the moment), but meanwhile please keep in mind that all solutions need to work with the old way of <input type=file> as well.

tomayac commented 2 years ago

Closed via #101 and #102. Thanks!

tomayac commented 2 years ago

@tmcw @tclangv: Please give the just released v0.27.0 a shot. Thanks both for your work!

tmcw commented 2 years ago

I'm not totally sure how @tclangv has been doing this - are you also creating a new Response object to feed into the save command?

Edit: yeah, still not sure if it's possible to cleanly implement a solution here. I'm not yet able to get any solution that works without creating a fake Response type, and that breaks in Firefox.

tmcw commented 2 years ago

Sorry to be a nag, but it really seems like the legacy integration simply doesn't work with readable streams or even native responses.

Here's a reduced testcase of the streamToBlob method:

https://observablehq.com/d/5e661dd48af858a4

In Chrome this produces

TypeError: Failed to execute 'releaseLock' on 'ReadableStreamDefaultReader': Cannot release a readable stream reader when it still has outstanding read() calls that have not yet settled

In firefox it produces

AbortError: The operation was aborted.

In Safari this produces

TypeError: TypeError: read() called on a reader owned by no readable stream

I think the root cause is

https://github.com/GoogleChromeLabs/browser-fs-access/blob/b3be8c6bd93d7be3388aed3ca77da258283494ba/src/legacy/file-save.mjs#L87-L89

await res.blob() is what causes the stream to be read, but it is being called after reader.releaseLock, which closes the stream for reading.


PR in https://github.com/GoogleChromeLabs/browser-fs-access/pull/104

tclangv commented 2 years ago

I'm not totally sure how @tclangv has been doing this - are you also creating a new Response object to feed into the save command?

Edit: yeah, still not sure if it's possible to cleanly implement a solution here. I'm not yet able to get any solution that works without creating a fake Response type, and that breaks in Firefox.

Yes, I create a Response object and yes, it only works with the new API. It seems to me that there might is no hope for Firefox support under the constraints I have. But I am not well versed in JS, so don't take my word for it. For the time being I have given up on Firefox though.

tclangv commented 2 years ago

@tmcw @tclangv: Please give the just released v0.27.0 a shot. Thanks both for your work!

Works fine for me! And thanks for the quick release as well!