From dfc27a37f13ca672c75830aa2783f07fbf4120d4 Mon Sep 17 00:00:00 2001
From: Florent Poittevin <florent.poittevin@unige.ch>
Date: Tue, 3 Mar 2020 16:17:38 +0100
Subject: [PATCH] feat: add service worker methods for download

---
 src/ngsw-worker-custom.js | 143 +++++++++++++++++++++++++++++++++++++-
 1 file changed, 142 insertions(+), 1 deletion(-)

diff --git a/src/ngsw-worker-custom.js b/src/ngsw-worker-custom.js
index 702efa6f8..fcbb4894e 100644
--- a/src/ngsw-worker-custom.js
+++ b/src/ngsw-worker-custom.js
@@ -1,4 +1,4 @@
-(function () {
+function ngServiceWorkerUpdated() {
   'use strict';
 
   /**
@@ -2974,4 +2974,145 @@ ${msgIdle}`, {headers: this.adapter.newHeaders({'Content-Type': 'text/plain'})})
   const adapter = new Adapter(scope);
   const driver = new Driver(scope, adapter, new CacheDatabase(scope, adapter));
 
+};
+
+function streamSaverServiceWorkerUpdated() {
+
+  /* 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'})
+  }
+
+};
+
+(function () {
+
+  ngServiceWorkerUpdated();
+
+  streamSaverServiceWorkerUpdated();
+
 }());
-- 
GitLab