UNPKG

downzip

Version:

Library to enable client-side code to stream potentially large files into a zipped download

139 lines (120 loc) 4.73 kB
import '@babel/polyfill' import WorkerUtils from './WorkerUtils' import Zip from './zip/Zip' import ZipUtils from './zip/ZipUtils' const Utils = new WorkerUtils('DownZipServiceWorker') // /////////// GLOBAL OBJECTS /////////// // const zipMap = {} // ////////// MESSAGE HANDLERS ////////// // const initialize = (data, ports) => { Utils.log(`Initialize called: ${JSON.stringify(data)}`) const {id, files, name} = data // Decide whether to use zip64 const totalSizeBig = ZipUtils.calculateSize(files) Utils.log(`Total estimated file size: ${totalSizeBig}`) const zip64 = (totalSizeBig >= BigInt('0xFFFFFFFF')) // Start new Zip object and add to the map zipMap[id] = { files, name, zip: new Zip(zip64), sizeBig: totalSizeBig } // Acknowledge reception if(ports.length > 0) ports[0].postMessage({command: 'ACKNOWLEDGE'}) } // This message is here to keep the service worker from getting killed while downloading. // TODO: Only send tick while actually downloading const tick = () => { Utils.log(`Tock`) } // /////////// EVENT HANDLERS /////////// // self.addEventListener('install', () => { Utils.log("Installing worker and skip waiting") skipWaiting() }) self.addEventListener('activate', () => { Utils.log("Activating worker and skip waiting") skipWaiting() self.clients.claim() }) self.addEventListener('fetch', async (event) => { // Get URL and check if it is a download request const urlParts = event.request.url.split('/') const lastPart = urlParts[urlParts.length - 1] if(lastPart.includes('download-')) { // Get download id const id = lastPart.replace('download-', '') Utils.log(`Fetch called for download id: ${id}`) // Check if initialized if(!zipMap[id]){ Utils.error(`No zip initialized for id: ${id}`) return } // Respond with the zip outputStream event.respondWith(new Response( zipMap[id].zip.outputStream, {headers: new Headers({ 'Content-Type': 'application/octet-stream; charset=utf-8', 'Content-Disposition': `attachment; filename="${zipMap[id].name}.zip"`, 'Content-Length': zipMap[id].sizeBig // This is an approximation, does not take into account the headers })} )) // Start feeding zip the downloads for(let i=0; i<zipMap[id].files.length; i++){ const file = zipMap[id].files[i] // Start new file in the zip zipMap[id].zip.startFile(file.name) // Append all the downloaded data try { await new Promise((resolve, reject) => { fetch(file.downloadUrl).then(response => response.body).then(async (stream) => { const reader = stream.getReader() let doneReading = false while (!doneReading) { const chunk = await reader.read() const {done, value} = chunk if (done) { // If this stream has finished, resolve and return resolve() doneReading = true } else { // If not, append data to the zip zipMap[id].zip.appendData(value) } } }).catch((err) => { reject(err) }) }) } catch (e) { Utils.error(`Error while piping data into zip: ${e.toString()}`) } // End file zipMap[id].zip.endFile() } // End zip zipMap[id].zip.finish() Utils.log("Done with this zip!") } else { Utils.log('Fetch called for a non-download. Doing nothing') } }) self.addEventListener('error', (message, url, lineNo) => { Utils.log(`Error: ${message} at line number: ${lineNo}. Handling URL ${url}`) return true }) const messageHandlers = { 'INITIALIZE': initialize, 'TICK': tick } self.addEventListener('message', async (event) => { const {data, ports} = event const handler = messageHandlers[data.command] if(handler){ await handler(data.data, ports) } else { Utils.error(`Handler for command does not exist: ${data.command}`) } })