diff --git a/public/streamsaver/mitm.html b/public/streamsaver/mitm.html
new file mode 100644
index 00000000..f6d94e2f
--- /dev/null
+++ b/public/streamsaver/mitm.html
@@ -0,0 +1,179 @@
+
+
diff --git a/public/streamsaver/sw.js b/public/streamsaver/sw.js
new file mode 100644
index 00000000..f6cb4c3a
--- /dev/null
+++ b/public/streamsaver/sw.js
@@ -0,0 +1,132 @@
+/* eslint-disable */
+/* 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 second 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 = Array.from({ length: 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 through 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(reason) {
+ console.log('user aborted', reason);
+ port.postMessage({ abort: true });
+ }
+ });
+}
+
+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',
+ 'Cross-Origin-Embedder-Policy': 'require-corp'
+ });
+
+ const 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' });
+};
diff --git a/src/hooks/business/download.ts b/src/hooks/business/download.ts
index 210e1253..060b4579 100644
--- a/src/hooks/business/download.ts
+++ b/src/hooks/business/download.ts
@@ -51,6 +51,7 @@ export function useDownload() {
contentLength?: number
): Promise {
window.$loading?.endLoading();
+ StreamSaver.mitm = '/streamsaver/mitm.html?version=2.0.0';
const fileStream = StreamSaver.createWriteStream(filename, { size: contentLength });
if (window.WritableStream && readableStream?.pipeTo) {