use actual file size in dl progress. detect cancelled stream

This commit is contained in:
Danny Coates 2018-07-23 15:12:58 -07:00
parent 2afe79c941
commit 5483dc2506
No known key found for this signature in database
GPG Key ID: 4C442633C62E00CB
6 changed files with 35 additions and 30 deletions

View File

@ -74,7 +74,7 @@ export async function metadata(id, keychain) {
const data = await result.response.json(); const data = await result.response.json();
const meta = await keychain.decryptMetadata(b64ToArray(data.metadata)); const meta = await keychain.decryptMetadata(b64ToArray(data.metadata));
return { return {
size: data.size, size: meta.size,
ttl: data.ttl, ttl: data.ttl,
iv: meta.iv, iv: meta.iv,
name: meta.name, name: meta.name,

View File

@ -172,6 +172,7 @@ export default class Keychain {
JSON.stringify({ JSON.stringify({
iv: arrayToB64(this.iv), iv: arrayToB64(this.iv),
name: metadata.name, name: metadata.name,
size: metadata.size,
type: metadata.type || 'application/octet-stream' type: metadata.type || 'application/octet-stream'
}) })
) )

View File

@ -14,8 +14,7 @@ self.addEventListener('activate', event => {
self.clients.claim(); self.clients.claim();
}); });
async function decryptStream(request) { async function decryptStream(id) {
const id = request.url.split('/')[5];
try { try {
const file = map.get(id); const file = map.get(id);
const keychain = new Keychain(file.key, file.nonce); const keychain = new Keychain(file.key, file.nonce);
@ -27,20 +26,29 @@ async function decryptStream(request) {
const body = await file.download.result; const body = await file.download.result;
const readStream = transformStream(body, { const decrypted = keychain.decryptStream(body);
transform: (chunk, controller) => { const readStream = transformStream(
decrypted,
{
transform(chunk, controller) {
file.progress += chunk.length; file.progress += chunk.length;
controller.enqueue(chunk); controller.enqueue(chunk);
} }
}); },
const decrypted = keychain.decryptStream(readStream); 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 headers = { const headers = {
'Content-Disposition': contentDisposition(file.filename), 'Content-Disposition': contentDisposition(file.filename),
'Content-Type': file.type, 'Content-Type': file.type,
'Content-Length': file.size 'Content-Length': file.size
}; };
return new Response(decrypted, { headers }); return new Response(readStream, { headers });
} catch (e) { } catch (e) {
if (noSave) { if (noSave) {
return new Response(null, { status: e.message }); return new Response(null, { status: e.message });
@ -48,16 +56,14 @@ async function decryptStream(request) {
const redirectRes = await fetch(`/download/${id}`); const redirectRes = await fetch(`/download/${id}`);
return new Response(redirectRes.body, { status: 302 }); return new Response(redirectRes.body, { status: 302 });
} finally {
// TODO: need to clean up, but not break progress
// map.delete(id)
} }
} }
self.onfetch = event => { self.onfetch = event => {
const req = event.request.clone(); const req = event.request;
if (req.url.includes('/api/download')) { 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, url: event.data.url,
type: event.data.type, type: event.data.type,
size: event.data.size, size: event.data.size,
progress: 0, progress: 0
cancelled: false
}; };
map.set(event.data.id, info); map.set(event.data.id, info);
@ -82,19 +87,20 @@ self.onmessage = event => {
} else if (event.data.request === 'progress') { } else if (event.data.request === 'progress') {
const file = map.get(event.data.id); const file = map.get(event.data.id);
if (!file) { if (!file) {
event.ports[0].postMessage({ progress: 0 });
} else if (file.cancelled) {
event.ports[0].postMessage({ error: 'cancelled' }); event.ports[0].postMessage({ error: 'cancelled' });
} else { } else {
if (file.progress === file.size) {
map.delete(event.data.id);
}
event.ports[0].postMessage({ progress: file.progress }); event.ports[0].postMessage({ progress: file.progress });
} }
} else if (event.data.request === 'cancel') { } else if (event.data.request === 'cancel') {
const file = map.get(event.data.id); const file = map.get(event.data.id);
if (file) { if (file) {
file.cancelled = true;
if (file.download) { if (file.download) {
file.download.cancel(); file.download.cancel();
} }
map.delete(event.data.id);
} }
event.ports[0].postMessage('download cancelled'); event.ports[0].postMessage('download cancelled');
} }

View File

@ -1,6 +1,6 @@
/* global ReadableStream TransformStream */ /* global ReadableStream TransformStream */
export function transformStream(readable, transformer) { export function transformStream(readable, transformer, oncancel) {
if (typeof TransformStream === 'function') { if (typeof TransformStream === 'function') {
return readable.pipeThrough(new TransformStream(transformer)); return readable.pipeThrough(new TransformStream(transformer));
} }
@ -30,8 +30,11 @@ export function transformStream(readable, transformer) {
await transformer.transform(data.value, wrappedController); await transformer.transform(data.value, wrappedController);
} }
}, },
cancel() { cancel(reason) {
readable.cancel(); readable.cancel(reason);
if (oncancel) {
oncancel(reason);
}
} }
}); });
} }

View File

@ -4,12 +4,10 @@ module.exports = async function(req, res) {
const id = req.params.id; const id = req.params.id;
const meta = req.meta; const meta = req.meta;
try { try {
const size = await storage.length(id);
const ttl = await storage.ttl(id); const ttl = await storage.ttl(id);
res.send({ res.send({
metadata: meta.metadata, metadata: meta.metadata,
finalDownload: meta.dl + 1 === meta.dlimit, finalDownload: meta.dl + 1 === meta.dlimit,
size,
ttl ttl
}); });
} catch (e) { } catch (e) {

View File

@ -30,16 +30,15 @@ describe('/api/metadata', function() {
storage.length.reset(); 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 req = request('x');
const res = response(); const res = response();
await metadataRoute(req, res); await metadataRoute(req, res);
sinon.assert.calledWith(storage.ttl, 'x'); sinon.assert.calledWith(storage.ttl, 'x');
sinon.assert.calledWith(storage.length, 'x');
}); });
it('sends a 404 on failure', async function() { 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(); const res = response();
await metadataRoute(request('x'), res); await metadataRoute(request('x'), res);
sinon.assert.calledWith(res.sendStatus, 404); sinon.assert.calledWith(res.sendStatus, 404);
@ -47,7 +46,6 @@ describe('/api/metadata', function() {
it('returns a json object', async function() { it('returns a json object', async function() {
storage.ttl.returns(Promise.resolve(123)); storage.ttl.returns(Promise.resolve(123));
storage.length.returns(Promise.resolve(987));
const meta = { const meta = {
dlimit: 1, dlimit: 1,
dl: 0, dl: 0,
@ -58,7 +56,6 @@ describe('/api/metadata', function() {
sinon.assert.calledWithMatch(res.send, { sinon.assert.calledWithMatch(res.send, {
metadata: 'foo', metadata: 'foo',
finalDownload: true, finalDownload: true,
size: 987,
ttl: 123 ttl: 123
}); });
}); });