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