Merge pull request #457 from mozilla/refactor-progress

factored out progress into progress.js
This commit is contained in:
Danny Coates 2017-08-07 09:51:29 -07:00 committed by GitHub
commit d2b57039bf
12 changed files with 229 additions and 240 deletions

View File

@ -1,144 +1,118 @@
const { Raven } = require('./common'); const { Raven } = require('./common');
const FileReceiver = require('./fileReceiver'); const FileReceiver = require('./fileReceiver');
const { notify, gcmCompliant } = require('./utils'); const { bytes, notify, gcmCompliant } = require('./utils');
const bytes = require('bytes');
const Storage = require('./storage'); const Storage = require('./storage');
const storage = new Storage(localStorage); const storage = new Storage(localStorage);
const links = require('./links'); const links = require('./links');
const metrics = require('./metrics'); const metrics = require('./metrics');
const progress = require('./progress');
const $ = require('jquery'); const $ = require('jquery');
require('jquery-circle-progress');
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 });
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 });
progress.setText(' ');
document.l10n
.formatValues('downloadNotification', 'downloadFinish')
.then(translated => {
notify(translated[0]);
$title.text(translated[1]);
});
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 $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));
gcmCompliant() gcmCompliant()
.then(() => { .then(() => {
const $downloadBtn = $('#download-btn'); $('#download-btn').on('click', download);
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 })})`
);
});
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();
}
});
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]);
});
}
});
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 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));
};
$downloadBtn.on('click', download);
}) })
.catch(err => { .catch(err => {
metrics.unsupported({ err }).then(() => { metrics.unsupported({ err }).then(() => {

View File

@ -62,7 +62,7 @@ class FileReceiver extends EventEmitter {
}); });
}) })
.then(([fdata, key]) => { .then(([fdata, key]) => {
this.emit('decrypting', true); this.emit('decrypting');
return Promise.all([ return Promise.all([
window.crypto.subtle window.crypto.subtle
.decrypt( .decrypt(
@ -76,7 +76,6 @@ class FileReceiver extends EventEmitter {
fdata.data fdata.data
) )
.then(decrypted => { .then(decrypted => {
this.emit('decrypting', false);
return Promise.resolve(decrypted); return Promise.resolve(decrypted);
}), }),
fdata.filename, fdata.filename,
@ -84,11 +83,10 @@ class FileReceiver extends EventEmitter {
]); ]);
}) })
.then(([decrypted, fname, proposedHash]) => { .then(([decrypted, fname, proposedHash]) => {
this.emit('hashing', true); this.emit('hashing');
return window.crypto.subtle return window.crypto.subtle
.digest('SHA-256', decrypted) .digest('SHA-256', decrypted)
.then(calculatedHash => { .then(calculatedHash => {
this.emit('hashing', false);
const integrity = const integrity =
new Uint8Array(calculatedHash).toString() === new Uint8Array(calculatedHash).toString() ===
proposedHash.toString(); proposedHash.toString();

View File

@ -34,7 +34,7 @@ class FileSender extends EventEmitter {
upload() { upload() {
const self = this; const self = this;
self.emit('loading', true); self.emit('loading');
return Promise.all([ return Promise.all([
window.crypto.subtle.generateKey( window.crypto.subtle.generateKey(
{ {
@ -48,12 +48,10 @@ class FileSender extends EventEmitter {
const reader = new FileReader(); const reader = new FileReader();
reader.readAsArrayBuffer(this.file); reader.readAsArrayBuffer(this.file);
reader.onload = function(event) { reader.onload = function(event) {
self.emit('loading', false); self.emit('hashing');
self.emit('hashing', true);
const plaintext = new Uint8Array(this.result); const plaintext = new Uint8Array(this.result);
window.crypto.subtle.digest('SHA-256', plaintext).then(hash => { window.crypto.subtle.digest('SHA-256', plaintext).then(hash => {
self.emit('hashing', false); self.emit('encrypting');
self.emit('encrypting', true);
resolve({ plaintext: plaintext, hash: new Uint8Array(hash) }); resolve({ plaintext: plaintext, hash: new Uint8Array(hash) });
}); });
}; };
@ -64,23 +62,16 @@ class FileSender extends EventEmitter {
]) ])
.then(([secretKey, file]) => { .then(([secretKey, file]) => {
return Promise.all([ return Promise.all([
window.crypto.subtle window.crypto.subtle.encrypt(
.encrypt( {
{ name: 'AES-GCM',
name: 'AES-GCM', iv: this.iv,
iv: this.iv, additionalData: file.hash,
additionalData: file.hash, tagLength: 128
tagLength: 128 },
}, secretKey,
secretKey, file.plaintext
file.plaintext ),
)
.then(encrypted => {
self.emit('encrypting', false);
return new Promise((resolve, reject) => {
resolve(encrypted);
});
}),
window.crypto.subtle.exportKey('jwk', secretKey), window.crypto.subtle.exportKey('jwk', secretKey),
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
resolve(file.hash); resolve(file.hash);

41
frontend/src/progress.js Normal file
View File

@ -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
};

View File

@ -2,18 +2,18 @@
const { Raven } = require('./common'); const { Raven } = require('./common');
const FileSender = require('./fileSender'); const FileSender = require('./fileSender');
const { const {
bytes,
copyToClipboard, copyToClipboard,
notify, notify,
gcmCompliant, gcmCompliant,
ONE_DAY_IN_MS ONE_DAY_IN_MS
} = require('./utils'); } = require('./utils');
const bytes = require('bytes');
const Storage = require('./storage'); const Storage = require('./storage');
const storage = new Storage(localStorage); const storage = new Storage(localStorage);
const metrics = require('./metrics'); const metrics = require('./metrics');
const progress = require('./progress');
const $ = require('jquery'); const $ = require('jquery');
require('jquery-circle-progress');
const allowedCopy = () => { const allowedCopy = () => {
const support = !!document.queryCommandSupported; const support = !!document.queryCommandSupported;
@ -27,10 +27,8 @@ $(() => {
const $copyBtn = $('#copy-btn'); const $copyBtn = $('#copy-btn');
const $link = $('#link'); const $link = $('#link');
const $uploadWindow = $('.upload-window'); const $uploadWindow = $('.upload-window');
const $ulProgress = $('#ul-progress');
const $uploadError = $('#upload-error'); const $uploadError = $('#upload-error');
const $uploadProgress = $('#upload-progress'); const $uploadProgress = $('#upload-progress');
const $progressText = $('.progress-text');
const $fileList = $('#file-list'); const $fileList = $('#file-list');
$pageOne.removeAttr('hidden'); $pageOne.removeAttr('hidden');
@ -96,15 +94,6 @@ $(() => {
$uploadWindow.removeClass('ondrag'); $uploadWindow.removeClass('ondrag');
}); });
//initiate progress bar
$ulProgress.circleProgress({
value: 0.0,
startAngle: -Math.PI / 2,
fill: '#3B9DFF',
size: 158,
animation: { duration: 300 }
});
//link back to homepage //link back to homepage
$('.send-new').attr('href', window.location); $('.send-new').attr('href', window.location);
@ -152,9 +141,15 @@ $(() => {
$pageOne.attr('hidden', true); $pageOne.attr('hidden', true);
$uploadError.attr('hidden', true); $uploadError.attr('hidden', true);
$uploadProgress.removeAttr('hidden'); $uploadProgress.removeAttr('hidden');
document.l10n.formatValue('importingFile').then(importingFile => { document.l10n
$progressText.text(importingFile); .formatValue('uploadingPageProgress', {
}); size: bytes(file.size),
filename: file.name
})
.then(str => {
$('#upload-filename').text(str);
});
document.l10n.formatValue('importingFile').then(progress.setText);
//don't allow drag and drop when not on page-one //don't allow drag and drop when not on page-one
$(document.body).off('drop', onUpload); $(document.body).off('drop', onUpload);
@ -168,40 +163,21 @@ $(() => {
location.reload(); location.reload();
}); });
fileSender.on('progress', progress => {
const percent = progress[0] / progress[1];
// update progress bar
$ulProgress.circleProgress('value', percent);
$ulProgress.circleProgress().on('circle-animation-end', function() {
$('.percent-number').text(`${Math.floor(percent * 100)}`);
});
$progressText.text(
`${file.name} (${bytes(progress[0], {
decimalPlaces: 1,
fixedDecimals: true
})} of ${bytes(progress[1], { decimalPlaces: 1 })})`
);
});
fileSender.on('hashing', isStillHashing => {
// The file is being hashed
if (isStillHashing) {
document.l10n.formatValue('verifyingFile').then(verifyingFile => {
$progressText.text(verifyingFile);
});
}
});
let uploadStart; let uploadStart;
fileSender.on('encrypting', isStillEncrypting => { fileSender.on('progress', data => {
// The file is being encrypted uploadStart = uploadStart || Date.now();
if (isStillEncrypting) { progress.setProgress({
document.l10n.formatValue('encryptingFile').then(encryptingFile => { complete: data[0],
$progressText.text(encryptingFile); total: data[1]
}); });
} else { });
uploadStart = Date.now();
} fileSender.on('hashing', () => {
document.l10n.formatValue('verifyingFile').then(progress.setText);
});
fileSender.on('encrypting', () => {
document.l10n.formatValue('encryptingFile').then(progress.setText);
}); });
let t; let t;
@ -244,16 +220,11 @@ $(() => {
}; };
storage.addFile(info.fileId, fileData); storage.addFile(info.fileId, fileData);
$('#upload-filename').attr(
'data-l10n-id', $pageOne.attr('hidden', true);
'uploadSuccessConfirmHeader' $uploadProgress.attr('hidden', true);
); $uploadError.attr('hidden', true);
t = window.setTimeout(() => { $('#share-link').removeAttr('hidden');
$pageOne.attr('hidden', true);
$uploadProgress.attr('hidden', true);
$uploadError.attr('hidden', true);
$('#share-link').removeAttr('hidden');
}, 1000);
populateFileList(fileData); populateFileList(fileData);
document.l10n.formatValue('notifyUploadDone').then(str => { document.l10n.formatValue('notifyUploadDone').then(str => {
@ -331,7 +302,7 @@ $(() => {
$link.attr('value', url); $link.attr('value', url);
$('#copy-text') $('#copy-text')
.attr('data-l10n-args', `{"filename": "${file.name}"}`) .attr('data-l10n-args', JSON.stringify({ filename: file.name }))
.attr('data-l10n-id', 'copyUrlFormLabelWithName'); .attr('data-l10n-id', 'copyUrlFormLabelWithName');
$popupText.attr('tabindex', '-1'); $popupText.attr('tabindex', '-1');

View File

@ -104,9 +104,29 @@ function copyToClipboard(str) {
return result; return result;
} }
const LOCALIZE_NUMBERS = !!(
typeof Intl === 'object' &&
Intl &&
typeof Intl.NumberFormat === 'function'
);
const UNITS = ['B', 'kB', 'MB', 'GB'];
function bytes(num) {
const exponent = Math.min(Math.floor(Math.log10(num) / 3), UNITS.length - 1);
const n = Number(num / Math.pow(1000, exponent));
const nStr = LOCALIZE_NUMBERS
? n.toLocaleString(navigator.languages, {
minimumFractionDigits: 1,
maximumFractionDigits: 1
})
: n.toFixed(1);
return `${nStr}${UNITS[exponent]}`;
}
const ONE_DAY_IN_MS = 86400000; const ONE_DAY_IN_MS = 86400000;
module.exports = { module.exports = {
bytes,
copyToClipboard, copyToClipboard,
arrayToHex, arrayToHex,
hexToArray, hexToArray,

5
package-lock.json generated
View File

@ -663,11 +663,6 @@
"readable-stream": "1.1.14" "readable-stream": "1.1.14"
} }
}, },
"bytes": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-2.5.0.tgz",
"integrity": "sha1-TJQj6i0lLCcMQbK97+/5u2tiwGo="
},
"cached-path-relative": { "cached-path-relative": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/cached-path-relative/-/cached-path-relative-1.0.1.tgz", "resolved": "https://registry.npmjs.org/cached-path-relative/-/cached-path-relative-1.0.1.tgz",

View File

@ -6,7 +6,6 @@
"dependencies": { "dependencies": {
"aws-sdk": "^2.89.0", "aws-sdk": "^2.89.0",
"body-parser": "^1.17.2", "body-parser": "^1.17.2",
"bytes": "^2.5.0",
"connect-busboy": "0.0.2", "connect-busboy": "0.0.2",
"convict": "^3.0.0", "convict": "^3.0.0",
"express": "^4.15.3", "express": "^4.15.3",

View File

@ -11,7 +11,7 @@ uploadPageBrowseButton = Select a file on your computer
.title = Select a file on your computer .title = Select a file on your computer
uploadPageMultipleFilesAlert = Uploading multiple files or a folder is currently not supported. uploadPageMultipleFilesAlert = Uploading multiple files or a folder is currently not supported.
uploadPageBrowseButtonTitle = Upload file uploadPageBrowseButtonTitle = Upload file
uploadingPageHeader = Uploading Your File uploadingPageProgress = Uploading { $filename } ({ $size })
importingFile = Importing… importingFile = Importing…
verifyingFile = Verifying… verifyingFile = Verifying…
encryptingFile = Encrypting… encryptingFile = Encrypting…
@ -50,6 +50,8 @@ downloadButtonLabel = Download
.title = Download .title = Download
downloadNotification = Your download has completed. downloadNotification = Your download has completed.
downloadFinish = Download Complete downloadFinish = Download Complete
// This message is displayed when uploading or downloading a file, e.g. "(1,3 MB of 10 MB)".
fileSizeProgress = ({ $partialSize } of { $totalSize })
// Firefox Send is a brand name and should not be localized. Title text for button should be the same. // Firefox Send is a brand name and should not be localized. Title text for button should be the same.
sendYourFilesLink = Try Firefox Send sendYourFilesLink = Try Firefox Send
.title = Try Firefox Send .title = Try Firefox Send

View File

@ -4,7 +4,6 @@ const busboy = require('connect-busboy');
const path = require('path'); const path = require('path');
const bodyParser = require('body-parser'); const bodyParser = require('body-parser');
const helmet = require('helmet'); const helmet = require('helmet');
const bytes = require('bytes');
const conf = require('./config.js'); const conf = require('./config.js');
const storage = require('./storage.js'); const storage = require('./storage.js');
const Raven = require('raven'); const Raven = require('raven');
@ -141,13 +140,15 @@ app.get('/download/:id', async (req, res) => {
} }
try { try {
const filename = await storage.filename(id); const efilename = await storage.filename(id);
const contentLength = await storage.length(id); const filename = decodeURIComponent(efilename);
const filenameJson = JSON.stringify({ filename });
const sizeInBytes = await storage.length(id);
const ttl = await storage.ttl(id); const ttl = await storage.ttl(id);
res.render('download', { res.render('download', {
filename: decodeURIComponent(filename), filename,
filesize: bytes(contentLength), filenameJson,
sizeInBytes: contentLength, sizeInBytes,
ttl ttl
}); });
} catch (e) { } catch (e) {

View File

@ -2,13 +2,13 @@
<script src="/download.js"></script> <script src="/download.js"></script>
<div id="download-page-one"> <div id="download-page-one">
<div class="title"> <div class="title">
<span id="dl-filename" <span id="dl-file"
data-filename="{{filename}}"
data-size="{{sizeInBytes}}"
data-ttl="{{ttl}}"
data-l10n-id="downloadFileName" data-l10n-id="downloadFileName"
data-l10n-args='{"filename": "{{filename}}"}'></span> data-l10n-args='{{filenameJson}}'></span>
<span data-l10n-id="downloadFileSize" <span id="dl-filesize"></span>
data-l10n-args='{"size": "{{filesize}}"}'></span>
<span id="dl-size" hidden="true">{{sizeInBytes}}</span>
<span id="dl-ttl" hidden="true">{{ttl}}</span>
</div> </div>
<div class="description" data-l10n-id="downloadMessage"></div> <div class="description" data-l10n-id="downloadMessage"></div>
<img src="/resources/illustration_download.svg" id="download-img" data-l10n-id="downloadAltText"/> <img src="/resources/illustration_download.svg" id="download-img" data-l10n-id="downloadAltText"/>
@ -18,10 +18,7 @@
</div> </div>
<div id="download-progress" hidden="true"> <div id="download-progress" hidden="true">
<div class="title" <div id="dl-title" class="title"></div>
data-l10n-id="downloadingPageProgress"
data-l10n-args='{"filename": "{{filename}}", "size": "{{filesize}}"}'>
</div>
<div class="description" data-l10n-id="downloadingPageMessage"></div> <div class="description" data-l10n-id="downloadingPageMessage"></div>
<!-- progress bar here --> <!-- progress bar here -->
<div class="progress-bar" id="dl-progress"> <div class="progress-bar" id="dl-progress">

View File

@ -35,7 +35,7 @@
</div> </div>
<div id="upload-progress" hidden="true"> <div id="upload-progress" hidden="true">
<div class="title" id="upload-filename" data-l10n-id="uploadingPageHeader"></div> <div class="title" id="upload-filename"></div>
<div class="description"></div> <div class="description"></div>
<!-- progress bar here --> <!-- progress bar here -->
<div class="progress-bar" id="ul-progress"> <div class="progress-bar" id="ul-progress">