more frontend tests and some factoring based on them
This commit is contained in:
parent
78728ce4ca
commit
d6c0489fa3
115
app/api.js
115
app/api.js
|
@ -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,79 +137,63 @@ 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() {},
|
xhr.abort();
|
||||||
cancel: function() {
|
|
||||||
xhr.abort();
|
|
||||||
},
|
|
||||||
result: new Promise(async function(resolve, reject) {
|
|
||||||
xhr.addEventListener('loadend', function() {
|
|
||||||
const authHeader = xhr.getResponseHeader('WWW-Authenticate');
|
|
||||||
if (authHeader) {
|
|
||||||
keychain.nonce = parseNonce(authHeader);
|
|
||||||
}
|
|
||||||
if (xhr.status !== 200) {
|
|
||||||
return reject(new Error(xhr.status));
|
|
||||||
}
|
|
||||||
|
|
||||||
const blob = new Blob([xhr.response]);
|
|
||||||
const fileReader = new FileReader();
|
|
||||||
fileReader.readAsArrayBuffer(blob);
|
|
||||||
fileReader.onload = function() {
|
|
||||||
resolve(this.result);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
xhr.addEventListener('progress', function(event) {
|
|
||||||
if (event.lengthComputable && event.target.status === 200) {
|
|
||||||
download.onprogress([event.loaded, event.total]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const auth = await keychain.authHeader();
|
|
||||||
xhr.open('get', `/api/download/${id}`);
|
|
||||||
xhr.setRequestHeader('Authorization', auth);
|
|
||||||
xhr.responseType = 'blob';
|
|
||||||
xhr.send();
|
|
||||||
})
|
|
||||||
};
|
};
|
||||||
|
return new Promise(async function(resolve, reject) {
|
||||||
|
xhr.addEventListener('loadend', function() {
|
||||||
|
canceller.oncancel = function() {};
|
||||||
|
const authHeader = xhr.getResponseHeader('WWW-Authenticate');
|
||||||
|
if (authHeader) {
|
||||||
|
keychain.nonce = parseNonce(authHeader);
|
||||||
|
}
|
||||||
|
if (xhr.status !== 200) {
|
||||||
|
return reject(new Error(xhr.status));
|
||||||
|
}
|
||||||
|
|
||||||
return download;
|
const blob = new Blob([xhr.response]);
|
||||||
|
const fileReader = new FileReader();
|
||||||
|
fileReader.readAsArrayBuffer(blob);
|
||||||
|
fileReader.onload = function() {
|
||||||
|
resolve(this.result);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
xhr.addEventListener('progress', function(event) {
|
||||||
|
if (event.lengthComputable && event.target.status === 200) {
|
||||||
|
onprogress([event.loaded, event.total]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const auth = await keychain.authHeader();
|
||||||
|
xhr.open('get', `/api/download/${id}`);
|
||||||
|
xhr.setRequestHeader('Authorization', auth);
|
||||||
|
xhr.responseType = 'blob';
|
||||||
|
xhr.send();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.progress = p;
|
||||||
|
this.emit('progress', p);
|
||||||
|
}
|
||||||
);
|
);
|
||||||
this.msg = 'fileSizeProgress';
|
this.msg = 'fileSizeProgress';
|
||||||
this.uploadRequest.onprogress = p => {
|
|
||||||
this.progress = p;
|
|
||||||
this.emit('progress', p);
|
|
||||||
};
|
|
||||||
try {
|
try {
|
||||||
const result = await this.uploadRequest.result;
|
const result = await this.uploadRequest.result;
|
||||||
const time = Date.now() - start;
|
const time = Date.now() - start;
|
||||||
|
|
|
@ -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');
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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();
|
||||||
|
|
Loading…
Reference in New Issue