robbederks/downzip

View on GitHub
src/downzip-sw.js

Summary

Maintainability
A
0 mins
Test Coverage
F
0%
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}`)
    }
})