首次提交

This commit is contained in:
2026-06-16 11:24:50 +08:00
commit 710bf3f9c2
807 changed files with 1046836 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
root = true
[*]
indent_style = tab
indent_size = 4
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2016 Jimmy Karl Roland Wärting
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+156
View File
@@ -0,0 +1,156 @@
StreamSaver.js
==============
[![npm version][npm-image]][npm-url]
First I want to thank [Eli Grey][1] for a fantastic work implementing the
[FileSaver.js][2] to save files & blobs so easily!
But there is one obstacle - The RAM it can hold and the max blob size limitation
StreamSaver.js takes a different approach. Instead of saving data in client-side
storage or in memory you could now actually create a writable stream directly to
the file system (I'm not talking about chromes sandboxed file system or any other
web storage)
StreamSaver.js is the solution to saving streams on the client-side.
It is perfect for webapps that need to save really large amounts of data created
on the client-side, where the RAM is really limited, like on mobile devices.
Getting started
===============
StreamSaver in it's simplest form
```html
<script src="https://cdn.jsdelivr.net/npm/web-streams-polyfill@2.0.2/dist/ponyfill.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/streamsaver@2.0.3/StreamSaver.min.js"></script>
<script>
import streamSaver from 'streamsaver'
const streamSaver = require('streamsaver')
const streamSaver = window.streamSaver
</script>
<script>
const fileStream = streamSaver.createWriteStream('filename.txt', {
size: 22, // (optional) Will show progress
writableStrategy: undefined, // (optional)
readableStrategy: undefined // (optional)
})
new Response('StreamSaver is awesome').body
.pipeTo(fileStream)
.then(success, error)
</script>
```
Some browser have ReadableStream but not WritableStream. [web-streams-polyfill](https://github.com/MattiasBuelens/web-streams-polyfill) can fix this gap. It's better to load the ponyfill instead of the polyfill and override the existing implementation because StreamSaver works better when a native ReadableStream is transferable to the service worker. hopefully [MattiasBuelens](https://github.com/MattiasBuelens) will fix the missing implementations instead of overriding the existing. If you think you can help out here is the [issue](https://github.com/MattiasBuelens/web-streams-polyfill/issues/20)
## Best practice
**Use https** if you can. That way you don't have to open the man in the middle
in a popup to install the service worker from another secure context. Popups are often blocked
but if you can't it's best that you **initiate the `createWriteStream`
on user interaction**. Even if you don't have any data ready - this is so that you can get around the popup blockers. (In secure context this don't matter)
Another benefit of using https is that the mitm-iframe can ping the service worker to prevent it from going idle. (worker goes idle after 30 sec in firefox, 5 minutes in blink) but also this won't mater if the browser supports [transferable streams](https://github.com/whatwg/streams/blob/master/transferable-streams-explainer.md) throught postMessage since service worker don't have to handle any logic. (the stream that you transfer to the service worker will be the stream we respond with)
**Handle unload event** when user leaves the page. The download gets broken when you leave the page.
Because it looks like a regular native download process some might think that it's okey to leave the page beforehand since it's is downloading in the background directly from some a server, but it isn't.
```js
// abort so it dose not look stuck
window.onunload = () => {
writableStream.abort()
// also possible to call abort on the writer you got from `getWriter()`
writer.abort()
}
window.onbeforeunload = evt => {
if (!done) {
evt.returnValue = `Are you sure you want to leave?`;
}
}
```
Note that when using insecure context StreamSaver will navigate to the download url instead of using an hidden iframe to initiate the download, this will trigger the `onbefureunload` event when the download starts, but it will not call the `onunload` event... In secure context you can add this handler immediately. Otherwise this has to be added sometime later.
# Configuration
There a some few settings you can apply to StreamSaver to configure what it should use
```js
// StreamSaver can detect and use the Ponyfill that is loaded from the cdn.
streamSaver.WritableStream = streamSaver.WritableStream
streamSaver.TransformStream = streamSaver.TransformStream
// if you decide to host mitm + sw yourself
streamSaver.mitm = 'https://example.com/custom_mitm.html'
```
Examples
========
There are a few examples in the [examples] directory
- [Saving audio or video stream using mediaRecorder](https://jimmywarting.github.io/StreamSaver.js/examples/media-stream.html)
- [Piping a fetch response to StreamSaver](https://jimmywarting.github.io/StreamSaver.js/examples/fetch.html)
- [Write as you type](https://jimmywarting.github.io/StreamSaver.js/examples/plain-text.html)
- [Saving a blob/file](https://jimmywarting.github.io/StreamSaver.js/examples/saving-a-blob.html)
- [Saving a file using webtorrent](https://jimmywarting.github.io/StreamSaver.js/examples/torrent.html)
- [Saving multiple files as a zip](https://jimmywarting.github.io/StreamSaver.js/examples/saving-multiple-files.html)
- [slowly write 1 byte / sec](https://jimmywarting.github.io/StreamSaver.js/examples/write-slowly.html)
In the wild
- [Adding ID3 tag to mp3 file on the fly](https://egoroof.ru/browser-id3-writer/stream) - by [Artyom Egorov](https://github.com/egoroof)
How dose it work?
=====================
There is no magical `saveAs()` function that saves a stream, file or blob. (at least not if/when native-filesystem api becomes avalible)
The way we mostly save Blobs/Files today is with the help of [Object URLs](https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL) and [`a[download]`][5] attribute
[FileSaver.js][2] takes advantage of this and create a convenient `saveAs(blob, filename)`. fantastic! But you can't create a objectUrl from a stream and attach
it to a link...
```javascript
link = document.createElement('a')
link.href = URL.createObjectURL(stream) // DOES NOT WORK
link.download = 'filename'
link.click() // Save
```
So the one and only other solution is to do what the server does: Send a stream
with `Content-Disposition` header to tell the browser to save the file.
But we don't have a server or the content isn't on a server! So the solution is to create a service worker
that can intercept request and use [respondWith()][4] and act as a server.<br>
But a service workers are only allowed in secure contexts and it requires some effort to put up. Most of the time you are working in the main thread and the service worker are only alive for < 5 minutes before it goes idle.<br>
1. So StreamSaver creates a own man in the middle that installs the service worker in a secure context hosted on github static pages. either from a iframe (in secure context) or a new popup if your page is insecure.
2. Transfer the stream (or DataChannel) over to the service worker using postMessage.
3. And then the worker creates a download link that we then open.
if a "transferable" readable stream was not passed to the service worker then the mitm will also try to keep the service worker alive by pinging it every x second to prevent it from going idle.
To test this locally, spin up a local server<br>
(we don't use any pre compiler or such)
```bash
# A simple php or python server is enough
php -S localhost:3001
python -m SimpleHTTPServer 3001
# then open localhost:3001/example.html
```
[1]: https://github.com/eligrey
[2]: https://github.com/eligrey/FileSaver.js
[3]: https://github.com/jimmywarting/StreamSaver.js/blob/master/example.html
[4]: https://developer.mozilla.org/en-US/docs/Web/API/FetchEvent/respondWith
[5]: https://developer.mozilla.org/en/docs/Web/HTML/Element/a#attr-download
[6]: https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API
[7]: https://developer.mozilla.org/en-US/docs/Web/API/MessageChannel
[8]: https://developer.mozilla.org/en-US/docs/Web/API/MessagePort/postMessage
[9]: https://developer.mozilla.org/en/docs/Web/API/Fetch_API
[10]: https://developer.mozilla.org/en-US/docs/Web/API/FetchEvent/respondWith
[11]: https://developer.mozilla.org/en/docs/Web/HTML/Element/iframe
[12]: https://developer.mozilla.org/en-US/docs/Web/API/Window/open
[13]: https://developer.mozilla.org/en-US/docs/Web/API/Response
[14]: https://streams.spec.whatwg.org/#rs-class
[ReadableStream]: https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream
[WritableStream]: https://developer.mozilla.org/en-US/docs/Web/API/WritableStream
[15]: https://www.npmjs.com/package/@mattiasbuelens/web-streams-polyfill
[16]: https://developer.microsoft.com/en-us/microsoft-edge/platform/status/fetchapi
[19]: https://webtorrent.io
[examples]: https://github.com/jimmywarting/StreamSaver.js/blob/master/examples
[npm-image]: https://img.shields.io/npm/v/streamsaver.svg?style=flat-square
[npm-url]: https://www.npmjs.com/package/streamsaver
+304
View File
@@ -0,0 +1,304 @@
/* 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'
let 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'
mitm:""
};
fetch("./scripts/StreamSaver/mitm.html",{
method:'GET',
}).then(res=>{
streamSaver.mitm=res.url;
})
/**
* 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': 'application/octet-stream; charset=utf-8',
'Content-Disposition': "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
})
+28
View File
@@ -0,0 +1,28 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title>StreamSaver example codes</title>
</head>
<body>
<a class="github-corner" target="_blank" href="https://github.com/jimmywarting/StreamSaver.js">
<svg width="100" height="100" viewbox="0 0 250 250" style="fill:#151513; color:#fff; position: absolute; top: 0; border: 0; right: 0;">
<path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path>
<path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path><path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path>
<text class="github-corner-text" text-anchor="middle" x="175" y="25" transform="rotate(45)" font-size="30" font-weight="bold">Fork me on Github</text>
</svg>
<style>.github-corner:hover .octo-arm{animation: octocat-wave 560ms ease-in-out}@keyframes octocat-wave{0%, 100%{transform: rotate(0)}20%, 60%{transform: rotate(-25deg)}40%, 80%{transform: rotate(10deg)}}@media (max-width: 500px){.github-corner:hover .octo-arm{animation: none}.github-corner .octo-arm{animation: octocat-wave 560ms ease-in-out}.github-corner-text{display: none;}.github-corner svg{height: 50px; width: 50px;}}</style>
</a>
<h2>Example of saving a stream directly to the filesystem</h2>
<ul>
<li><a href="examples/media-stream.html">Saving audio or video stream using mediaRecorder</a>
<li><a href="examples/fetch.html">Piping a fetch response to StreamSaver</a>
<li><a href="examples/plain-text.html">Write as you type</a>
<li><a href="examples/torrent.html">Saving a file using webtorrent</a>
<li><a href="examples/saving-a-blob.html">Saving one Blob (File)</a>
<li><a href="examples/saving-multiple-files.html">Saving multiple files as a zip</a>
<li><a href="examples/write-slowly.html">Slowly write 1 byte / sec</a>
</ul>
</body>
</html>
@@ -0,0 +1,39 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<button id="$start">Start</button>
<script src="https://cdn.jsdelivr.net/npm/web-streams-polyfill@2.0.2/dist/ponyfill.min.js"></script>
<script src="../StreamSaver.js"></script>
<script>
$start.onclick = () => {
const url = 'https://d8d913s460fub.cloudfront.net/videoserver/cat-test-video-320x240.mp4'
const fileStream = streamSaver.createWriteStream('cat.mp4')
fetch(url).then(res => {
const readableStream = res.body
// more optimized
if (window.WritableStream && readableStream.pipeTo) {
return readableStream.pipeTo(fileStream)
.then(() => console.log('done writing'))
}
window.writer = fileStream.getWriter()
const reader = res.body.getReader()
const pump = () => reader.read()
.then(res => res.done
? writer.close()
: writer.write(res.value).then(pump))
pump()
})
}
</script>
</body>
</html>
@@ -0,0 +1,76 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<p>
This will use the userMedia audio and or video to get a stream. <br>
It will then use mediaRecorder to "pipe" the data to StreamSaver (aka hard drive)
</p>
<p>
Note: This is only allowed in
<a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/isSecureContext">secure web context</a>
</p>
<label><input id="$vid" type="checkbox"> Use Webcam</label>
<label><input id="$aud" type="checkbox"> Use Microphone</label>
<button id="$start">Start</button>
<script src="https://cdn.jsdelivr.net/webtorrent/latest/webtorrent.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/web-streams-polyfill@2.0.2/dist/ponyfill.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/jimmywarting/browser-su@master/build/permissions.js"></script>
<script src="../StreamSaver.js"></script>
<script>
$vid.disabled = $aud.disabled = $start.disabled = !window.isSecureContext
$start.onclick = () => {
permission = { name: 'userMedia', video: $vid.checked, audio: $aud.checked }
su.request(permission).then(stream => {
let fr = new FileReader()
let mediaRecorder = new MediaRecorder(stream)
let chunks = Promise.resolve()
mediaRecorder.start()
$close.onclick = event => {
stopStream(stream)
mediaRecorder.stop()
setTimeout(()=>{
chunks.then(evt => {
fileStream.close()
})
}, 1000)
}
mediaRecorder.ondataavailable = evt => {
let blob = evt.data
chunks = chunks.then(() => new Promise(resolve => {
fr.onload = () => {
// Should we let the serviceWorker be able to accept
// anything other then uint8array? ReadableStream don't seems
// so happy with anything else... but could load of some work
// of the main thread +1
let uint8array = new Uint8Array(fr.result)
myFile.write(uint8array)
resolve()
}
fr.readAsArrayBuffer(blob)
}))
}
})
}
function stopStream (stream) {
let tracks = [
...stream.getAudioTracks(),
...stream.getVideoTracks()
]
for(let track of tracks)
track.stop()
}
</script>
</body>
</html>
@@ -0,0 +1,102 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<script src="https://cdn.jsdelivr.net/webtorrent/latest/webtorrent.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/web-streams-polyfill@2.0.2/dist/ponyfill.min.js"></script>
<script src="../StreamSaver.js"></script>
<p>1) Set a filename</p>
<input type="text" id="$filename" value="sample.txt">
<hr>
<p>2) Write some data</p>
<button id="$a">Write some aaa's</button>
<button id="$b">Write some bbb's</button>
<button id="$c">Write some ccc's</button>
<button id="$ipsum">Write a lot of Lorem ipsum</button><br>
<input type="text" id="$custom" placeholder="custom text">
(may look like nothing is happening, but writes all keystorke to fileStream)
<hr>
<p>3) Abort for cancel, close for ending the write stream</p>
<button disabled id="$abort">Abort</button>
<button disabled id="$close">Close</button>
<script>
var Lorem = `Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Pellentesque gravida condimentum metus et porttitor. Curabitur
pharetra vestibulum egestas. Pellentesque quis tortor id ligula
cursus luctus ac at nisi. Mauris rutrum mattis vulputate. Donec
tempor eget lectus eu rhoncus. Etiam et auctor est. Aenean sem augue,
consectetur et ipsum fringilla, rhoncus tincidunt sem. Duis vel
rutrum lectus, non dui. Duis non urna non dolor elementum
commodo. Praesent commodo maximus lobortis. Curabitur fringilla
tellus`.replace(/\s\s+/g, ' ')
var fileStream, writer
var encode = TextEncoder.prototype.encode.bind(new TextEncoder)
let text = encode((Lorem + "\n\n").repeat(2*1024)) // 1 MiB
let a = new Uint8Array(1024).fill(97)
let b = new Uint8Array(1024).fill(98)
let c = new Uint8Array(1024).fill(99)
// Abort the download stream when leaving the page
window.isSecureContext && window.addEventListener('beforeunload', evt => {
writer.abort()
})
$abort.onclick = () => {
writer.abort()
document.body.innerHTML = '<a href="./plain-text.html">Try again</a>'
}
$close.onclick = () => {
writer.close()
document.body.innerHTML = '<a href="./plain-text.html">Try again</a>'
}
$a.onclick = $b.onclick = $c.onclick = $ipsum.onclick = $custom.oninput = evt => {
if (evt.target === $ipsum) {
var n = ~~prompt("How many MiB of lorem ipsum text do you want?", '1024')
}
if (!fileStream) {
fileStream = streamSaver.createWriteStream($filename.value || 'sample.txt')
writer = fileStream.getWriter()
$filename.disabled = true
$abort.disabled = $close.disabled = false
}
var data
if (evt.target === $a) data = a
if (evt.target === $b) data = b
if (evt.target === $c) data = c
if (evt.target === $custom) data = encode($custom.value)
$custom.value = ''
data && writer.write(data)
if (evt.target === $ipsum) {
let que = Promise.resolve()
let pump = () => {
n-- && que.then(() => {
writer.write(text).then(()=>{setTimeout(pump)})
})
}
pump()
}
}
</script>
</body>
</html>
@@ -0,0 +1,50 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<button id="$start">Start</button>
<script src="https://cdn.jsdelivr.net/npm/web-streams-polyfill@2.0.2/dist/ponyfill.min.js"></script>
<!-- includes blob.stream() polyfill -->
<script src="https://cdn.jsdelivr.net/gh/eligrey/Blob.js/Blob.js"></script>
<script src="../StreamSaver.js"></script>
<script>
// Saving a blob is as simple as the fetch example, you just get the
// readableStream from the blob by calling blob.stream() to get a
// readableStream and then pipe it
$start.onclick = () => {
const blob = new Blob(['StreamSaver is awesome'])
const fileStream = streamSaver.createWriteStream('sample.txt', {
size: blob.size // Makes the procentage visiable in the download
})
// One quick alternetive way if you don't want the hole blob.js thing:
// const readableStream = new Response(
// Blob || String || ArrayBuffer || ArrayBufferView
// ).body
const readableStream = blob.stream()
// more optimized pipe version
// (Safari may have pipeTo but it's useless without the WritableStream)
if (window.WritableStream && readableStream.pipeTo) {
return readableStream.pipeTo(fileStream)
.then(() => console.log('done writing'))
}
// Write (pipe) manually
window.writer = fileStream.getWriter()
const reader = readableStream.getReader()
const pump = () => reader.read()
.then(res => res.done
? writer.close()
: writer.write(res.value).then(pump))
pump()
}
</script>
</body>
</html>
@@ -0,0 +1,97 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<button id="$start">Start</button>
<script src="https://cdn.jsdelivr.net/npm/web-streams-polyfill@2.0.2/dist/ponyfill.min.js"></script>
<!--
includes blob.stream() polyfill
while Also making File constructor work in some browser that don't support it
-->
<script src="https://cdn.jsdelivr.net/gh/eligrey/Blob.js/Blob.js"></script>
<script src="../StreamSaver.js"></script>
<script src="zip-stream.js"></script>
<script>
$start.onclick = () => {
const fileStream = streamSaver.createWriteStream('archive.zip')
const file1 = new File(['file1 content'], '/streamsaver-zip-example/file1.txt')
// File Like object works too
const file2 = {
name: '/streamsaver-zip-example/file2.txt',
stream () {
// if you want to play it cool and use new api's
//
// const { readable, writable } = new TextEncoderStream()
// writable.write('file2 content')
// return readable
return new ReadableStream({
start (ctrl) {
ctrl.enqueue(new TextEncoder().encode('file2 generated with readableStream'))
ctrl.close()
}
})
}
}
const blob = new Blob(['support blobs too'])
const file3 = {
name: '/streamsaver-zip-example/blob-example.txt',
stream: () => blob.stream()
}
// In a ideall world i would just have used a TransformStream
// where you would get `{ readable writable } = new TransformStream()`
// `readable` would be piped to streamsaver, and the writer would accept
// file-like object, but that made it dependent on TransformStream and WritableStream
// So i built ZIP-Stream simular to a ReadbleStream but you enqueue
// file-like objects meaning it should have at at the very least { name, stream() }
//
// it supports pull() too that gets called when it ask for more files.
//
// NOTE: My zip library can't generate zip's over 4gb and has no compresseion
// it was built solo for the purpus of saving multiple files in browser
const readableZipStream = new ZIP({
start (ctrl) {
ctrl.enqueue(file1)
ctrl.enqueue(file2)
ctrl.enqueue(file3)
ctrl.enqueue({name: '/streamsaver-zip-example/empty folder', directory: true})
// ctrl.close()
},
async pull (ctrl) {
const url = 'https://d8d913s460fub.cloudfront.net/videoserver/cat-test-video-320x240.mp4'
const res = await fetch(url)
const stream = () => res.body
const name = '/streamsaver-zip-example/cat.mp4'
ctrl.enqueue({ name, stream })
ctrl.close()
}
})
// more optimized
if (window.WritableStream && readableZipStream.pipeTo) {
return readableZipStream.pipeTo(fileStream).then(() => console.log('done writing'))
}
// less optimized
window.writer = fileStream.getWriter()
const reader = readableZipStream.getReader()
const pump = () => reader.read()
.then(res => res.done ? writer.close() : writer.write(res.value).then(pump))
pump()
}
</script>
</body>
</html>
@@ -0,0 +1,84 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<p>
This download the <a href="https://webtorrent.io/torrents/sintel.torrent">Sintel torrent</a>
using HTTP, WebRTC and WebSeed.<br>Using a combination of node streams and whatwg streams
</p>
<button id="$start">Start</button>
<script src="https://cdn.jsdelivr.net/webtorrent/latest/webtorrent.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/web-streams-polyfill@2.0.2/dist/ponyfill.min.js"></script>
<script src="../StreamSaver.js"></script>
<script>
function size (bytes, precision) {
if (isNaN(parseFloat(bytes)) || !isFinite(bytes)) return '-';
if (typeof precision === 'undefined') precision = 1;
var units = ['bytes', 'kiB', 'MiB', 'GiB', 'TiB', 'PiB'],
number = Math.floor(Math.log(bytes) / Math.log(1024));
return (bytes / Math.pow(1024, Math.floor(number))).toFixed(precision) + ' ' + units[number];
}
const client = window.client = new WebTorrent()
// Sintel, a free, Creative Commons movie
const torrentId = 'https://webtorrent.io/torrents/sintel.torrent'
$start.onclick = () => {
document.body.innerHTML = '<p id="$info">Downloading Torrent-file metadata</p>'
// PS: If you are using insecure sites you better create the writestream with a user interaction event.
// with https it doesn't matther.
window.fileStream = streamSaver.createWriteStream('Sintel.mp4', {
size: 129241752,
// writableStrategy: new ByteLengthQueuingStrategy({ highWaterMark: 1024000 }),
// readableStrategy: new ByteLengthQueuingStrategy({ highWaterMark: 1024000 })
})
window.writer = fileStream.getWriter()
client.add(torrentId, torrent => {
$info.remove()
const meter = document.createElement('meter')
const speed = document.createElement('p')
const downloaded = document.createElement('p')
const timeLeft = document.createElement('p')
const writerSize = document.createElement('p')
document.body.appendChild(meter)
document.body.appendChild(speed)
document.body.appendChild(downloaded)
document.body.appendChild(timeLeft)
document.body.appendChild(writerSize)
writerSize.innerText = `writer.desiredSize = ${writer.desiredSize}`
torrent.on('download', function (bytes) {
downloaded.innerText = `total downloaded: ${size(torrent.downloaded)} of ${size(torrent.length)}`
speed.innerText = 'download speed: ' + size(torrent.downloadSpeed)
timeLeft.innerText = 'Time Left: ' + (torrent.timeRemaining / 1000).toFixed(0) + ' sec'
meter.value = torrent.progress
})
const file = torrent.files[5]
// Unfortunately we have two different stream protocol so we can't pipe.
file.createReadStream()
.on('data', data => {
writer.write(data).then(() => {
writerSize.innerText = `writer.desiredSize = ${(writer.desiredSize)}`
})
writerSize.innerText = `writer.desiredSize = ${(writer.desiredSize)}`
})
.on('end', () => writer.close())
})
}
</script>
</body>
</html>
@@ -0,0 +1,108 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title>Write bytes slowly</title>
</head>
<body>
<p>
This test will write <input type="text" value="a" id="$val"> every second until it has written it <input id="$num" type="number" max="9999" value="1024"> times to test wheter or not<br>
We need to try and keep the service worker alive. Simply passing a stream over won't need keep alive techniques<br>
Make sure you don't have the developer tool open b/c it can prevent service
worker from restarting
</p>
<input type="checkbox" id="$tra" disabled> Using Transfariable ReadableStream<br>
<input type="checkbox" id="$mes" disabled> Using MessageChannel as stream (postMessage)<br>
<input type="checkbox" id="$wor" disabled> Using techniques (postMessage or fetch) to keep sw alive<br>
<input type="checkbox" id="$sec" disabled> Using a secure web context<br>
<input type="checkbox" id="$ifr" disabled> Using hidden iframe to download<br>
<input type="checkbox" id="$pop" disabled> Using popup to install sw<br>
<input type="checkbox" id="$loc" disabled> Using "location.href" to download`<br>
<input type="checkbox" id="$cro" disabled> Using cross origin service worker<br>
<br>
Choose a filename
<input id="$nam" value="sample.txt">
<br>
<button id="$start">Start</button>
<span id="$written"></span>
<br><br>
<label>
<input id="$not" type="checkbox"> enable desktop notification when finish
</label>
<br>
<label>
<input id="$sou" type="checkbox" name="wtf"> play sound when finish
</label>
<script src='https://code.responsivevoice.org/responsivevoice.js'></script>
<script src="https://cdn.jsdelivr.net/npm/web-streams-polyfill@2.0.2/dist/ponyfill.min.js"></script>
<script src="../StreamSaver.js"></script>
<script>
if ('isSecureContext' in window) {
$ifr.checked = $sec.checked = isSecureContext
$pop.checked = !isSecureContext
} else {
$sec.indeterminate = true
}
$loc.checked = !$ifr.checked
$cro.checked = new URL(streamSaver.mitm).origin !== window.origin
try {
const { readable } = new TransformStream()
const mc = new MessageChannel()
mc.port1.postMessage(readable, [readable])
mc.port1.close()
mc.port2.close()
$tra.checked = true
} catch (e) {
$mes.checked = true
$wor.checked = true
}
if (Notification.permission !== 'granted') {
$not.onchange = () => Notification.requestPermission().then(console.log, console.log)
}
$start.onclick = () => {
const max = $num.valueAsNumber
const progress = document.createElement('progress')
const byte = new TextEncoder().encode($val.value)
const start = Date.now()
$num.disabled = true
progress.max = max
progress.value = 0
$start.replaceWith(progress)
window.fileStream = streamSaver.createWriteStream($nam.value, { size: max * byte.length })
window.writer = fileStream.getWriter()
window.onunload = () => writer.abort()
$nam.disabled = $val.disabled = $num.disabled = true
writer.write(byte)
let i = 1
const interval = setInterval(() => {
writer.write(byte)
i++
progress.value = i
$written.innerText = (i * byte.length) + ' bytes written'
if (i === max) {
$sou.checked && responsiveVoice.speak('Download completed')
writer.close()
clearInterval(interval)
}
}, 1000)
}
</script>
</body>
</html>
@@ -0,0 +1,198 @@
class Crc32 {
constructor () {
this.crc = -1
}
append (data) {
var crc = this.crc | 0; var table = this.table
for (var offset = 0, len = data.length | 0; offset < len; offset++) {
crc = (crc >>> 8) ^ table[(crc ^ data[offset]) & 0xFF]
}
this.crc = crc
}
get () {
return ~this.crc
}
}
Crc32.prototype.table = (() => {
var i; var j; var t; var table = []
for (i = 0; i < 256; i++) {
t = i
for (j = 0; j < 8; j++) {
t = (t & 1)
? (t >>> 1) ^ 0xEDB88320
: t >>> 1
}
table[i] = t
}
return table
})()
const getDataHelper = byteLength => {
var uint8 = new Uint8Array(byteLength)
return {
array: uint8,
view: new DataView(uint8.buffer)
}
}
const pump = zipObj => zipObj.reader.read().then(chunk => {
if (chunk.done) return zipObj.writeFooter()
const outputData = chunk.value
zipObj.crc.append(outputData)
zipObj.uncompressedLength += outputData.length
zipObj.compressedLength += outputData.length
zipObj.ctrl.enqueue(outputData)
})
/**
* [createWriter description]
* @param {Object} underlyingSource [description]
* @return {Boolean} [description]
*/
function createWriter (underlyingSource) {
const files = Object.create(null)
const filenames = []
const encoder = new TextEncoder()
let offset = 0
let activeZipIndex = 0
let ctrl
let activeZipObject, closed
function next () {
activeZipIndex++
activeZipObject = files[filenames[activeZipIndex]]
if (activeZipObject) processNextChunk()
else if (closed) closeZip()
}
var zipWriter = {
enqueue (fileLike) {
if (closed) throw new TypeError('Cannot enqueue a chunk into a readable stream that is closed or has been requested to be closed')
let name = fileLike.name.trim()
const date = new Date(typeof fileLike.lastModified === 'undefined' ? Date.now() : fileLike.lastModified)
if (fileLike.directory && !name.endsWith('/')) name += '/'
if (files[name]) throw new Error('File already exists.')
const nameBuf = encoder.encode(name)
filenames.push(name)
const zipObject = files[name] = {
level: 0,
ctrl,
directory: !!fileLike.directory,
nameBuf,
comment: encoder.encode(fileLike.comment || ''),
compressedLength: 0,
uncompressedLength: 0,
writeHeader () {
var header = getDataHelper(26)
var data = getDataHelper(30 + nameBuf.length)
zipObject.offset = offset
zipObject.header = header
if (zipObject.level !== 0 && !zipObject.directory) {
header.view.setUint16(4, 0x0800)
}
header.view.setUint32(0, 0x14000808)
header.view.setUint16(6, (((date.getHours() << 6) | date.getMinutes()) << 5) | date.getSeconds() / 2, true)
header.view.setUint16(8, ((((date.getFullYear() - 1980) << 4) | (date.getMonth() + 1)) << 5) | date.getDate(), true)
header.view.setUint16(22, nameBuf.length, true)
data.view.setUint32(0, 0x504b0304)
data.array.set(header.array, 4)
data.array.set(nameBuf, 30)
offset += data.array.length
ctrl.enqueue(data.array)
},
writeFooter () {
var footer = getDataHelper(16)
footer.view.setUint32(0, 0x504b0708)
if (zipObject.crc) {
zipObject.header.view.setUint32(10, zipObject.crc.get(), true)
zipObject.header.view.setUint32(14, zipObject.compressedLength, true)
zipObject.header.view.setUint32(18, zipObject.uncompressedLength, true)
footer.view.setUint32(4, zipObject.crc.get(), true)
footer.view.setUint32(8, zipObject.compressedLength, true)
footer.view.setUint32(12, zipObject.uncompressedLength, true)
}
ctrl.enqueue(footer.array)
offset += zipObject.compressedLength + 16
next()
},
fileLike
}
if (!activeZipObject) {
activeZipObject = zipObject
processNextChunk()
}
},
close () {
if (closed) throw new TypeError('Cannot close a readable stream that has already been requested to be closed')
if (!activeZipObject) closeZip()
closed = true
}
}
function closeZip () {
var length = 0
var index = 0
var indexFilename, file
for (indexFilename = 0; indexFilename < filenames.length; indexFilename++) {
file = files[filenames[indexFilename]]
length += 46 + file.nameBuf.length + file.comment.length
}
const data = getDataHelper(length + 22)
for (indexFilename = 0; indexFilename < filenames.length; indexFilename++) {
file = files[filenames[indexFilename]]
data.view.setUint32(index, 0x504b0102)
data.view.setUint16(index + 4, 0x1400)
data.array.set(file.header.array, index + 6)
data.view.setUint16(index + 32, file.comment.length, true)
if (file.directory) {
data.view.setUint8(index + 38, 0x10)
}
data.view.setUint32(index + 42, file.offset, true)
data.array.set(file.nameBuf, index + 46)
data.array.set(file.comment, index + 46 + file.nameBuf.length)
index += 46 + file.nameBuf.length + file.comment.length
}
data.view.setUint32(index, 0x504b0506)
data.view.setUint16(index + 8, filenames.length, true)
data.view.setUint16(index + 10, filenames.length, true)
data.view.setUint32(index + 12, length, true)
data.view.setUint32(index + 16, offset, true)
ctrl.enqueue(data.array)
ctrl.close()
}
function processNextChunk () {
if (!activeZipObject) return
if (activeZipObject.directory) return activeZipObject.writeFooter(activeZipObject.writeHeader())
if (activeZipObject.reader) return pump(activeZipObject)
if (activeZipObject.fileLike.stream) {
activeZipObject.crc = new Crc32()
activeZipObject.reader = activeZipObject.fileLike.stream().getReader()
activeZipObject.writeHeader()
} else next()
}
return new ReadableStream({
start: c => {
ctrl = c
underlyingSource.start && Promise.resolve(underlyingSource.start(zipWriter))
},
pull () {
return processNextChunk() || (
underlyingSource.pull &&
Promise.resolve(underlyingSource.pull(zipWriter))
)
}
})
}
window.ZIP = createWriter
+174
View File
@@ -0,0 +1,174 @@
<!--
mitm.html is the lite "man in the middle"
This is only meant to signal the opener's messageChannel to
the service worker - when that is done this mitm can be closed
but it's better to keep it alive since this also stops the sw
from restarting
The service worker is capable of intercepting all request and fork their
own "fake" response - wish we are going to craft
when the worker then receives a stream then the worker will tell the opener
to open up a link that will start the download
-->
<script>
// This will prevent the sw from restarting
let keepAlive = () => {
keepAlive = () => {}
var ping = location.href.substr(0, location.href.lastIndexOf('/')) + '/ping'
var interval = setInterval(() => {
console.log(sw);
if (sw) {
sw.postMessage('ping')
} else {
// fetch(ping).then(res => res.text(!res.ok && clearInterval(interval)))
fetch(ping).then(res =>{
console.log("打印:"+res);
res.text(!res.ok&&clearInterval(interval))
})
}
}, 10000)
}
// message event is the first thing we need to setup a listner for
// don't want the opener to do a random timeout - instead they can listen for
// the ready event
// but since we need to wait for the Service Worker registration, we store the
// message for later
let messages = []
window.onmessage = evt => messages.push(evt)
let sw = null
let scope = ''
function registerWorker() {
return navigator.serviceWorker.getRegistration('./').then(swReg => {
return swReg || navigator.serviceWorker.register('sw.js', { scope: './' })
}).then(swReg => {
console.log(swReg);
const swRegTmp = swReg.installing || swReg.waiting
scope = swReg.scope
return (sw = swReg.active) || new Promise(resolve => {
swRegTmp.addEventListener('statechange', fn = () => {
if (swRegTmp.state === 'activated') {
swRegTmp.removeEventListener('statechange', fn)
sw = swReg.active
resolve()
}
})
})
})
}
// Now that we have the Service Worker registered we can process messages
function onMessage (event) {
let { data, ports, origin } = event
// It's important to have a messageChannel, don't want to interfere
// with other simultaneous downloads
if (!ports || !ports.length) {
throw new TypeError("[StreamSaver] You didn't send a messageChannel")
}
if (typeof data !== 'object') {
throw new TypeError("[StreamSaver] You didn't send a object")
}
// the default public service worker for StreamSaver is shared among others.
// so all download links needs to be prefixed to avoid any other conflict
data.origin = origin
// if we ever (in some feature versoin of streamsaver) would like to
// redirect back to the page of who initiated a http request
data.referrer = data.referrer || document.referrer || origin
// pass along version for possible backwards compatibility in sw.js
data.streamSaverVersion = new URLSearchParams(location.search).get('version')
if (data.streamSaverVersion === '1.2.0') {
console.warn('[StreamSaver] please update streamsaver')
}
/** @since v2.0.0 */
if (!data.headers) {
console.warn("[StreamSaver] pass `data.headers` that you would like to pass along to the service worker\nit should be a 2D array or a key/val object that fetch's Headers api accepts")
} else {
// test if it's correct
// should thorw a typeError if not
new Headers(data.headers)
}
/** @since v2.0.0 */
if (typeof data.filename === 'string') {
console.warn("[StreamSaver] You shouldn't send `data.filename` anymore. It should be included in the Content-Disposition header option")
// Do what File constructor do with fileNames
data.filename = data.filename.replace(/\//g, ':')
}
/** @since v2.0.0 */
if (data.size) {
console.warn("[StreamSaver] You shouldn't send `data.size` anymore. It should be included in the content-length header option")
}
/** @since v2.0.0 */
if (data.readableStream) {
console.warn("[StreamSaver] You should send the readableStream in the messageChannel, not throught mitm")
}
/** @since v2.0.0 */
if (!data.pathname) {
console.warn("[StreamSaver] Please send `data.pathname` (eg: /pictures/summer.jpg)")
data.pathname = Math.random().toString().slice(-6) + '/' + data.filename
}
// remove all leading slashes
data.pathname = data.pathname.replace(/^\/+/g, '')
// remove protocol
let org = origin.replace(/(^\w+:|^)\/\//, '')
// set the absolute pathname to the download url.
data.url = new URL(`${scope + org}/${data.pathname}`).toString()
if (!data.url.startsWith(`${scope + org}/`)) {
throw new TypeError('[StreamSaver] bad `data.pathname`')
}
// This sends the message data as well as transferring
// messageChannel.port2 to the service worker. The service worker can
// then use the transferred port to reply via postMessage(), which
// will in turn trigger the onmessage handler on messageChannel.port1.
const transferable = data.readableStream
? [ ports[0], data.readableStream ]
: [ ports[0] ]
if (!(data.readableStream || data.transferringReadable)) {
keepAlive()
}
return sw.postMessage(data, transferable)
}
if (window.opener) {
// The opener can't listen to onload event, so we need to help em out!
// (telling them that we are ready to accept postMessage's)
window.opener.postMessage('StreamSaver::loadedPopup', '*')
}
if (navigator.serviceWorker) {
registerWorker().then(() => {
window.onmessage = onMessage
messages.forEach(window.onmessage)
})
} else {
// FF can ping sw with fetch from a secure hidden iframe
// shouldn't really be possible?
keepAlive()
}
</script>
+27
View File
@@ -0,0 +1,27 @@
{
"name": "streamsaver",
"version": "2.0.3",
"description": "StreamSaver writes stream to the filesystem directly - asynchronous",
"main": "StreamSaver.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 0"
},
"repository": {
"type": "git",
"url": "git+https://jimmywarting@github.com/jimmywarting/StreamSaver.js.git"
},
"keywords": [
"saving",
"streams",
"stream",
"html5",
"file",
"blob"
],
"author": "Jimmy Wärting <jimmy@warting.se>",
"license": "MIT",
"bugs": {
"url": "https://github.com/jimmywarting/StreamSaver.js/issues"
},
"homepage": "https://github.com/jimmywarting/StreamSaver.js#readme"
}
+128
View File
@@ -0,0 +1,128 @@
/* global self ReadableStream Response */
self.addEventListener('install', () => {
self.skipWaiting()
})
self.addEventListener('activate', event => {
event.waitUntil(self.clients.claim())
})
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 => {
// We send a heartbeat every x secound to keep the
// service worker alive if a transferable stream is not sent
if (event.data === 'ping') {
return
}
const data = event.data
const downloadUrl = data.url || self.registration.scope + Math.random() + '/' + (typeof data === 'string' ? data : data.filename)
const port = event.ports[0]
const metadata = new Array(3) // [stream, data, port]
metadata[1] = data
metadata[2] = port
// Note to self:
// old streamsaver v1.2.0 might still use `readableStream`...
// but v2.0.0 will always transfer the stream throught MessageChannel #94
if (event.data.readableStream) {
metadata[0] = event.data.readableStream
} else if (event.data.transferringReadable) {
port.onmessage = evt => {
port.onmessage = null
metadata[0] = evt.data.readableStream
}
} else {
metadata[0] = createStream(port)
}
map.set(downloadUrl, metadata)
port.postMessage({ download: downloadUrl })
}
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') {
controller.error('Aborted the download')
return
}
controller.enqueue(data)
}
},
cancel () {
console.log('user aborted')
}
})
}
self.onfetch = event => {
const url = event.request.url
// this only works for Firefox
if (url.endsWith('/ping')) {
return event.respondWith(new Response('pong'))
}
const hijacke = map.get(url)
if (!hijacke) return null
const [ stream, data, port ] = hijacke
map.delete(url)
// Not comfortable letting any user control all headers
// so we only copy over the length & disposition
const responseHeaders = new Headers({
'Content-Type': 'application/octet-stream; charset=utf-8',
// To be on the safe side, The link can be opened in a iframe.
// but octet-stream should stop it.
'Content-Security-Policy': "default-src 'none'",
'X-Content-Security-Policy': "default-src 'none'",
'X-WebKit-CSP': "default-src 'none'",
'X-XSS-Protection': '1; mode=block'
})
let headers = new Headers(data.headers || {})
if (headers.has('Content-Length')) {
responseHeaders.set('Content-Length', headers.get('Content-Length'))
}
if (headers.has('Content-Disposition')) {
responseHeaders.set('Content-Disposition', headers.get('Content-Disposition'))
}
// data, data.filename and size should not be used anymore
if (data.size) {
console.warn('Depricated')
responseHeaders.set('Content-Length', data.size)
}
let fileName = typeof data === 'string' ? data : data.filename
if (fileName) {
console.warn('Depricated')
// Make filename RFC5987 compatible
fileName = encodeURIComponent(fileName).replace(/['()]/g, escape).replace(/\*/g, '%2A')
responseHeaders.set('Content-Disposition', "attachment; filename*=UTF-8''" + fileName)
}
event.respondWith(new Response(stream, { headers: responseHeaders }))
port.postMessage({ debug: 'Download started' })
}