Skip to content
This repository has been archived by the owner on May 22, 2021. It is now read-only.

factored out progress into progress.js #457

Merged
merged 1 commit into from
Aug 7, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
228 changes: 101 additions & 127 deletions frontend/src/download.js
Original file line number Diff line number Diff line change
@@ -1,144 +1,118 @@
const { Raven } = require('./common');
const FileReceiver = require('./fileReceiver');
const { notify, gcmCompliant } = require('./utils');
const bytes = require('bytes');
const { bytes, notify, gcmCompliant } = require('./utils');
const Storage = require('./storage');
const storage = new Storage(localStorage);
const links = require('./links');
const metrics = require('./metrics');

const progress = require('./progress');
const $ = require('jquery');
require('jquery-circle-progress');

$(() => {
gcmCompliant()
.then(() => {
const $downloadBtn = $('#download-btn');
const $dlProgress = $('#dl-progress');
const $progressText = $('.progress-text');
const $title = $('.title');

const filename = $('#dl-filename').text();
const size = Number($('#dl-size').text());
const ttl = Number($('#dl-ttl').text());

//initiate progress bar
$dlProgress.circleProgress({
value: 0.0,
startAngle: -Math.PI / 2,
fill: '#3B9DFF',
size: 158,
animation: { duration: 300 }
});

const download = () => {
// Disable the download button to avoid accidental double clicks.
$downloadBtn.attr('disabled', 'disabled');
links.setOpenInNewTab(true);

const fileReceiver = new FileReceiver();

fileReceiver.on('progress', progress => {
window.onunload = function() {
metrics.cancelledDownload({ size });
};

$('#download-page-one').attr('hidden', true);
$('#download-progress').removeAttr('hidden');
const percent = progress[0] / progress[1];
// update progress bar
$dlProgress.circleProgress('value', percent);
$('.percent-number').text(`${Math.floor(percent * 100)}`);
$progressText.text(
`${filename} (${bytes(progress[0], {
decimalPlaces: 1,
fixedDecimals: true
})} of ${bytes(progress[1], { decimalPlaces: 1 })})`
);
});
function onUnload(size) {
metrics.cancelledDownload({ size });
}

function download() {
const $downloadBtn = $('#download-btn');
const $title = $('.title');
const $file = $('#dl-file');
const size = Number($file.attr('data-size'));
const ttl = Number($file.attr('data-ttl'));
const unloadHandler = onUnload.bind(null, size);
const startTime = Date.now();
const fileReceiver = new FileReceiver();

$downloadBtn.attr('disabled', 'disabled');
$('#download-page-one').attr('hidden', true);
$('#download-progress').removeAttr('hidden');
metrics.startedDownload({ size, ttl });
links.setOpenInNewTab(true);
window.addEventListener('unload', unloadHandler);

fileReceiver.on('progress', data => {
progress.setProgress({ complete: data[0], total: data[1] });
});

let downloadEnd;
fileReceiver.on('decrypting', () => {
downloadEnd = Date.now();
window.removeEventListener('unload', unloadHandler);
fileReceiver.removeAllListeners('progress');
document.l10n.formatValue('decryptingFile').then(progress.setText);
});

fileReceiver.on('hashing', () => {
document.l10n.formatValue('verifyingFile').then(progress.setText);
});

fileReceiver
.download()
.catch(err => {
metrics.stoppedDownload({ size, err });

let downloadEnd;
fileReceiver.on('decrypting', isStillDecrypting => {
// The file is being decrypted
if (isStillDecrypting) {
fileReceiver.removeAllListeners('progress');
window.onunload = null;
document.l10n.formatValue('decryptingFile').then(decryptingFile => {
$progressText.text(decryptingFile);
});
} else {
downloadEnd = Date.now();
}
if (err.message === 'notfound') {
location.reload();
} else {
document.l10n.formatValue('errorPageHeader').then(translated => {
$title.text(translated);
});

fileReceiver.on('hashing', isStillHashing => {
// The file is being hashed to make sure a malicious user hasn't tampered with it
if (isStillHashing) {
document.l10n.formatValue('verifyingFile').then(verifyingFile => {
$progressText.text(verifyingFile);
});
} else {
$progressText.text(' ');
document.l10n
.formatValues('downloadNotification', 'downloadFinish')
.then(translated => {
notify(translated[0]);
$title.text(translated[1]);
});
}
$downloadBtn.attr('hidden', true);
$('#expired-img').removeAttr('hidden');
}
throw err;
})
.then(([decrypted, fname]) => {
const endTime = Date.now();
const time = endTime - startTime;
const downloadTime = endTime - downloadEnd;
const speed = size / (downloadTime / 1000);
storage.totalDownloads += 1;
metrics.completedDownload({ size, time, speed });
progress.setText(' ');
document.l10n
.formatValues('downloadNotification', 'downloadFinish')
.then(translated => {
notify(translated[0]);
$title.text(translated[1]);
});

const startTime = Date.now();

metrics.startedDownload({ size, ttl });

fileReceiver
.download()
.catch(err => {
metrics.stoppedDownload({ size, err });

if (err.message === 'notfound') {
location.reload();
} else {
document.l10n.formatValue('errorPageHeader').then(translated => {
$title.text(translated);
});
$downloadBtn.attr('hidden', true);
$('#expired-img').removeAttr('hidden');
}
throw err;
})
.then(([decrypted, fname]) => {
const endTime = Date.now();
const time = endTime - startTime;
const downloadTime = endTime - downloadEnd;
const speed = size / (downloadTime / 1000);
storage.totalDownloads += 1;
metrics.completedDownload({ size, time, speed });

const dataView = new DataView(decrypted);
const blob = new Blob([dataView]);
const downloadUrl = URL.createObjectURL(blob);
const dataView = new DataView(decrypted);
const blob = new Blob([dataView]);
const downloadUrl = URL.createObjectURL(blob);

const a = document.createElement('a');
a.href = downloadUrl;
if (window.navigator.msSaveBlob) {
// if we are in microsoft edge or IE
window.navigator.msSaveBlob(blob, fname);
return;
}
a.download = fname;
document.body.appendChild(a);
a.click();
})
.catch(err => {
Raven.captureException(err);
return Promise.reject(err);
})
.then(() => links.setOpenInNewTab(false));
}

const a = document.createElement('a');
a.href = downloadUrl;
if (window.navigator.msSaveBlob) {
// if we are in microsoft edge or IE
window.navigator.msSaveBlob(blob, fname);
return;
}
a.download = fname;
document.body.appendChild(a);
a.click();
})
.catch(err => {
Raven.captureException(err);
return Promise.reject(err);
})
.then(() => links.setOpenInNewTab(false));
};
$(() => {
const $file = $('#dl-file');
const filename = $file.attr('data-filename');
const b = Number($file.attr('data-size'));
const size = bytes(b);
document.l10n
.formatValue('downloadFileSize', { size })
.then(str => $('#dl-filesize').text(str));
document.l10n
.formatValue('downloadingPageProgress', { filename, size })
.then(str => $('#dl-title').text(str));

$downloadBtn.on('click', download);
gcmCompliant()
.then(() => {
$('#download-btn').on('click', download);
})
.catch(err => {
metrics.unsupported({ err }).then(() => {
Expand Down
6 changes: 2 additions & 4 deletions frontend/src/fileReceiver.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ class FileReceiver extends EventEmitter {
});
})
.then(([fdata, key]) => {
this.emit('decrypting', true);
this.emit('decrypting');
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Turns out we don't really need the start/finish part of these events. Removing the finished case simplifies the listeners.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although I don't think it affects the outcome of the tests, I think we should change the frontend tests to reflect that these listeners only get called once.

return Promise.all([
window.crypto.subtle
.decrypt(
Expand All @@ -76,19 +76,17 @@ class FileReceiver extends EventEmitter {
fdata.data
)
.then(decrypted => {
this.emit('decrypting', false);
return Promise.resolve(decrypted);
}),
fdata.filename,
hexToArray(fdata.aad)
]);
})
.then(([decrypted, fname, proposedHash]) => {
this.emit('hashing', true);
this.emit('hashing');
return window.crypto.subtle
.digest('SHA-256', decrypted)
.then(calculatedHash => {
this.emit('hashing', false);
const integrity =
new Uint8Array(calculatedHash).toString() ===
proposedHash.toString();
Expand Down
35 changes: 13 additions & 22 deletions frontend/src/fileSender.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class FileSender extends EventEmitter {

upload() {
const self = this;
self.emit('loading', true);
self.emit('loading');
return Promise.all([
window.crypto.subtle.generateKey(
{
Expand All @@ -48,12 +48,10 @@ class FileSender extends EventEmitter {
const reader = new FileReader();
reader.readAsArrayBuffer(this.file);
reader.onload = function(event) {
self.emit('loading', false);
self.emit('hashing', true);
self.emit('hashing');
const plaintext = new Uint8Array(this.result);
window.crypto.subtle.digest('SHA-256', plaintext).then(hash => {
self.emit('hashing', false);
self.emit('encrypting', true);
self.emit('encrypting');
resolve({ plaintext: plaintext, hash: new Uint8Array(hash) });
});
};
Expand All @@ -64,23 +62,16 @@ class FileSender extends EventEmitter {
])
.then(([secretKey, file]) => {
return Promise.all([
window.crypto.subtle
.encrypt(
{
name: 'AES-GCM',
iv: this.iv,
additionalData: file.hash,
tagLength: 128
},
secretKey,
file.plaintext
)
.then(encrypted => {
self.emit('encrypting', false);
return new Promise((resolve, reject) => {
resolve(encrypted);
});
}),
window.crypto.subtle.encrypt(
{
name: 'AES-GCM',
iv: this.iv,
additionalData: file.hash,
tagLength: 128
},
secretKey,
file.plaintext
),
window.crypto.subtle.exportKey('jwk', secretKey),
new Promise((resolve, reject) => {
resolve(file.hash);
Expand Down
41 changes: 41 additions & 0 deletions frontend/src/progress.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
const { bytes } = require('./utils');
const $ = require('jquery');
require('jquery-circle-progress');

let $progress = null;
let $percent = null;
let $text = null;

document.addEventListener('DOMContentLoaded', function() {
$percent = $('.percent-number');
$text = $('.progress-text');
$progress = $('.progress-bar');
$progress.circleProgress({
value: 0.0,
startAngle: -Math.PI / 2,
fill: '#3B9DFF',
size: 158,
animation: { duration: 300 }
});
});

function setProgress(params) {
const percent = params.complete / params.total;
$progress.circleProgress('value', percent);
$percent.text(`${Math.floor(percent * 100)}`);
document.l10n
.formatValue('fileSizeProgress', {
partialSize: bytes(params.complete),
totalSize: bytes(params.total)
})
.then(setText);
}

function setText(str) {
$text.text(str);
}

module.exports = {
setProgress,
setText
};
Loading