StreamSaver.js icon indicating copy to clipboard operation
StreamSaver.js copied to clipboard

Inconsistent behaviour of multiple files on different browsers

Open vbgm opened this issue 4 years ago • 11 comments

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:

  1. On Safari (12.1.1) only the 'first' file is downloaded.
  2. On Chrome (78.0.3904.97 - 64bit) only the 'second' file is downloaded.
  3. 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'?

vbgm avatar Nov 16 '19 18:11 vbgm

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...

jimmywarting avatar Nov 16 '19 21:11 jimmywarting

ps, http://localhost is considered as secure also...

jimmywarting avatar Nov 16 '19 21:11 jimmywarting

maybe you would like to try using my streamable zip tool?

jimmywarting avatar Nov 16 '19 21:11 jimmywarting

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.

vbgm avatar Nov 16 '19 23:11 vbgm

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"]

jimmywarting avatar Nov 17 '19 00:11 jimmywarting

any update @vblagomir ?

jimmywarting avatar Jan 23 '20 12:01 jimmywarting

@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 image

tudor-pop avatar Oct 12 '20 17:10 tudor-pop

here is the demo that you can use to maybe better understand what I'm talking about:

  1. using Chrome go to https://dev.super-transfer.com/sender/?userId=user1 - the parameters are hardcoded
  2. open Chrome console, choose a random file from your computer
  3. 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].
  4. 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!

tudor-pop avatar Oct 12 '20 21:10 tudor-pop

@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

  1. upon close being called, identify if the client is safari & data is Uint8Array => convert to arraybuffer and build the Blob
  2. 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

tudor-pop avatar Oct 13 '20 20:10 tudor-pop

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

tudor-pop avatar Oct 13 '20 21:10 tudor-pop

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);
}

tudor-pop avatar Oct 17 '20 11:10 tudor-pop