131 lines
		
	
	
		
			3.6 KiB
		
	
	
	
		
			JavaScript
		
	
	
			
		
		
	
	
			131 lines
		
	
	
		
			3.6 KiB
		
	
	
	
		
			JavaScript
		
	
	
| /* 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' })
 | |
| }
 |