StreamSaver.js
StreamSaver.js copied to clipboard
Inconsistent behaviour of multiple files on different browsers
The following most simple code, of generating and downloading 2 files, works inconsistently on different browsers:
import streamSaver from 'streamsaver'
import { WritableStream } from 'web-streams-polyfill/ponyfill'
if (!streamSaver.WritableStream) streamSaver.WritableStream = WritableStream
function test () {
const encoder = new TextEncoder()
const writer1 = streamSaver.createWriteStream('first.txt').getWriter()
const firstFile = encoder.encode('first')
writer1.write(firstFile).then(() => {
writer1.close().then(() => {
const writer2 = streamSaver.createWriteStream('second.txt').getWriter()
const secondFile = encoder.encode('second')
writer2.write(secondFile).then(() => {
writer2.close()
})
})
})
}
test()
Namely:
- On Safari (12.1.1) only the 'first' file is downloaded.
- On Chrome (78.0.3904.97 - 64bit) only the 'second' file is downloaded.
- On Firefox (70.0.1 - 64bit) both 'first' and 'second' files are downloaded.
Trying this on local machine (MacOS 10.13.6), using non secure protocol (http), but iframe opens fine for both Chrome and Firefox.
This is related to #130, #64
Overall, after deeper investigation, it looks like the 'close()' promise is not properly resolving? The question is if this the issue of 'streamsaver' or 'web-streams-polyfill'?
For me: In chrome: it ask me if i wanted to allow save multiple files, I allowed it and the second file was downloaded. tried it again: both where downloaded. firefox: it ask where where i wanted to save both files. one after the other Safari: only second file was downloaded...
ps, http://localhost is considered as secure also...
maybe you would like to try using my streamable zip tool?
Strange, I will try again in a clean html setup. Meanwhile could you please let me know your browsers versions? The zip is unfortunately not an option in my current use case.
ok,
chrome v78, FF 70, safari 13.03 (all on mac)
doe i tried it in a secure context with onclick event. it behave a tiny bit different when using insecure... and safari is a hole other story, it just uses a good old blob constructor and works similar to how FileSaver works with a[href="blob:url"][download="first.txt"]
any update @vblagomir ?
@jimmywarting hey I got the same issue right now (maybe a bit different but it might help). What I'm doing is to let the user choose a file in Firefox/Safari/Brave/Chrome and open a WebRTC channel and then download it from Safari through WebRTC...I get the entire file on Safari side and pass chunks to StreamSaver. When I received the entire content, I close the writer. But if I try to download again, I get an error in the other browsers Error reading file: DOMException: The requested file could not be read, typically due to permission problems that have occurred after a reference to a file was acquired.
. However if I don't close the StreamSaver in Safari, then I don't get this error and I'm able to download the file multiple times with no issues, but it doesn't actually save the file..it just downloads the content. I find it very strange that it breaks the other browser at the second download. The close works fine if I download any other browser combination
I'm using latest version of Safari on localhost...do you think it can work fine on https? I will make a setup anyway with https in about 2-3 hours..
This is the code that receives file's content and writes it to StreamSaver, nothing fancy
here is the demo that you can use to maybe better understand what I'm talking about:
- using Chrome go to https://dev.super-transfer.com/sender/?userId=user1 - the parameters are hardcoded
- open Chrome console, choose a random file from your computer
- open Safari, go to https://dev.super-transfer.com/receive?userId=user2&toUserId=user1 - this will automaticaly try to connect to user1. Open safari console. In both Chrome & Safari you should see onOpen event. If you see those events, you can click 'Download' in Safari and should download 1 time your chosen file. After you download it once, if you click download again, you will get the error in Chrome. If [onOpen] is does not appear in the console, just refresh Safari a couple of times until you get [onOpen].
- Repeat the previous steps in Chrome-Firefox...you should be able to download the same file multiple times without issue.
The WebRTC negotiation is not quite perfect but refreshing pages a couple of times should get you the onOpen event. If you have any idea what could cause the problem, I'm glad to fork StreamSaver and experiment with things. The blob fallback seems straightforward so there is no room for error there...I'll wait for your response. I'll also remember to donate to this great library if I ever get some real clients on my app :)
Thank you!
@jimmywarting found the problem...should I move everything in another github issue?
This behaviour is not StreamSaver fault but rather the browser or data types inconsistencies.
I don't think I would have caught it without typescript since I'm not familiar with web types/buffers/arrays but the issue is a bit related with https://github.com/jimmywarting/StreamSaver.js/issues/181 and https://github.com/jimmywarting/StreamSaver.js/issues/180
What happens is that createWriteStream().getWriter().write(data)
expects Uint8Array however this seems to break the blob creation for Safari which it work fine with arraybuffer
(I can save the same file multiple times) and not with Uint8Array
(which causes exception in the other browser...don't ask me how).
So I see 2 possible fixes
- upon close being called, identify if the client is safari & data is Uint8Array => convert to
arraybuffer
and build the Blob - when calling write(
arraybuffer
) if client is Safari, push it as is into the chunks array...if is something else, convert to Uint8Array. I think version 2 would be better since it would also solve both issues 180 & 181
maybe replace https://github.com/jimmywarting/StreamSaver.js/blob/1bce493b16fd51cf313570445e2b9f96045393cb/StreamSaver.js#L254
with chunks.push(chunk.buffer)
would also do the trick because chunk will be a typed array Uint8Array
that already has access to arraybuffer
Update: the above solution doesn't seem to work...is definitely something with how safari handles clicks/saving arraybuffer[]/blob/file because if I don't save the file, the transfer works fine and happens multiple times. I tried revoking the object and I also tried FileSaver.js but nothing works and at this point I will just drop Safari support since I'm out of ideas So the code below is just something that I tried with StreamSaver trying to reset the chunks & ObjectURL but doesn't work
if (useBlobFallback) {
const blob = new Blob(chunks, { type: 'application/octet-stream; charset=utf-8' })
const link = document.createElement('a')
link.href = URL.createObjectURL(blob)
link.download = filename
link.click()
setTimeout(function () {
window.URL.revokeObjectURL(link.href);
chunks.length=0;
}, 3000);
}