streamsaver
Advanced tools
Comparing version 1.0.1 to 1.2.0
{ | ||
"name": "streamsaver", | ||
"version": "1.0.1", | ||
"version": "1.2.0", | ||
"description": "StreamSaver writes stream to the filesystem directly - asynchronous", | ||
@@ -5,0 +5,0 @@ "main": "StreamSaver.js", |
@@ -26,4 +26,4 @@ StreamSaver.js | ||
| Chrome 52+ | Yes | | | ||
| Firefox | No | Streams | | ||
| Safari | No | SW | | ||
| Firefox 65+| Yes | | | ||
| Safari | No | download functionality | | ||
| Edge | No | Streams, SW | | ||
@@ -46,3 +46,3 @@ | IE | No | Everything (IE is dead) | | ||
<script src="StreamSaver.js"></script> <!-- load before streams polyfill to detect support --> | ||
<script src="https://cdn.rawgit.com/creatorrr/web-streams-polyfill/master/dist/polyfill.min.js"></script> | ||
<script src="https://unpkg.com/@mattiasbuelens/web-streams-polyfill/dist/polyfill.min.js"></script> | ||
<script> | ||
@@ -79,3 +79,3 @@ // it also support commonJs and amd | ||
``` | ||
That is pretty much all StreamSaver.js dose :) | ||
That is pretty much all StreamSaver.js does :) | ||
@@ -145,4 +145,2 @@ | ||
### Get a "stream" from ajax | ||
res.body is a readableByteStream, but don't have pipeTo yet<br> | ||
So we have to use the reader instead which is the underlying method in streams | ||
@@ -153,5 +151,10 @@ ```javascript | ||
const writer = fileStream.getWriter() | ||
// Later you will be able to just simply do | ||
// res.body.pipeTo(fileStream) | ||
// more optimized | ||
if (res.body.pipeTo) { | ||
// like as we never did fileStream.getWriter() | ||
writer.releaseLock() | ||
return res.body.pipeTo(fileStream) | ||
} | ||
const reader = res.body.getReader() | ||
@@ -257,3 +260,3 @@ const pump = () => reader.read() | ||
[14]: https://streams.spec.whatwg.org/#rs-class | ||
[15]: https://www.npmjs.com/package/web-streams-polyfill | ||
[15]: https://www.npmjs.com/package/@mattiasbuelens/web-streams-polyfill | ||
[16]: https://developer.microsoft.com/en-us/microsoft-edge/platform/status/fetchapi | ||
@@ -260,0 +263,0 @@ [17]: https://developer.microsoft.com/en-us/microsoft-edge/platform/status/serviceworker |
@@ -0,120 +1,168 @@ | ||
/* global location WritableStream ReadableStream define MouseEvent MessageChannel TransformStream */ | ||
;((name, definition) => { | ||
'undefined' != typeof module ? module.exports = definition() : | ||
'function' == typeof define && 'object' == typeof define.amd ? define(definition) : | ||
this[name] = definition() | ||
typeof module !== 'undefined' | ||
? module.exports = definition() | ||
: typeof define === 'function' && typeof define.amd === 'object' | ||
? define(definition) | ||
: this[name] = definition() | ||
})('streamSaver', () => { | ||
'use strict' | ||
'use strict' | ||
let | ||
iframe, loaded, | ||
secure = location.protocol == 'https:' || location.hostname == 'localhost', | ||
streamSaver = { | ||
createWriteStream, | ||
supported: false, | ||
version: { | ||
full: '1.0.0', | ||
major: 1, minor: 0, dot: 0 | ||
} | ||
} | ||
const secure = location.protocol === 'https:' || | ||
location.protocol === 'chrome-extension:' || | ||
location.hostname === 'localhost' | ||
let iframe | ||
let loaded | ||
let transfarableSupport = false | ||
let streamSaver = { | ||
createWriteStream, | ||
supported: false, | ||
version: { | ||
full: '1.2.0', | ||
major: 1, | ||
minor: 2, | ||
dot: 0 | ||
} | ||
} | ||
streamSaver.mitm = 'https://jimmywarting.github.io/StreamSaver.js/mitm.html?version=' + | ||
streamSaver.version.full | ||
streamSaver.mitm = 'https://jimmywarting.github.io/StreamSaver.js/mitm.html?version=' + | ||
streamSaver.version.full | ||
try { | ||
// Some browser has it but ain't allowed to construct a stream yet | ||
streamSaver.supported = 'serviceWorker' in navigator && !!new ReadableStream() && !!new WritableStream() | ||
} catch(err) { | ||
// if you are running chrome < 52 then you can enable it | ||
// `chrome://flags/#enable-experimental-web-platform-features` | ||
} | ||
try { | ||
// Some browser has it but ain't allowed to construct a stream yet | ||
streamSaver.supported = 'serviceWorker' in navigator && !!new ReadableStream() && !!new WritableStream() | ||
} catch (err) {} | ||
function createWriteStream(filename, queuingStrategy, size) { | ||
try { | ||
const { readable } = new TransformStream() | ||
const mc = new MessageChannel() | ||
mc.port1.postMessage(readable, [readable]) | ||
mc.port1.close() | ||
mc.port2.close() | ||
transfarableSupport = readable.locked === true | ||
} catch (err) { | ||
// Was first enabled in chrome v73 | ||
} | ||
// normalize arguments | ||
if (Number.isFinite(queuingStrategy)) | ||
[size, queuingStrategy] = [queuingStrategy, size] | ||
function createWriteStream (filename, queuingStrategy, size) { | ||
// normalize arguments | ||
if (Number.isFinite(queuingStrategy)) { | ||
[size, queuingStrategy] = [queuingStrategy, size] | ||
} | ||
let channel = new MessageChannel, | ||
popup, | ||
setupChannel = () => new Promise((resolve, reject) => { | ||
channel.port1.onmessage = evt => { | ||
if(evt.data.download) { | ||
resolve() | ||
if(!secure) popup.close() // don't need the popup any longer | ||
let link = document.createElement('a') | ||
let click = new MouseEvent('click') | ||
let channel = new MessageChannel() | ||
let popup | ||
let setupChannel = readableStream => new Promise(resolve => { | ||
const args = [ { filename, size }, '*', [ channel.port2 ] ] | ||
link.href = evt.data.download | ||
link.dispatchEvent(click) | ||
} | ||
} | ||
// Pass along transfarable stream | ||
if (readableStream) { | ||
args[0].readableStream = readableStream | ||
args[2].push(readableStream) | ||
} | ||
if(secure && !iframe) { | ||
iframe = document.createElement('iframe') | ||
iframe.src = streamSaver.mitm | ||
iframe.hidden = true | ||
document.body.appendChild(iframe) | ||
} | ||
channel.port1.onmessage = evt => { | ||
// Service worker sent us a link from where | ||
// we recive the readable link (stream) | ||
if (evt.data.download) { | ||
resolve() // Signal that the writestream are ready to recive data | ||
if (!secure) popup.close() // don't need the popup any longer | ||
if (window.chrome && chrome.extension && | ||
chrome.extension.getBackgroundPage && | ||
chrome.extension.getBackgroundPage() === window) { | ||
chrome.tabs.create({ url: evt.data.download, active: false }) | ||
} else { | ||
window.location = evt.data.download | ||
} | ||
if(secure && !loaded) { | ||
let fn; | ||
iframe.addEventListener('load', fn = evt => { | ||
loaded = true | ||
iframe.removeEventListener('load', fn) | ||
iframe.contentWindow.postMessage( | ||
{filename, size}, '*', [channel.port2]) | ||
}) | ||
} | ||
// Cleanup | ||
if (readableStream) { | ||
// We don't need postMessages now when stream are transferable | ||
channel.port1.close() | ||
channel.port2.close() | ||
} | ||
if(secure && loaded) { | ||
iframe.contentWindow.postMessage({filename, size}, '*', [channel.port2]) | ||
} | ||
channel.port1.onmessage = null | ||
} | ||
} | ||
if(!secure) { | ||
popup = window.open(streamSaver.mitm, Math.random()) | ||
let onready = evt => { | ||
if(evt.source === popup){ | ||
popup.postMessage({filename, size}, '*', [channel.port2]) | ||
removeEventListener('message', onready) | ||
} | ||
} | ||
if (secure && !iframe) { | ||
iframe = document.createElement('iframe') | ||
iframe.src = streamSaver.mitm | ||
iframe.hidden = true | ||
document.body.appendChild(iframe) | ||
} | ||
// Another problem that cross origin don't allow is scripting | ||
// so popup.onload() don't work but postMessage still dose | ||
// work cross origin | ||
addEventListener('message', onready) | ||
} | ||
}) | ||
if (secure && !loaded) { | ||
let fn | ||
iframe.addEventListener('load', fn = () => { | ||
loaded = true | ||
iframe.removeEventListener('load', fn) | ||
iframe.contentWindow.postMessage(...args) | ||
}) | ||
} | ||
return new WritableStream({ | ||
start(error) { | ||
// is called immediately, and should perform any actions | ||
// necessary to acquire access to the underlying sink. | ||
// If this process is asynchronous, it can return a promise | ||
// to signal success or failure. | ||
return setupChannel() | ||
}, | ||
write(chunk) { | ||
// 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. | ||
if (secure && loaded) { | ||
iframe.contentWindow.postMessage(...args) | ||
} | ||
// TODO: Kind of important that service worker respond back when | ||
// it has been written. Otherwise we can't handle backpressure | ||
channel.port1.postMessage(chunk) | ||
}, | ||
close() { | ||
channel.port1.postMessage('end') | ||
console.log('All data successfully read!') | ||
}, | ||
abort(e) { | ||
channel.port1.postMessage('abort') | ||
} | ||
}, queuingStrategy) | ||
} | ||
if (!secure) { | ||
popup = window.open(streamSaver.mitm, Math.random()) | ||
let onready = evt => { | ||
if (evt.source === popup) { | ||
popup.postMessage(...args) | ||
window.removeEventListener('message', onready) | ||
} | ||
} | ||
return streamSaver | ||
// Another problem that cross origin don't allow is scripting | ||
// so popup.onload() don't work but postMessage still dose | ||
// work cross origin | ||
window.addEventListener('message', onready) | ||
} | ||
}) | ||
if (transfarableSupport) { | ||
const ts = new TransformStream({ | ||
start () { | ||
return new Promise(resolve => | ||
setTimeout(() => setupChannel(ts.readable).then(resolve)) | ||
) | ||
} | ||
}, queuingStrategy) | ||
return ts.writable | ||
} | ||
return new WritableStream({ | ||
start () { | ||
// is called immediately, and should perform any actions | ||
// necessary to acquire access to the underlying sink. | ||
// If this process is asynchronous, it can return a promise | ||
// to signal success or failure. | ||
return setupChannel() | ||
}, | ||
write (chunk) { | ||
// 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) | ||
}, | ||
close () { | ||
channel.port1.postMessage('end') | ||
}, | ||
abort () { | ||
channel.port1.postMessage('abort') | ||
} | ||
}, queuingStrategy) | ||
} | ||
return streamSaver | ||
}) |
124
sw.js
@@ -1,91 +0,81 @@ | ||
'use strict' | ||
const map = new Map | ||
/* global self ReadableStream Response */ | ||
const map = new Map() | ||
// This should be called once per download | ||
// Each event has a dataChannel that the data will be piped through | ||
self.onmessage = event => { | ||
// Create a uniq link for the download | ||
let uniqLink = self.registration.scope + 'intercept-me-nr' + Math.random() | ||
let port = event.ports[0] | ||
// We send a heartbeat every x secound to keep the | ||
// service worker alive | ||
if (event.data === 'ping') { | ||
return | ||
} | ||
let p = new Promise((resolve, reject) => { | ||
let stream = createStream(resolve, reject, port) | ||
map.set(uniqLink, [stream, event.data]) | ||
port.postMessage({download: uniqLink}) | ||
// Create a uniq link for the download | ||
const uniqLink = self.registration.scope + 'intercept-me-nr' + Math.random() | ||
const port = event.ports[0] | ||
// Mistage adding this and have streamsaver.js rely on it | ||
// depricated as from 0.2.1 | ||
port.postMessage({debug: 'Mocking a download request'}) | ||
}) | ||
const stream = event.data.readableStream || createStream(port) | ||
map.set(uniqLink, [stream, event.data]) | ||
port.postMessage({ download: uniqLink, ping: self.registration.scope + 'ping' }) | ||
// Beginning in Chrome 51, event is an ExtendableMessageEvent, which supports | ||
// the waitUntil() method for extending the lifetime of the event handler | ||
// until the promise is resolved. | ||
if ('waitUntil' in event) { | ||
event.waitUntil(p) | ||
} | ||
// Without support for waitUntil(), there's a chance that if the promise chain | ||
// takes "too long" to execute, the service worker might be automatically | ||
// stopped before it's complete. | ||
// Mistage adding this and have streamsaver.js rely on it | ||
// depricated as from 0.2.1 | ||
port.postMessage({ debug: 'Mocking a download request' }) | ||
} | ||
function createStream(resolve, reject, port){ | ||
// ReadableStream is only supported by chrome 52 | ||
var bytesWritten = 0 | ||
return new ReadableStream({ | ||
start(controller) { | ||
// When we receive data on the messageChannel, we write | ||
port.onmessage = ({data}) => { | ||
if (data === 'end') { | ||
resolve() | ||
return controller.close() | ||
} | ||
function createStream (port) { | ||
// ReadableStream is only supported by chrome 52 | ||
return new ReadableStream({ | ||
start (controller) { | ||
// When we receive data on the messageChannel, we write | ||
port.onmessage = ({ data }) => { | ||
if (data === 'end') { | ||
return controller.close() | ||
} | ||
if (data === 'abort') { | ||
resolve() | ||
controller.error('Aborted the download') | ||
return | ||
} | ||
if (data === 'abort') { | ||
controller.error('Aborted the download') | ||
return | ||
} | ||
controller.enqueue(data) | ||
bytesWritten += data.byteLength | ||
port.postMessage({ bytesWritten }) | ||
} | ||
}, | ||
cancel() { | ||
console.log("user aborted") | ||
} | ||
}) | ||
controller.enqueue(data) | ||
} | ||
}, | ||
cancel () { | ||
console.log('user aborted') | ||
} | ||
}) | ||
} | ||
self.onfetch = event => { | ||
let url = event.request.url | ||
let hijacke = map.get(url) | ||
let listener, filename, headers | ||
const url = event.request.url | ||
console.log("Handleing ", url) | ||
if (url.endsWith('/ping')) { | ||
return event.respondWith(new Response('pong', { | ||
headers: { 'Access-Control-Allow-Origin': '*' } | ||
})) | ||
} | ||
if(!hijacke) return null | ||
const hijacke = map.get(url) | ||
let [stream, data] = hijacke | ||
if (!hijacke) return null | ||
map.delete(url) | ||
const [stream, data] = hijacke | ||
filename = typeof data === 'string' ? data : data.filename | ||
map.delete(url) | ||
// Make filename RFC5987 compatible | ||
filename = encodeURIComponent(filename) | ||
.replace(/['()]/g, escape) | ||
.replace(/\*/g, '%2A') | ||
// Make filename RFC5987 compatible | ||
const filename = encodeURIComponent(typeof data === 'string' ? data : data.filename) | ||
.replace(/['()]/g, escape) | ||
.replace(/\*/g, '%2A') | ||
headers = { | ||
'Content-Type': 'application/octet-stream; charset=utf-8', | ||
'Content-Disposition': "attachment; filename*=UTF-8''" + filename | ||
} | ||
const headers = { | ||
'Content-Type': 'application/octet-stream; charset=utf-8', | ||
'Content-Disposition': "attachment; filename*=UTF-8''" + filename | ||
} | ||
if(data.size) headers['Content-Length'] = data.size | ||
if (data.size) headers['Content-Length'] = data.size | ||
event.respondWith(new Response(stream, { headers })) | ||
event.respondWith(new Response(stream, { headers })) | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
32086
211
262