/* 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 = 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 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' }) 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' }) }