|
@@ -0,0 +1,296 @@ |
|
|
|
|
|
/* global chrome location ReadableStream define MessageChannel TransformStream */ |
|
|
|
|
|
|
|
|
|
|
|
;((name, definition) => { |
|
|
|
|
|
typeof module !== 'undefined' |
|
|
|
|
|
? module.exports = definition() |
|
|
|
|
|
: typeof define === 'function' && typeof define.amd === 'object' |
|
|
|
|
|
? define(definition) |
|
|
|
|
|
: this[name] = definition() |
|
|
|
|
|
})('streamSaver', () => { |
|
|
|
|
|
'use strict' |
|
|
|
|
|
|
|
|
|
|
|
let mitmTransporter = null |
|
|
|
|
|
let supportsTransferable = false |
|
|
|
|
|
const test = fn => { try { fn() } catch (e) {} } |
|
|
|
|
|
const ponyfill = window.WebStreamsPolyfill || {} |
|
|
|
|
|
const isSecureContext = window.isSecureContext |
|
|
|
|
|
let useBlobFallback = /constructor/i.test(window.HTMLElement) || !!window.safari |
|
|
|
|
|
const downloadStrategy = isSecureContext || 'MozAppearance' in document.documentElement.style |
|
|
|
|
|
? 'iframe' |
|
|
|
|
|
: 'navigate' |
|
|
|
|
|
|
|
|
|
|
|
const streamSaver = { |
|
|
|
|
|
createWriteStream, |
|
|
|
|
|
WritableStream: window.WritableStream || ponyfill.WritableStream, |
|
|
|
|
|
supported: true, |
|
|
|
|
|
version: { full: '2.0.0', major: 2, minor: 0, dot: 0 }, |
|
|
|
|
|
mitm: 'https://jimmywarting.github.io/StreamSaver.js/mitm.html?version=2.0.0' |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* create a hidden iframe and append it to the DOM (body) |
|
|
|
|
|
* |
|
|
|
|
|
* @param {string} src page to load |
|
|
|
|
|
* @return {HTMLIFrameElement} page to load |
|
|
|
|
|
*/ |
|
|
|
|
|
function makeIframe (src) { |
|
|
|
|
|
if (!src) throw new Error('meh') |
|
|
|
|
|
const iframe = document.createElement('iframe') |
|
|
|
|
|
iframe.hidden = true |
|
|
|
|
|
iframe.src = src |
|
|
|
|
|
iframe.loaded = false |
|
|
|
|
|
iframe.name = 'iframe' |
|
|
|
|
|
iframe.isIframe = true |
|
|
|
|
|
iframe.postMessage = (...args) => iframe.contentWindow.postMessage(...args) |
|
|
|
|
|
iframe.addEventListener('load', () => { |
|
|
|
|
|
iframe.loaded = true |
|
|
|
|
|
}, { once: true }) |
|
|
|
|
|
document.body.appendChild(iframe) |
|
|
|
|
|
return iframe |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* create a popup that simulates the basic things |
|
|
|
|
|
* of what a iframe can do |
|
|
|
|
|
* |
|
|
|
|
|
* @param {string} src page to load |
|
|
|
|
|
* @return {object} iframe like object |
|
|
|
|
|
*/ |
|
|
|
|
|
function makePopup (src) { |
|
|
|
|
|
const options = 'width=200,height=100' |
|
|
|
|
|
const delegate = document.createDocumentFragment() |
|
|
|
|
|
const popup = { |
|
|
|
|
|
frame: window.open(src, 'popup', options), |
|
|
|
|
|
loaded: false, |
|
|
|
|
|
isIframe: false, |
|
|
|
|
|
isPopup: true, |
|
|
|
|
|
remove () { popup.frame.close() }, |
|
|
|
|
|
addEventListener (...args) { delegate.addEventListener(...args) }, |
|
|
|
|
|
dispatchEvent (...args) { delegate.dispatchEvent(...args) }, |
|
|
|
|
|
removeEventListener (...args) { delegate.removeEventListener(...args) }, |
|
|
|
|
|
postMessage (...args) { popup.frame.postMessage(...args) } |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const onReady = evt => { |
|
|
|
|
|
if (evt.source === popup.frame) { |
|
|
|
|
|
popup.loaded = true |
|
|
|
|
|
window.removeEventListener('message', onReady) |
|
|
|
|
|
popup.dispatchEvent(new Event('load')) |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
window.addEventListener('message', onReady) |
|
|
|
|
|
|
|
|
|
|
|
return popup |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
try { |
|
|
|
|
|
// We can't look for service worker since it may still work on http |
|
|
|
|
|
new Response(new ReadableStream()) |
|
|
|
|
|
if (isSecureContext && !('serviceWorker' in navigator)) { |
|
|
|
|
|
useBlobFallback = true |
|
|
|
|
|
} |
|
|
|
|
|
} catch (err) { |
|
|
|
|
|
useBlobFallback = true |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
test(() => { |
|
|
|
|
|
// Transfariable stream was first enabled in chrome v73 behind a flag |
|
|
|
|
|
const { readable } = new TransformStream() |
|
|
|
|
|
const mc = new MessageChannel() |
|
|
|
|
|
mc.port1.postMessage(readable, [readable]) |
|
|
|
|
|
mc.port1.close() |
|
|
|
|
|
mc.port2.close() |
|
|
|
|
|
supportsTransferable = true |
|
|
|
|
|
// Freeze TransformStream object (can only work with native) |
|
|
|
|
|
Object.defineProperty(streamSaver, 'TransformStream', { |
|
|
|
|
|
configurable: false, |
|
|
|
|
|
writable: false, |
|
|
|
|
|
value: TransformStream |
|
|
|
|
|
}) |
|
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
function loadTransporter () { |
|
|
|
|
|
if (!mitmTransporter) { |
|
|
|
|
|
mitmTransporter = isSecureContext |
|
|
|
|
|
? makeIframe(streamSaver.mitm) |
|
|
|
|
|
: makePopup(streamSaver.mitm) |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* @param {string} filename filename that should be used |
|
|
|
|
|
* @param {object} options [description] |
|
|
|
|
|
* @param {number} size depricated |
|
|
|
|
|
* @return {WritableStream} |
|
|
|
|
|
*/ |
|
|
|
|
|
function createWriteStream (filename, options, size) { |
|
|
|
|
|
let opts = { |
|
|
|
|
|
size: null, |
|
|
|
|
|
pathname: null, |
|
|
|
|
|
writableStrategy: undefined, |
|
|
|
|
|
readableStrategy: undefined |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// normalize arguments |
|
|
|
|
|
if (Number.isFinite(options)) { |
|
|
|
|
|
[ size, options ] = [ options, size ] |
|
|
|
|
|
console.warn('[StreamSaver] Depricated pass an object as 2nd argument when creating a write stream') |
|
|
|
|
|
opts.size = size |
|
|
|
|
|
opts.writableStrategy = options |
|
|
|
|
|
} else if (options && options.highWaterMark) { |
|
|
|
|
|
console.warn('[StreamSaver] Depricated pass an object as 2nd argument when creating a write stream') |
|
|
|
|
|
opts.size = size |
|
|
|
|
|
opts.writableStrategy = options |
|
|
|
|
|
} else { |
|
|
|
|
|
opts = options || {} |
|
|
|
|
|
} |
|
|
|
|
|
if (!useBlobFallback) { |
|
|
|
|
|
loadTransporter() |
|
|
|
|
|
|
|
|
|
|
|
var bytesWritten = 0 // by StreamSaver.js (not the service worker) |
|
|
|
|
|
var downloadUrl = null |
|
|
|
|
|
var channel = new MessageChannel() |
|
|
|
|
|
|
|
|
|
|
|
// Make filename RFC5987 compatible |
|
|
|
|
|
filename = encodeURIComponent(filename.replace(/\//g, ':')) |
|
|
|
|
|
.replace(/['()]/g, escape) |
|
|
|
|
|
.replace(/\*/g, '%2A') |
|
|
|
|
|
|
|
|
|
|
|
const response = { |
|
|
|
|
|
transferringReadable: supportsTransferable, |
|
|
|
|
|
pathname: opts.pathname || Math.random().toString().slice(-6) + '/' + filename, |
|
|
|
|
|
headers: { |
|
|
|
|
|
'Content-Type': (options.contentType ? options.contentType : |
|
|
|
|
|
'application/octet-stream; charset=utf-8'), |
|
|
|
|
|
'Content-Disposition': (options.inline ? 'inline' : |
|
|
|
|
|
("attachment; filename*=UTF-8''" + filename)) |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (opts.size) { |
|
|
|
|
|
response.headers['Content-Length'] = opts.size |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const args = [ response, '*', [ channel.port2 ] ] |
|
|
|
|
|
|
|
|
|
|
|
if (supportsTransferable) { |
|
|
|
|
|
const transformer = downloadStrategy === 'iframe' ? undefined : { |
|
|
|
|
|
// This transformer & flush method is only used by insecure context. |
|
|
|
|
|
transform (chunk, controller) { |
|
|
|
|
|
bytesWritten += chunk.length |
|
|
|
|
|
controller.enqueue(chunk) |
|
|
|
|
|
|
|
|
|
|
|
if (downloadUrl) { |
|
|
|
|
|
location.href = downloadUrl |
|
|
|
|
|
downloadUrl = null |
|
|
|
|
|
} |
|
|
|
|
|
}, |
|
|
|
|
|
flush () { |
|
|
|
|
|
if (downloadUrl) { |
|
|
|
|
|
location.href = downloadUrl |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
var ts = new streamSaver.TransformStream( |
|
|
|
|
|
transformer, |
|
|
|
|
|
opts.writableStrategy, |
|
|
|
|
|
opts.readableStrategy |
|
|
|
|
|
) |
|
|
|
|
|
const readableStream = ts.readable |
|
|
|
|
|
|
|
|
|
|
|
channel.port1.postMessage({ readableStream }, [ readableStream ]) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
channel.port1.onmessage = evt => { |
|
|
|
|
|
// Service worker sent us a link that we should open. |
|
|
|
|
|
if (evt.data.download) { |
|
|
|
|
|
// Special treatment for popup... |
|
|
|
|
|
if (downloadStrategy === 'navigate') { |
|
|
|
|
|
mitmTransporter.remove() |
|
|
|
|
|
mitmTransporter = null |
|
|
|
|
|
if (bytesWritten) { |
|
|
|
|
|
location.href = evt.data.download |
|
|
|
|
|
} else { |
|
|
|
|
|
downloadUrl = evt.data.download |
|
|
|
|
|
} |
|
|
|
|
|
} else { |
|
|
|
|
|
if (mitmTransporter.isPopup) { |
|
|
|
|
|
mitmTransporter.remove() |
|
|
|
|
|
// Special case for firefox, they can keep sw alive with fetch |
|
|
|
|
|
if (downloadStrategy === 'iframe') { |
|
|
|
|
|
makeIframe(streamSaver.mitm) |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// We never remove this iframes b/c it can interrupt saving |
|
|
|
|
|
makeIframe(evt.data.download) |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (mitmTransporter.loaded) { |
|
|
|
|
|
mitmTransporter.postMessage(...args) |
|
|
|
|
|
} else { |
|
|
|
|
|
mitmTransporter.addEventListener('load', () => { |
|
|
|
|
|
mitmTransporter.postMessage(...args) |
|
|
|
|
|
}, { once: true }) |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
let chunks = [] |
|
|
|
|
|
|
|
|
|
|
|
return (!useBlobFallback && ts && ts.writable) || new streamSaver.WritableStream({ |
|
|
|
|
|
write (chunk) { |
|
|
|
|
|
if (useBlobFallback) { |
|
|
|
|
|
// Safari... The new IE6 |
|
|
|
|
|
// https://github.com/jimmywarting/StreamSaver.js/issues/69 |
|
|
|
|
|
// |
|
|
|
|
|
// even doe it has everything it fails to download anything |
|
|
|
|
|
// that comes from the service worker..! |
|
|
|
|
|
chunks.push(chunk) |
|
|
|
|
|
return |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// is called when a new chunk of data is ready to be written |
|
|
|
|
|
// to the underlying sink. It can return a promise to signal |
|
|
|
|
|
// success or failure of the write operation. The stream |
|
|
|
|
|
// implementation guarantees that this method will be called |
|
|
|
|
|
// only after previous writes have succeeded, and never after |
|
|
|
|
|
// close or abort is called. |
|
|
|
|
|
|
|
|
|
|
|
// TODO: Kind of important that service worker respond back when |
|
|
|
|
|
// it has been written. Otherwise we can't handle backpressure |
|
|
|
|
|
// EDIT: Transfarable streams solvs this... |
|
|
|
|
|
channel.port1.postMessage(chunk) |
|
|
|
|
|
bytesWritten += chunk.length |
|
|
|
|
|
|
|
|
|
|
|
if (downloadUrl) { |
|
|
|
|
|
location.href = downloadUrl |
|
|
|
|
|
downloadUrl = null |
|
|
|
|
|
} |
|
|
|
|
|
}, |
|
|
|
|
|
close () { |
|
|
|
|
|
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() |
|
|
|
|
|
} else { |
|
|
|
|
|
channel.port1.postMessage('end') |
|
|
|
|
|
} |
|
|
|
|
|
}, |
|
|
|
|
|
abort () { |
|
|
|
|
|
chunks = [] |
|
|
|
|
|
channel.port1.postMessage('abort') |
|
|
|
|
|
channel.port1.onmessage = null |
|
|
|
|
|
channel.port1.close() |
|
|
|
|
|
channel.port2.close() |
|
|
|
|
|
channel = null |
|
|
|
|
|
} |
|
|
|
|
|
}, opts.writableStrategy) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return streamSaver |
|
|
|
|
|
}) |