From 5483dc2506c9b0c78f243bd5df7cf9f47803d403 Mon Sep 17 00:00:00 2001 From: Danny Coates Date: Mon, 23 Jul 2018 15:12:58 -0700 Subject: [PATCH] use actual file size in dl progress. detect cancelled stream --- app/api.js | 2 +- app/keychain.js | 1 + app/serviceWorker.js | 44 +++++++++++++++++++--------------- app/streams.js | 9 ++++--- server/routes/metadata.js | 2 -- test/backend/metadata-tests.js | 7 ++---- 6 files changed, 35 insertions(+), 30 deletions(-) diff --git a/app/api.js b/app/api.js index a2e7ef96..d766f6ce 100644 --- a/app/api.js +++ b/app/api.js @@ -74,7 +74,7 @@ export async function metadata(id, keychain) { const data = await result.response.json(); const meta = await keychain.decryptMetadata(b64ToArray(data.metadata)); return { - size: data.size, + size: meta.size, ttl: data.ttl, iv: meta.iv, name: meta.name, diff --git a/app/keychain.js b/app/keychain.js index ab57b852..cc867e29 100644 --- a/app/keychain.js +++ b/app/keychain.js @@ -172,6 +172,7 @@ export default class Keychain { JSON.stringify({ iv: arrayToB64(this.iv), name: metadata.name, + size: metadata.size, type: metadata.type || 'application/octet-stream' }) ) diff --git a/app/serviceWorker.js b/app/serviceWorker.js index 8194e988..e2c5f271 100644 --- a/app/serviceWorker.js +++ b/app/serviceWorker.js @@ -14,8 +14,7 @@ self.addEventListener('activate', event => { self.clients.claim(); }); -async function decryptStream(request) { - const id = request.url.split('/')[5]; +async function decryptStream(id) { try { const file = map.get(id); const keychain = new Keychain(file.key, file.nonce); @@ -27,20 +26,29 @@ async function decryptStream(request) { const body = await file.download.result; - const readStream = transformStream(body, { - transform: (chunk, controller) => { - file.progress += chunk.length; - controller.enqueue(chunk); + const decrypted = keychain.decryptStream(body); + const readStream = transformStream( + decrypted, + { + transform(chunk, controller) { + file.progress += chunk.length; + controller.enqueue(chunk); + } + }, + function oncancel() { + // NOTE: cancel doesn't currently fire on chrome + // https://bugs.chromium.org/p/chromium/issues/detail?id=638494 + file.download.cancel(); + map.delete(id); } - }); - const decrypted = keychain.decryptStream(readStream); + ); const headers = { 'Content-Disposition': contentDisposition(file.filename), 'Content-Type': file.type, 'Content-Length': file.size }; - return new Response(decrypted, { headers }); + return new Response(readStream, { headers }); } catch (e) { if (noSave) { return new Response(null, { status: e.message }); @@ -48,16 +56,14 @@ async function decryptStream(request) { const redirectRes = await fetch(`/download/${id}`); return new Response(redirectRes.body, { status: 302 }); - } finally { - // TODO: need to clean up, but not break progress - // map.delete(id) } } self.onfetch = event => { - const req = event.request.clone(); + const req = event.request; if (req.url.includes('/api/download')) { - event.respondWith(decryptStream(req)); + const id = req.url.split('/')[5]; + event.respondWith(decryptStream(id)); } }; @@ -73,8 +79,7 @@ self.onmessage = event => { url: event.data.url, type: event.data.type, size: event.data.size, - progress: 0, - cancelled: false + progress: 0 }; map.set(event.data.id, info); @@ -82,19 +87,20 @@ self.onmessage = event => { } else if (event.data.request === 'progress') { const file = map.get(event.data.id); if (!file) { - event.ports[0].postMessage({ progress: 0 }); - } else if (file.cancelled) { event.ports[0].postMessage({ error: 'cancelled' }); } else { + if (file.progress === file.size) { + map.delete(event.data.id); + } event.ports[0].postMessage({ progress: file.progress }); } } else if (event.data.request === 'cancel') { const file = map.get(event.data.id); if (file) { - file.cancelled = true; if (file.download) { file.download.cancel(); } + map.delete(event.data.id); } event.ports[0].postMessage('download cancelled'); } diff --git a/app/streams.js b/app/streams.js index d0e7b972..0971fa58 100644 --- a/app/streams.js +++ b/app/streams.js @@ -1,6 +1,6 @@ /* global ReadableStream TransformStream */ -export function transformStream(readable, transformer) { +export function transformStream(readable, transformer, oncancel) { if (typeof TransformStream === 'function') { return readable.pipeThrough(new TransformStream(transformer)); } @@ -30,8 +30,11 @@ export function transformStream(readable, transformer) { await transformer.transform(data.value, wrappedController); } }, - cancel() { - readable.cancel(); + cancel(reason) { + readable.cancel(reason); + if (oncancel) { + oncancel(reason); + } } }); } diff --git a/server/routes/metadata.js b/server/routes/metadata.js index a671cee6..2e50537c 100644 --- a/server/routes/metadata.js +++ b/server/routes/metadata.js @@ -4,12 +4,10 @@ module.exports = async function(req, res) { const id = req.params.id; const meta = req.meta; try { - const size = await storage.length(id); const ttl = await storage.ttl(id); res.send({ metadata: meta.metadata, finalDownload: meta.dl + 1 === meta.dlimit, - size, ttl }); } catch (e) { diff --git a/test/backend/metadata-tests.js b/test/backend/metadata-tests.js index fbaeaefd..9208b912 100644 --- a/test/backend/metadata-tests.js +++ b/test/backend/metadata-tests.js @@ -30,16 +30,15 @@ describe('/api/metadata', function() { storage.length.reset(); }); - it('calls storage.[ttl|length] with the id parameter', async function() { + it('calls storage.ttl with the id parameter', async function() { const req = request('x'); const res = response(); await metadataRoute(req, res); sinon.assert.calledWith(storage.ttl, 'x'); - sinon.assert.calledWith(storage.length, 'x'); }); it('sends a 404 on failure', async function() { - storage.length.returns(Promise.reject(new Error())); + storage.ttl.returns(Promise.reject(new Error())); const res = response(); await metadataRoute(request('x'), res); sinon.assert.calledWith(res.sendStatus, 404); @@ -47,7 +46,6 @@ describe('/api/metadata', function() { it('returns a json object', async function() { storage.ttl.returns(Promise.resolve(123)); - storage.length.returns(Promise.resolve(987)); const meta = { dlimit: 1, dl: 0, @@ -58,7 +56,6 @@ describe('/api/metadata', function() { sinon.assert.calledWithMatch(res.send, { metadata: 'foo', finalDownload: true, - size: 987, ttl: 123 }); });