more frontend tests and some factoring based on them

This commit is contained in:
Danny Coates 2018-02-24 11:24:12 -08:00
parent 78728ce4ca
commit d6c0489fa3
No known key found for this signature in database
GPG Key ID: 4C442633C62E00CB
6 changed files with 153 additions and 107 deletions

View File

@ -91,10 +91,15 @@ export async function setPassword(id, owner_token, keychain) {
return response.ok; return response.ok;
} }
export function uploadFile(encrypted, metadata, verifierB64, keychain) { export function uploadFile(
encrypted,
metadata,
verifierB64,
keychain,
onprogress
) {
const xhr = new XMLHttpRequest(); const xhr = new XMLHttpRequest();
const upload = { const upload = {
onprogress: function() {},
cancel: function() { cancel: function() {
xhr.abort(); xhr.abort();
}, },
@ -122,7 +127,7 @@ export function uploadFile(encrypted, metadata, verifierB64, keychain) {
fd.append('data', blob); fd.append('data', blob);
xhr.upload.addEventListener('progress', function(event) { xhr.upload.addEventListener('progress', function(event) {
if (event.lengthComputable) { if (event.lengthComputable) {
upload.onprogress([event.loaded, event.total]); onprogress([event.loaded, event.total]);
} }
}); });
xhr.open('post', '/api/upload', true); xhr.open('post', '/api/upload', true);
@ -132,15 +137,14 @@ export function uploadFile(encrypted, metadata, verifierB64, keychain) {
return upload; return upload;
} }
function download(id, keychain) { function download(id, keychain, onprogress, canceller) {
const xhr = new XMLHttpRequest(); const xhr = new XMLHttpRequest();
const download = { canceller.oncancel = function() {
onprogress: function() {},
cancel: function() {
xhr.abort(); xhr.abort();
}, };
result: new Promise(async function(resolve, reject) { return new Promise(async function(resolve, reject) {
xhr.addEventListener('loadend', function() { xhr.addEventListener('loadend', function() {
canceller.oncancel = function() {};
const authHeader = xhr.getResponseHeader('WWW-Authenticate'); const authHeader = xhr.getResponseHeader('WWW-Authenticate');
if (authHeader) { if (authHeader) {
keychain.nonce = parseNonce(authHeader); keychain.nonce = parseNonce(authHeader);
@ -158,7 +162,7 @@ function download(id, keychain) {
}); });
xhr.addEventListener('progress', function(event) { xhr.addEventListener('progress', function(event) {
if (event.lengthComputable && event.target.status === 200) { if (event.lengthComputable && event.target.status === 200) {
download.onprogress([event.loaded, event.total]); onprogress([event.loaded, event.total]);
} }
}); });
const auth = await keychain.authHeader(); const auth = await keychain.authHeader();
@ -166,45 +170,30 @@ function download(id, keychain) {
xhr.setRequestHeader('Authorization', auth); xhr.setRequestHeader('Authorization', auth);
xhr.responseType = 'blob'; xhr.responseType = 'blob';
xhr.send(); xhr.send();
}) });
};
return download;
} }
async function tryDownload(id, keychain, onprogress, tries = 1) { async function tryDownload(id, keychain, onprogress, canceller, tries = 1) {
const dl = download(id, keychain);
dl.onprogress = onprogress;
try { try {
const result = await dl.result; const result = await download(id, keychain, onprogress, canceller);
return result; return result;
} catch (e) { } catch (e) {
if (e.message === '401' && --tries > 0) { if (e.message === '401' && --tries > 0) {
return tryDownload(id, keychain, onprogress, tries); return tryDownload(id, keychain, onprogress, canceller, tries);
} }
throw e; throw e;
} }
} }
export function downloadFile(id, keychain) { export function downloadFile(id, keychain, onprogress) {
let cancelled = false; const canceller = {
function updateProgress(p) { oncancel: function() {} // download() sets this
if (cancelled) { };
// This is a bit of a hack function cancel() {
// We piggyback off of the progress event as a chance to cancel. canceller.oncancel();
// Otherwise wiring the xhr abort up while allowing retries }
// gets pretty nasty. return {
// 'this' here is the object returned by download(id, keychain) cancel,
return this.cancel(); result: tryDownload(id, keychain, onprogress, canceller, 2)
}
dl.onprogress(p);
}
const dl = {
onprogress: function() {},
cancel: function() {
cancelled = true;
},
result: tryDownload(id, keychain, updateProgress, 2)
}; };
return dl;
} }

View File

@ -149,8 +149,6 @@ export default function(state, emitter) {
const receiver = new FileReceiver(file); const receiver = new FileReceiver(file);
try { try {
await receiver.getMetadata(); await receiver.getMetadata();
receiver.on('progress', updateProgress);
receiver.on('decrypting', render);
state.transfer = receiver; state.transfer = receiver;
} catch (e) { } catch (e) {
if (e.message === '401') { if (e.message === '401') {
@ -164,14 +162,16 @@ export default function(state, emitter) {
}); });
emitter.on('download', async file => { emitter.on('download', async file => {
state.transfer.on('progress', render); state.transfer.on('progress', updateProgress);
state.transfer.on('decrypting', render); state.transfer.on('decrypting', render);
const links = openLinksInNewTab(); const links = openLinksInNewTab();
const size = file.size; const size = file.size;
try { try {
const start = Date.now(); const start = Date.now();
metrics.startedDownload({ size: file.size, ttl: file.ttl }); 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 time = Date.now() - start;
const speed = size / (time / 1000); const speed = size / (time / 1000);
await delay(1000); await delay(1000);

View File

@ -30,54 +30,44 @@ export default class FileReceiver extends Nanobus {
} }
cancel() { cancel() {
this.cancelled = true; if (this.downloadRequest) {
if (this.fileDownload) { this.downloadRequest.cancel();
this.fileDownload.cancel();
} }
} }
reset() { reset() {
this.fileDownload = null;
this.msg = 'fileSizeProgress'; this.msg = 'fileSizeProgress';
this.state = 'initialized'; this.state = 'initialized';
this.progress = [0, 1]; this.progress = [0, 1];
this.cancelled = false;
} }
async getMetadata() { async getMetadata() {
const meta = await metadata(this.fileInfo.id, this.keychain); const meta = await metadata(this.fileInfo.id, this.keychain);
if (meta) {
this.keychain.setIV(meta.iv); this.keychain.setIV(meta.iv);
this.fileInfo.name = meta.name; this.fileInfo.name = meta.name;
this.fileInfo.type = meta.type; this.fileInfo.type = meta.type;
this.fileInfo.iv = meta.iv; this.fileInfo.iv = meta.iv;
this.fileInfo.size = meta.size; this.fileInfo.size = meta.size;
this.state = 'ready'; this.state = 'ready';
return;
}
this.state = 'invalid';
return;
} }
async download(noSave = false) { async download(noSave = false) {
this.state = 'downloading'; this.state = 'downloading';
this.emit('progress', this.progress); this.downloadRequest = await downloadFile(
try { this.fileInfo.id,
const download = await downloadFile(this.fileInfo.id, this.keychain); this.keychain,
download.onprogress = p => { p => {
this.progress = p; this.progress = p;
this.emit('progress', p); this.emit('progress');
}; }
this.fileDownload = download; );
const ciphertext = await download.result; try {
this.fileDownload = null; const ciphertext = await this.downloadRequest.result;
this.downloadRequest = null;
this.msg = 'decryptingFile'; this.msg = 'decryptingFile';
this.state = 'decrypting'; this.state = 'decrypting';
this.emit('decrypting'); this.emit('decrypting');
const plaintext = await this.keychain.decryptFile(ciphertext); const plaintext = await this.keychain.decryptFile(ciphertext);
if (this.cancelled) {
throw new Error(0);
}
if (!noSave) { if (!noSave) {
await saveFile({ await saveFile({
plaintext, plaintext,
@ -87,9 +77,8 @@ export default class FileReceiver extends Nanobus {
} }
this.msg = 'downloadFinish'; this.msg = 'downloadFinish';
this.state = 'complete'; this.state = 'complete';
return;
} catch (e) { } catch (e) {
this.state = 'invalid'; this.downloadRequest = null;
throw e; throw e;
} }
} }

View File

@ -9,11 +9,8 @@ export default class FileSender extends Nanobus {
constructor(file) { constructor(file) {
super('FileSender'); super('FileSender');
this.file = file; this.file = file;
this.uploadRequest = null;
this.msg = 'importingFile';
this.progress = [0, 1];
this.cancelled = false;
this.keychain = new Keychain(); this.keychain = new Keychain();
this.reset();
} }
get progressRatio() { 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() { cancel() {
this.cancelled = true; this.cancelled = true;
if (this.uploadRequest) { if (this.uploadRequest) {
@ -71,13 +75,13 @@ export default class FileSender extends Nanobus {
encrypted, encrypted,
metadata, metadata,
authKeyB64, authKeyB64,
this.keychain this.keychain,
); p => {
this.msg = 'fileSizeProgress';
this.uploadRequest.onprogress = p => {
this.progress = p; this.progress = p;
this.emit('progress', p); this.emit('progress', p);
}; }
);
this.msg = 'fileSizeProgress';
try { try {
const result = await this.uploadRequest.result; const result = await this.uploadRequest.result;
const time = Date.now() - start; const time = Date.now() - start;

View File

@ -16,11 +16,28 @@ describe('API', function() {
const encrypted = await keychain.encryptFile(plaintext); const encrypted = await keychain.encryptFile(plaintext);
const meta = await keychain.encryptMetadata(metadata); const meta = await keychain.encryptMetadata(metadata);
const verifierB64 = await keychain.authKeyB64(); 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; const result = await up.result;
assert.ok(result.url); assert.ok(result.url);
assert.ok(result.id); assert.ok(result.id);
assert.ok(result.ownerToken); 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');
}
});
}); });
}); });

View File

@ -87,6 +87,53 @@ describe('Upload / Download flow', function() {
assert.equal(fr.fileInfo.name, blob.name); 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.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() { it('can allow multiple downloads', async function() {
const fs = new FileSender(blob); const fs = new FileSender(blob);
const file = await fs.upload(); const file = await fs.upload();