diff --git a/app/api.js b/app/api.js index 585cf1e9d..b6080286d 100644 --- a/app/api.js +++ b/app/api.js @@ -91,10 +91,15 @@ export async function setPassword(id, owner_token, keychain) { return response.ok; } -export function uploadFile(encrypted, metadata, verifierB64, keychain) { +export function uploadFile( + encrypted, + metadata, + verifierB64, + keychain, + onprogress +) { const xhr = new XMLHttpRequest(); const upload = { - onprogress: function() {}, cancel: function() { xhr.abort(); }, @@ -122,7 +127,7 @@ export function uploadFile(encrypted, metadata, verifierB64, keychain) { fd.append('data', blob); xhr.upload.addEventListener('progress', function(event) { if (event.lengthComputable) { - upload.onprogress([event.loaded, event.total]); + onprogress([event.loaded, event.total]); } }); xhr.open('post', '/api/upload', true); @@ -132,10 +137,10 @@ export function uploadFile(encrypted, metadata, verifierB64, keychain) { return upload; } -function download(id, keychain) { +function download(id, keychain, onprogress) { const xhr = new XMLHttpRequest(); const download = { - onprogress: function() {}, + onprogress, cancel: function() { xhr.abort(); }, @@ -173,8 +178,7 @@ function download(id, keychain) { } async function tryDownload(id, keychain, onprogress, tries = 1) { - const dl = download(id, keychain); - dl.onprogress = onprogress; + const dl = download(id, keychain, onprogress); try { const result = await dl.result; return result; @@ -186,24 +190,24 @@ async function tryDownload(id, keychain, onprogress, tries = 1) { } } -export function downloadFile(id, keychain) { - let cancelled = false; +export function downloadFile(id, keychain, onprogress) { + let abort = function() {}; + // This is a bit of a hack + // We piggyback off of the progress event to set the abort function. + // Otherwise wiring the xhr abort up while allowing retries + // gets pretty nasty. + // 'this' here is the object returned by download(id, keychain) + // Calling dl.cancel() before any progress does nothing, so fileReceiver + // must be sure to wait for at least one progress event before calling it. function updateProgress(p) { - if (cancelled) { - // This is a bit of a hack - // We piggyback off of the progress event as a chance to cancel. - // Otherwise wiring the xhr abort up while allowing retries - // gets pretty nasty. - // 'this' here is the object returned by download(id, keychain) - return this.cancel(); - } - dl.onprogress(p); + abort = this.cancel; + onprogress(p); + } + function cancel() { + abort(); } const dl = { - onprogress: function() {}, - cancel: function() { - cancelled = true; - }, + cancel, result: tryDownload(id, keychain, updateProgress, 2) }; return dl; diff --git a/app/fileManager.js b/app/fileManager.js index a788e1b86..04eb99a33 100644 --- a/app/fileManager.js +++ b/app/fileManager.js @@ -149,8 +149,6 @@ export default function(state, emitter) { const receiver = new FileReceiver(file); try { await receiver.getMetadata(); - receiver.on('progress', updateProgress); - receiver.on('decrypting', render); state.transfer = receiver; } catch (e) { if (e.message === '401') { @@ -164,14 +162,16 @@ export default function(state, emitter) { }); emitter.on('download', async file => { - state.transfer.on('progress', render); + state.transfer.on('progress', updateProgress); state.transfer.on('decrypting', render); const links = openLinksInNewTab(); const size = file.size; try { const start = Date.now(); metrics.startedDownload({ size: file.size, ttl: file.ttl }); - await state.transfer.download(); + const dl = state.transfer.download(); + render(); + await dl; const time = Date.now() - start; const speed = size / (time / 1000); await delay(1000); diff --git a/app/fileReceiver.js b/app/fileReceiver.js index 9976ed1ab..639f6511f 100644 --- a/app/fileReceiver.js +++ b/app/fileReceiver.js @@ -31,13 +31,9 @@ export default class FileReceiver extends Nanobus { cancel() { this.cancelled = true; - if (this.fileDownload) { - this.fileDownload.cancel(); - } } reset() { - this.fileDownload = null; this.msg = 'fileSizeProgress'; this.state = 'initialized'; this.progress = [0, 1]; @@ -46,52 +42,37 @@ export default class FileReceiver extends Nanobus { async getMetadata() { const meta = await metadata(this.fileInfo.id, this.keychain); - if (meta) { - this.keychain.setIV(meta.iv); - this.fileInfo.name = meta.name; - this.fileInfo.type = meta.type; - this.fileInfo.iv = meta.iv; - this.fileInfo.size = meta.size; - this.state = 'ready'; - return; - } - this.state = 'invalid'; - return; + this.keychain.setIV(meta.iv); + this.fileInfo.name = meta.name; + this.fileInfo.type = meta.type; + this.fileInfo.iv = meta.iv; + this.fileInfo.size = meta.size; + this.state = 'ready'; } async download(noSave = false) { this.state = 'downloading'; - this.emit('progress', this.progress); - try { - const download = await downloadFile(this.fileInfo.id, this.keychain); - download.onprogress = p => { - this.progress = p; - this.emit('progress', p); - }; - this.fileDownload = download; - const ciphertext = await download.result; - this.fileDownload = null; - this.msg = 'decryptingFile'; - this.state = 'decrypting'; - this.emit('decrypting'); - const plaintext = await this.keychain.decryptFile(ciphertext); + const download = await downloadFile(this.fileInfo.id, this.keychain, p => { if (this.cancelled) { - throw new Error(0); + return download.cancel(); } - if (!noSave) { - await saveFile({ - plaintext, - name: decodeURIComponent(this.fileInfo.name), - type: this.fileInfo.type - }); - } - this.msg = 'downloadFinish'; - this.state = 'complete'; - return; - } catch (e) { - this.state = 'invalid'; - throw e; + this.progress = p; + this.emit('progress'); + }); + const ciphertext = await download.result; + this.msg = 'decryptingFile'; + this.state = 'decrypting'; + this.emit('decrypting'); + const plaintext = await this.keychain.decryptFile(ciphertext); + if (!noSave) { + await saveFile({ + plaintext, + name: decodeURIComponent(this.fileInfo.name), + type: this.fileInfo.type + }); } + this.msg = 'downloadFinish'; + this.state = 'complete'; } } diff --git a/app/fileSender.js b/app/fileSender.js index a6cbc0a54..a3fb7a257 100644 --- a/app/fileSender.js +++ b/app/fileSender.js @@ -9,11 +9,8 @@ export default class FileSender extends Nanobus { constructor(file) { super('FileSender'); this.file = file; - this.uploadRequest = null; - this.msg = 'importingFile'; - this.progress = [0, 1]; - this.cancelled = false; this.keychain = new Keychain(); + this.reset(); } get progressRatio() { @@ -31,6 +28,13 @@ export default class FileSender extends Nanobus { }; } + reset() { + this.uploadRequest = null; + this.msg = 'importingFile'; + this.progress = [0, 1]; + this.cancelled = false; + } + cancel() { this.cancelled = true; if (this.uploadRequest) { @@ -71,13 +75,13 @@ export default class FileSender extends Nanobus { encrypted, metadata, authKeyB64, - this.keychain + this.keychain, + p => { + this.progress = p; + this.emit('progress', p); + } ); this.msg = 'fileSizeProgress'; - this.uploadRequest.onprogress = p => { - this.progress = p; - this.emit('progress', p); - }; try { const result = await this.uploadRequest.result; const time = Date.now() - start; diff --git a/test/frontend/tests/api-tests.js b/test/frontend/tests/api-tests.js index e11c47b90..fb470addd 100644 --- a/test/frontend/tests/api-tests.js +++ b/test/frontend/tests/api-tests.js @@ -16,11 +16,28 @@ describe('API', function() { const encrypted = await keychain.encryptFile(plaintext); const meta = await keychain.encryptMetadata(metadata); const verifierB64 = await keychain.authKeyB64(); - const up = api.uploadFile(encrypted, meta, verifierB64, keychain); + const p = function() {}; + const up = api.uploadFile(encrypted, meta, verifierB64, keychain, p); const result = await up.result; assert.ok(result.url); assert.ok(result.id); assert.ok(result.ownerToken); }); + + it('can be cancelled', async function() { + const keychain = new Keychain(); + const encrypted = await keychain.encryptFile(plaintext); + const meta = await keychain.encryptMetadata(metadata); + const verifierB64 = await keychain.authKeyB64(); + const p = function() {}; + const up = api.uploadFile(encrypted, meta, verifierB64, keychain, p); + up.cancel(); + try { + await up.result; + assert.fail('not cancelled'); + } catch (e) { + assert.equal(e.message, '0'); + } + }); }); }); diff --git a/test/frontend/tests/workflow-tests.js b/test/frontend/tests/workflow-tests.js index 539aa40d3..46221f1fb 100644 --- a/test/frontend/tests/workflow-tests.js +++ b/test/frontend/tests/workflow-tests.js @@ -87,6 +87,54 @@ describe('Upload / Download flow', function() { assert.equal(fr.fileInfo.name, blob.name); }); + it('can cancel the upload', async function() { + const fs = new FileSender(blob); + const up = fs.upload(); + fs.cancel(); // before encrypting + try { + await up; + assert.fail('not cancelled'); + } catch (e) { + assert.equal(e.message, '0'); + } + fs.reset(); + fs.once('encrypting', () => fs.cancel()); + try { + await fs.upload(); + assert.fail('not cancelled'); + } catch (e) { + assert.equal(e.message, '0'); + } + fs.reset(); + fs.once('progress', () => fs.cancel()); + try { + await fs.upload(); + assert.fail('not cancelled'); + } catch (e) { + assert.equal(e.message, '0'); + } + }); + + it('can cancel the download', async function() { + const fs = new FileSender(blob); + const file = await fs.upload(); + const fr = new FileReceiver({ + secretKey: file.toJSON().secretKey, + id: file.id, + nonce: file.keychain.nonce, + requiresPassword: false + }); + await fr.getMetadata(); + fr.cancel(); + //fr.once('progress', () => fr.cancel()) + try { + await fr.download(noSave); + assert.fail('not cancelled'); + } catch (e) { + assert.equal(e.message, '0'); + } + }); + it('can allow multiple downloads', async function() { const fs = new FileSender(blob); const file = await fs.upload();