diff --git a/.dockerignore b/.dockerignore
index 7522f1ba..ac82f884 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -5,3 +5,4 @@ static
test
scripts
docs
+firefox
diff --git a/.eslintignore b/.eslintignore
index a5892099..a435bfcc 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -1,4 +1,3 @@
-public/bundle.js
-public/webcrypto-shim.js
+public
test/frontend/bundle.js
-firefox
\ No newline at end of file
+firefox
diff --git a/.gitignore b/.gitignore
index e1f30354..dc0910df 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,9 @@
.DS_Store
node_modules
-public/bundle.js
+public/upload.js
+public/download.js
public/version.json
+public/l20n.min.js
static/*
!static/info.txt
test/frontend/bundle.js
diff --git a/.stylelintrc b/.stylelintrc
index 3c593e83..c0c673c7 100644
--- a/.stylelintrc
+++ b/.stylelintrc
@@ -1,6 +1,6 @@
extends: stylelint-config-standard
rules:
- color-hex-case: upper
+ color-hex-case: lower
declaration-colon-newline-after: null
selector-list-comma-newline-after: null
diff --git a/circle.yml b/circle.yml
index ed714bbd..e25fa992 100644
--- a/circle.yml
+++ b/circle.yml
@@ -16,7 +16,7 @@ deployment:
latest:
branch: master
commands:
- - npm run predocker
+ - npm run build
- docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS
- docker build -t mozilla/send:latest .
- docker push mozilla/send:latest
@@ -24,7 +24,7 @@ deployment:
tag: /.*/
owner: mozilla
commands:
- - npm run predocker
+ - npm run build
- docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS
- docker build -t mozilla/send:$CIRCLE_TAG .
- docker push mozilla/send:$CIRCLE_TAG
diff --git a/docker-compose.yml b/docker-compose.yml
index 8274bde1..f72bf161 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -7,6 +7,6 @@ services:
ports:
- "1443:1443"
environment:
- - P2P_REDIS_HOST=redis
+ - REDIS_HOST=redis
redis:
image: redis:alpine
diff --git a/docs/docker.md b/docs/docker.md
index f45b770f..f94b23b3 100644
--- a/docs/docker.md
+++ b/docs/docker.md
@@ -3,12 +3,22 @@
| Name | Description
|------------------|-------------|
| `PORT` | Port the server will listen on (defaults to 1443).
-| `P2P_S3_BUCKET` | The S3 bucket name.
-| `P2P_REDIS_HOST` | Host name of the Redis server.
+| `S3_BUCKET` | The S3 bucket name.
+| `REDIS_HOST` | Host name of the Redis server.
+| `GOOGLE_ANALYTICS_ID` | Google Analytics ID
+| `SENTRY_CLIENT` | Sentry Client ID
+| `SENTRY_DSN` | Sentry DSN
+| `MAX_FILE_SIZE` | in bytes (defaults to 2147483648)
| `NODE_ENV` | "production"
## Example:
```sh
-$ docker run --net=host -e 'NODE_ENV=production' -e 'P2P_S3_BUCKET=testpilot-p2p-dev' -e 'P2P_REDIS_HOST=dyf9s2r4vo3.bolxr4.0001.usw2.cache.amazonaws.com' mozilla/send:latest
+$ docker run --net=host -e 'NODE_ENV=production' \
+ -e 'S3_BUCKET=testpilot-p2p-dev' \
+ -e 'REDIS_HOST=dyf9s2r4vo3.bolxr4.0001.usw2.cache.amazonaws.com' \
+ -e 'GOOGLE_ANALYTICS_ID=UA-35433268-78' \
+ -e 'SENTRY_CLIENT=https://51e23d7263e348a7a3b90a5357c61cb2@sentry.prod.mozaws.net/168' \
+ -e 'SENTRY_DSN=https://51e23d7263e348a7a3b90a5357c61cb2:65e23d7263e348a7a3b90a5357c61c44@sentry.prod.mozaws.net/168' \
+ mozilla/send:latest
```
diff --git a/docs/metrics.md b/docs/metrics.md
index 035a57fa..54d27deb 100644
--- a/docs/metrics.md
+++ b/docs/metrics.md
@@ -112,6 +112,7 @@ Fired whenever a user deletes a file they’ve uploaded.
- `cm6`
- `cm7`
- `cd1`
+- `cd4`
#### `copied`
Fired whenever a user copies the URL of an upload file.
diff --git a/frontend/src/common.js b/frontend/src/common.js
new file mode 100644
index 00000000..5fa712a3
--- /dev/null
+++ b/frontend/src/common.js
@@ -0,0 +1,10 @@
+window.Raven = require('raven-js');
+window.Raven.config(window.dsn).install();
+window.dsn = undefined;
+
+const testPilotGA = require('testpilot-ga');
+window.analytics = new testPilotGA({
+ an: 'Firefox Send',
+ ds: 'web',
+ tid: window.trackerId
+});
diff --git a/frontend/src/download.js b/frontend/src/download.js
index a6d96449..b0e05fe9 100644
--- a/frontend/src/download.js
+++ b/frontend/src/download.js
@@ -1,90 +1,175 @@
+require('./common');
const FileReceiver = require('./fileReceiver');
-const { notify } = require('./utils');
+const { notify, findMetric, gcmCompliant, sendEvent } = require('./utils');
+const bytes = require('bytes');
+const Storage = require('./storage');
+const storage = new Storage(localStorage);
+
const $ = require('jquery');
require('jquery-circle-progress');
const Raven = window.Raven;
+
$(document).ready(function() {
+ gcmCompliant().catch(err => {
+ $('#download').attr('hidden', true);
+ sendEvent('recipient', 'unsupported', {
+ cd6: err
+ }).then(() => {
+ location.replace('/unsupported');
+ });
+ });
//link back to homepage
$('.send-new').attr('href', window.location.origin);
- const filename = $('#dl-filename').html();
+ $('.send-new').click(function(target) {
+ target.preventDefault();
+ sendEvent('recipient', 'restarted', {
+ cd2: 'completed'
+ }).then(() => {
+ location.href = target.currentTarget.href;
+ });
+ });
+
+ $('.legal-links a, .social-links a, #dl-firefox').click(function(target) {
+ target.preventDefault();
+ const metric = findMetric(target.currentTarget.href);
+ // record exited event by recipient
+ sendEvent('recipient', 'exited', {
+ cd3: metric
+ }).then(() => {
+ location.href = target.currentTarget.href;
+ });
+ });
+
+ const filename = $('#dl-filename').text();
+ const bytelength = Number($('#dl-bytelength').text());
+ const timeToExpiry = Number($('#dl-ttl').text());
//initiate progress bar
$('#dl-progress').circleProgress({
value: 0.0,
startAngle: -Math.PI / 2,
- fill: '#00C8D7',
+ fill: '#3B9DFF',
size: 158,
animation: { duration: 300 }
});
$('#download-btn').click(download);
function download() {
+ storage.totalDownloads += 1;
+
const fileReceiver = new FileReceiver();
+ const unexpiredFiles = storage.numFiles;
fileReceiver.on('progress', progress => {
+ window.onunload = function() {
+ storage.referrer = 'cancelled-download';
+ // record download-stopped (cancelled by tab close or reload)
+ sendEvent('recipient', 'download-stopped', {
+ cm1: bytelength,
+ cm5: storage.totalUploads,
+ cm6: unexpiredFiles,
+ cm7: storage.totalDownloads,
+ cd2: 'cancelled'
+ });
+ };
+
$('#download-page-one').attr('hidden', true);
$('#download-progress').removeAttr('hidden');
const percent = progress[0] / progress[1];
// update progress bar
$('#dl-progress').circleProgress('value', percent);
- $('.percent-number').html(`${Math.floor(percent * 100)}`);
- if (progress[1] < 1000000) {
- $('.progress-text').html(
- `${filename} (${(progress[0] / 1000).toFixed(1)}KB of
- ${(progress[1] / 1000).toFixed(1)}KB)`
- );
- } else if (progress[1] < 1000000000) {
- $('.progress-text').html(
- `${filename} (${(progress[0] / 1000000).toFixed(1)}MB of ${(progress[1] / 1000000).toFixed(1)}MB)`
- );
- } else {
- $('.progress-text').html(
- `${filename} (${(progress[0] / 1000000).toFixed(1)}MB of ${(progress[1] / 1000000000).toFixed(1)}GB)`
- );
- }
- //on complete
- if (percent === 1) {
- fileReceiver.removeAllListeners('progress');
- document.l10n.formatValues('downloadNotification', 'downloadFinish')
- .then(translated => {
- notify(translated[0]);
- $('.title').html(translated[1]);
- });
- }
+ $('.percent-number').text(`${Math.floor(percent * 100)}`);
+ $('.progress-text').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) {
- console.log('Decrypting');
+ fileReceiver.removeAllListeners('progress');
+ window.onunload = null;
+ document.l10n.formatValue('decryptingFile').then(decryptingFile => {
+ $('.progress-text').text(decryptingFile);
+ });
} else {
console.log('Done decrypting');
+ 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) {
- console.log('Checking file integrity');
+ document.l10n.formatValue('verifyingFile').then(verifyingFile => {
+ $('.progress-text').text(verifyingFile);
+ });
} else {
- console.log('Integrity check done');
+ $('.progress-text').text(' ');
+ document.l10n
+ .formatValues('downloadNotification', 'downloadFinish')
+ .then(translated => {
+ notify(translated[0]);
+ $('.title').text(translated[1]);
+ });
}
});
+ const startTime = Date.now();
+
+ // record download-started by recipient
+ sendEvent('recipient', 'download-started', {
+ cm1: bytelength,
+ cm4: timeToExpiry,
+ cm5: storage.totalUploads,
+ cm6: unexpiredFiles,
+ cm7: storage.totalDownloads
+ });
+
fileReceiver
.download()
- .catch(() => {
- document.l10n.formatValue('expiredPageHeader')
- .then(translated => {
- $('.title').text(translated);
- });
+ .catch(err => {
+ // record download-stopped (errored) by recipient
+ sendEvent('recipient', 'download-stopped', {
+ cm1: bytelength,
+ cm5: storage.totalUploads,
+ cm6: unexpiredFiles,
+ cm7: storage.totalDownloads,
+ cd2: 'errored',
+ cd6: err
+ });
+
+ document.l10n.formatValue('expiredPageHeader').then(translated => {
+ $('.title').text(translated);
+ });
$('#download-btn').attr('hidden', true);
$('#expired-img').removeAttr('hidden');
console.log('The file has expired, or has already been deleted.');
return;
})
.then(([decrypted, fname]) => {
+ const endTime = Date.now();
+ const totalTime = endTime - startTime;
+ const downloadTime = endTime - downloadEnd;
+ const downloadSpeed = bytelength / (downloadTime / 1000);
+
+ storage.referrer = 'completed-download';
+ // record download-stopped (completed) by recipient
+ sendEvent('recipient', 'download-stopped', {
+ cm1: bytelength,
+ cm2: totalTime,
+ cm3: downloadSpeed,
+ cm5: storage.totalUploads,
+ cm6: unexpiredFiles,
+ cm7: storage.totalDownloads,
+ cd2: 'completed'
+ });
+
const dataView = new DataView(decrypted);
const blob = new Blob([dataView]);
const downloadUrl = URL.createObjectURL(blob);
diff --git a/frontend/src/fileReceiver.js b/frontend/src/fileReceiver.js
index 155a8dd9..882dcb2b 100644
--- a/frontend/src/fileReceiver.js
+++ b/frontend/src/fileReceiver.js
@@ -58,41 +58,47 @@ class FileReceiver extends EventEmitter {
true,
['encrypt', 'decrypt']
)
- ]).then(([fdata, key]) => {
- this.emit('decrypting', true);
- return Promise.all([
- window.crypto.subtle.decrypt(
- {
- name: 'AES-GCM',
- iv: hexToArray(fdata.iv),
- additionalData: hexToArray(fdata.aad)
- },
- key,
- 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);
- return window.crypto.subtle.digest('SHA-256', decrypted).then(calculatedHash => {
- this.emit('hashing', false);
- const integrity = new Uint8Array(calculatedHash).toString() === proposedHash.toString();
- if (!integrity) {
- this.emit('unsafe', true)
- return Promise.reject();
- } else {
- this.emit('safe', true);
- return Promise.all([
- decrypted,
- decodeURIComponent(fname)
- ]);
- }
+ ])
+ .then(([fdata, key]) => {
+ this.emit('decrypting', true);
+ return Promise.all([
+ window.crypto.subtle
+ .decrypt(
+ {
+ name: 'AES-GCM',
+ iv: hexToArray(fdata.iv),
+ additionalData: hexToArray(fdata.aad),
+ tagLength: 128
+ },
+ key,
+ 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);
+ return window.crypto.subtle
+ .digest('SHA-256', decrypted)
+ .then(calculatedHash => {
+ this.emit('hashing', false);
+ const integrity =
+ new Uint8Array(calculatedHash).toString() ===
+ proposedHash.toString();
+ if (!integrity) {
+ this.emit('unsafe', true);
+ return Promise.reject();
+ } else {
+ this.emit('safe', true);
+ return Promise.all([decrypted, decodeURIComponent(fname)]);
+ }
+ });
+ });
}
}
diff --git a/frontend/src/fileSender.js b/frontend/src/fileSender.js
index a16b1849..97c76deb 100644
--- a/frontend/src/fileSender.js
+++ b/frontend/src/fileSender.js
@@ -118,14 +118,16 @@ class FileSender extends EventEmitter {
xhr.onreadystatechange = () => {
if (xhr.readyState === XMLHttpRequest.DONE) {
- // uuid field and url field
- const responseObj = JSON.parse(xhr.responseText);
- resolve({
- url: responseObj.url,
- fileId: responseObj.id,
- secretKey: keydata.k,
- deleteToken: responseObj.delete
- });
+ if (xhr.status === 200) {
+ const responseObj = JSON.parse(xhr.responseText);
+ return resolve({
+ url: responseObj.url,
+ fileId: responseObj.id,
+ secretKey: keydata.k,
+ deleteToken: responseObj.delete
+ });
+ }
+ reject(xhr.status);
}
};
diff --git a/frontend/src/main.js b/frontend/src/main.js
deleted file mode 100644
index 12c05a38..00000000
--- a/frontend/src/main.js
+++ /dev/null
@@ -1,5 +0,0 @@
-window.Raven = require('raven-js');
-window.Raven.config(window.dsn).install();
-window.dsn = undefined;
-require('./upload');
-require('./download');
diff --git a/frontend/src/storage.js b/frontend/src/storage.js
new file mode 100644
index 00000000..c95d93a6
--- /dev/null
+++ b/frontend/src/storage.js
@@ -0,0 +1,66 @@
+const { isFile } = require('./utils');
+
+class Storage {
+ constructor(engine) {
+ this.engine = engine;
+ }
+
+ get totalDownloads() {
+ return Number(this.engine.getItem('totalDownloads'));
+ }
+ set totalDownloads(n) {
+ this.engine.setItem('totalDownloads', n);
+ }
+ get totalUploads() {
+ return Number(this.engine.getItem('totalUploads'));
+ }
+ set totalUploads(n) {
+ this.engine.setItem('totalUploads', n);
+ }
+ get referrer() {
+ return this.engine.getItem('referrer');
+ }
+ set referrer(str) {
+ this.engine.setItem('referrer', str);
+ }
+
+ get files() {
+ const fs = [];
+ for (let i = 0; i < this.engine.length; i++) {
+ const k = this.engine.key(i);
+ if (isFile(k)) {
+ fs.push(JSON.parse(this.engine.getItem(k))); // parse or whatever else
+ }
+ }
+ return fs;
+ }
+
+ get numFiles() {
+ let length = 0;
+ for (let i = 0; i < this.engine.length; i++) {
+ const k = this.engine.key(i);
+ if (isFile(k)) {
+ length += 1;
+ }
+ }
+ return length;
+ }
+
+ getFileById(id) {
+ return this.engine.getItem(id);
+ }
+
+ has(property) {
+ return this.engine.hasOwnProperty(property);
+ }
+
+ remove(property) {
+ this.engine.removeItem(property);
+ }
+
+ addFile(id, file) {
+ this.engine.setItem(id, JSON.stringify(file));
+ }
+}
+
+module.exports = Storage;
diff --git a/frontend/src/upload.js b/frontend/src/upload.js
index 82f6c55b..84279760 100644
--- a/frontend/src/upload.js
+++ b/frontend/src/upload.js
@@ -1,17 +1,74 @@
+/* global MAXFILESIZE EXPIRE_SECONDS */
+require('./common');
const FileSender = require('./fileSender');
-const { notify, gcmCompliant } = require('./utils');
+const {
+ notify,
+ gcmCompliant,
+ findMetric,
+ sendEvent,
+ ONE_DAY_IN_MS
+} = require('./utils');
+const bytes = require('bytes');
+const Storage = require('./storage');
+const storage = new Storage(localStorage);
+
const $ = require('jquery');
require('jquery-circle-progress');
const Raven = window.Raven;
+if (storage.has('referrer')) {
+ window.referrer = storage.referrer;
+ storage.remove('referrer');
+} else {
+ window.referrer = 'external';
+}
+
$(document).ready(function() {
gcmCompliant().catch(err => {
$('#page-one').attr('hidden', true);
- $('#unsupported-browser').removeAttr('hidden');
+ sendEvent('sender', 'unsupported', {
+ cd6: err
+ }).then(() => {
+ location.replace('/unsupported');
+ });
});
$('#file-upload').change(onUpload);
+
+ $('.legal-links a, .social-links a, #dl-firefox').click(function(target) {
+ target.preventDefault();
+ const metric = findMetric(target.currentTarget.href);
+ // record exited event by recipient
+ sendEvent('sender', 'exited', {
+ cd3: metric
+ }).then(() => {
+ location.href = target.currentTarget.href;
+ });
+ });
+
+ $('#send-new-completed').click(function(target) {
+ target.preventDefault();
+ // record restarted event
+ sendEvent('sender', 'restarted', {
+ cd2: 'completed'
+ }).then(() => {
+ storage.referrer = 'completed-upload';
+ location.href = target.currentTarget.href;
+ });
+ });
+
+ $('#send-new-error').click(function(target) {
+ target.preventDefault();
+ // record restarted event
+ sendEvent('sender', 'restarted', {
+ cd2: 'errored'
+ }).then(() => {
+ storage.referrer = 'errored-upload';
+ location.href = target.currentTarget.href;
+ });
+ });
+
$('body').on('dragover', allowDrop).on('drop', onUpload);
// reset copy button
const $copyBtn = $('#copy-btn');
@@ -19,18 +76,23 @@ $(document).ready(function() {
$('#link').attr('disabled', false);
$copyBtn.attr('data-l10n-id', 'copyUrlFormButton');
- if (localStorage.length === 0) {
+ const files = storage.files;
+ if (files.length === 0) {
toggleHeader();
} else {
- for (let i = 0; i < localStorage.length; i++) {
- const id = localStorage.key(i);
- //check if file exists before adding to list
- checkExistence(id, true);
+ for (const index in files) {
+ const id = files[index].fileId;
+ //check if file still exists before adding to list
+ checkExistence(id, files[index], true);
}
}
// copy link to clipboard
$copyBtn.click(() => {
+ // record copied event from success screen
+ sendEvent('sender', 'copied', {
+ cd4: 'success-screen'
+ });
const aux = document.createElement('input');
aux.setAttribute('value', $('#link').attr('value'));
document.body.appendChild(aux);
@@ -40,7 +102,9 @@ $(document).ready(function() {
//disable button for 3s
$copyBtn.attr('disabled', true);
$('#link').attr('disabled', true);
- $copyBtn.html('');
+ $copyBtn.html(
+ '
'
+ );
window.setTimeout(() => {
$copyBtn.attr('disabled', false);
$('#link').attr('disabled', false);
@@ -69,14 +133,28 @@ $(document).ready(function() {
// on file upload by browse or drag & drop
function onUpload(event) {
event.preventDefault();
+
+ // don't allow upload if not on upload page
+ if ($('#page-one').attr('hidden')){
+ return;
+ }
+
+ storage.totalUploads += 1;
+
let file = '';
if (event.type === 'drop') {
- if (event.originalEvent.dataTransfer.files.length > 1 || event.originalEvent.dataTransfer.files[0].size === 0){
+ if (!event.originalEvent.dataTransfer.files[0]) {
$('.upload-window').removeClass('ondrag');
- document.l10n.formatValue('uploadPageMultipleFilesAlert')
- .then(str => {
- alert(str);
- });
+ return;
+ }
+ if (
+ event.originalEvent.dataTransfer.files.length > 1 ||
+ event.originalEvent.dataTransfer.files[0].size === 0
+ ) {
+ $('.upload-window').removeClass('ondrag');
+ document.l10n.formatValue('uploadPageMultipleFilesAlert').then(str => {
+ alert(str);
+ });
return;
}
file = event.originalEvent.dataTransfer.files[0];
@@ -84,21 +162,39 @@ $(document).ready(function() {
file = event.target.files[0];
}
+ if (file.size > MAXFILESIZE) {
+ return document.l10n
+ .formatValue('fileTooBig', { size: bytes(MAXFILESIZE) })
+ .then(alert);
+ }
+
$('#page-one').attr('hidden', true);
$('#upload-error').attr('hidden', true);
$('#upload-progress').removeAttr('hidden');
+ document.l10n.formatValue('importingFile').then(importingFile => {
+ $('.progress-text').text(importingFile);
+ });
//don't allow drag and drop when not on page-one
$('body').off('drop', onUpload);
- const expiration = 24 * 60 * 60 * 1000; //will eventually come from a field
const fileSender = new FileSender(file);
$('#cancel-upload').click(() => {
fileSender.cancel();
location.reload();
- document.l10n.formatValue('uploadCancelNotification')
- .then(str => {
- notify(str);
- });
+ document.l10n.formatValue('uploadCancelNotification').then(str => {
+ notify(str);
+ });
+ storage.referrer = 'cancelled-upload';
+
+ // record upload-stopped (cancelled) by sender
+ sendEvent('sender', 'upload-stopped', {
+ cm1: file.size,
+ cm5: storage.totalUploads,
+ cm6: unexpiredFiles,
+ cm7: storage.totalDownloads,
+ cd1: event.type === 'drop' ? 'drop' : 'click',
+ cd2: 'cancelled'
+ });
});
fileSender.on('progress', progress => {
@@ -106,101 +202,152 @@ $(document).ready(function() {
// update progress bar
$('#ul-progress').circleProgress('value', percent);
$('#ul-progress').circleProgress().on('circle-animation-end', function() {
- $('.percent-number').html(`${Math.floor(percent * 100)}`);
+ $('.percent-number').text(`${Math.floor(percent * 100)}`);
});
- if (progress[1] < 1000000) {
- $('.progress-text').text(
- `${file.name} (${(progress[0] / 1000).toFixed(1)}KB of ${(progress[1] / 1000).toFixed(1)}KB)`
- );
- } else if (progress[1] < 1000000000) {
- $('.progress-text').text(
- `${file.name} (${(progress[0] / 1000000).toFixed(1)}MB of ${(progress[1] / 1000000).toFixed(1)}MB)`
- );
- } else {
- $('.progress-text').text(
- `${file.name} (${(progress[0] / 1000000).toFixed(1)}MB of ${(progress[1] / 1000000000).toFixed(1)}GB)`
- );
- }
- });
-
- fileSender.on('loading', isStillLoading => {
- // The file is loading into Firefox at this stage
- if (isStillLoading) {
- console.log('Processing');
- } else {
- console.log('Finished processing');
- }
+ $('.progress-text').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) {
- console.log('Hashing');
+ document.l10n.formatValue('verifyingFile').then(verifyingFile => {
+ $('.progress-text').text(verifyingFile);
+ });
} else {
console.log('Finished hashing');
}
});
+ let uploadStart;
fileSender.on('encrypting', isStillEncrypting => {
// The file is being encrypted
if (isStillEncrypting) {
- console.log('Encrypting');
+ document.l10n.formatValue('encryptingFile').then(encryptingFile => {
+ $('.progress-text').text(encryptingFile);
+ });
} else {
console.log('Finished encrypting');
+ uploadStart = Date.now();
}
});
- let t = '';
- fileSender
- .upload()
- .then(info => {
- const fileData = {
- name: file.name,
- fileId: info.fileId,
- url: info.url,
- secretKey: info.secretKey,
- deleteToken: info.deleteToken,
- creationDate: new Date(),
- expiry: expiration
- };
- localStorage.setItem(info.fileId, JSON.stringify(fileData));
- $('#upload-filename').attr('data-l10n-id', 'uploadSuccessConfirmHeader');
- t = window.setTimeout(() => {
+
+ let t;
+ const startTime = Date.now();
+ const unexpiredFiles = storage.numFiles + 1;
+
+ // record upload-started event by sender
+ sendEvent('sender', 'upload-started', {
+ cm1: file.size,
+ cm5: storage.totalUploads,
+ cm6: unexpiredFiles,
+ cm7: storage.totalDownloads,
+ cd1: event.type === 'drop' ? 'drop' : 'click',
+ cd5: window.referrer
+ });
+
+ // For large files we need to give the ui a tick to breathe and update
+ // before we kick off the FileSender
+ setTimeout(() => {
+ fileSender
+ .upload()
+ .then(info => {
+ const endTime = Date.now();
+ const totalTime = endTime - startTime;
+ const uploadTime = endTime - uploadStart;
+ const uploadSpeed = file.size / (uploadTime / 1000);
+ const expiration = EXPIRE_SECONDS * 1000;
+
+ // record upload-stopped (completed) by sender
+ sendEvent('sender', 'upload-stopped', {
+ cm1: file.size,
+ cm2: totalTime,
+ cm3: uploadSpeed,
+ cm5: storage.totalUploads,
+ cm6: unexpiredFiles,
+ cm7: storage.totalDownloads,
+ cd1: event.type === 'drop' ? 'drop' : 'click',
+ cd2: 'completed'
+ });
+
+ const fileData = {
+ name: file.name,
+ size: file.size,
+ fileId: info.fileId,
+ url: info.url,
+ secretKey: info.secretKey,
+ deleteToken: info.deleteToken,
+ creationDate: new Date(),
+ expiry: expiration,
+ totalTime: totalTime,
+ typeOfUpload: event.type === 'drop' ? 'drop' : 'click',
+ uploadSpeed: uploadSpeed
+ };
+
+ storage.addFile(info.fileId, fileData);
+ $('#upload-filename').attr(
+ 'data-l10n-id',
+ 'uploadSuccessConfirmHeader'
+ );
+ t = window.setTimeout(() => {
+ $('#page-one').attr('hidden', true);
+ $('#upload-progress').attr('hidden', true);
+ $('#upload-error').attr('hidden', true);
+ $('#share-link').removeAttr('hidden');
+ }, 1000);
+
+ populateFileList(fileData);
+ document.l10n.formatValue('notifyUploadDone').then(str => {
+ notify(str);
+ });
+ })
+ .catch(err => {
+ // err is 0 when coming from a cancel upload event
+ if (err === 0) {
+ return;
+ }
+ // only show error page when the error is anything other than user cancelling the upload
+ Raven.captureException(err);
$('#page-one').attr('hidden', true);
$('#upload-progress').attr('hidden', true);
- $('#upload-error').attr('hidden', true);
- $('#share-link').removeAttr('hidden');
- }, 1000);
+ $('#upload-error').removeAttr('hidden');
+ window.clearTimeout(t);
- populateFileList(JSON.stringify(fileData));
- document.l10n.formatValue('notifyUploadDone')
- .then(str => {
- notify(str);
- });
- })
- .catch(err => {
- Raven.captureException(err);
- console.log(err);
- $('#page-one').attr('hidden', true);
- $('#upload-progress').attr('hidden', true);
- $('#upload-error').removeAttr('hidden');
- window.clearTimeout(t);
- });
+ // record upload-stopped (errored) by sender
+ sendEvent('sender', 'upload-stopped', {
+ cm1: file.size,
+ cm5: storage.totalUploads,
+ cm6: unexpiredFiles,
+ cm7: storage.totalDownloads,
+ cd1: event.type === 'drop' ? 'drop' : 'click',
+ cd2: 'errored',
+ cd6: err
+ });
+ });
+ }, 10);
}
function allowDrop(ev) {
ev.preventDefault();
}
- function checkExistence(id, populate) {
+ function checkExistence(id, file, populate) {
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = () => {
if (xhr.readyState === XMLHttpRequest.DONE) {
if (xhr.status === 200) {
if (populate) {
- populateFileList(localStorage.getItem(id));
+ populateFileList(file);
}
} else if (xhr.status === 404) {
- localStorage.removeItem(id);
+ storage.remove(id);
+ if (storage.numFiles === 0) {
+ toggleHeader();
+ }
}
}
};
@@ -208,21 +355,23 @@ $(document).ready(function() {
xhr.send();
}
- //update file table with current files in localStorage
+ //update file table with current files in storage
function populateFileList(file) {
- try {
- file = JSON.parse(file);
- } catch (e) {
- return;
- }
-
const row = document.createElement('tr');
const name = document.createElement('td');
const link = document.createElement('td');
- const $copyIcon = $('
', { src: '/resources/copy-16.svg', class: 'icon-copy', 'data-l10n-id': 'copyUrlHover'});
+ const $copyIcon = $('
', {
+ src: '/resources/copy-16.svg',
+ class: 'icon-copy',
+ 'data-l10n-id': 'copyUrlHover'
+ });
const expiry = document.createElement('td');
const del = document.createElement('td');
- const $delIcon = $('
', { src: '/resources/close-16.svg', class: 'icon-delete', 'data-l10n-id': 'deleteButtonHover' });
+ const $delIcon = $('
', {
+ src: '/resources/close-16.svg',
+ class: 'icon-delete',
+ 'data-l10n-id': 'deleteButtonHover'
+ });
const popupDiv = document.createElement('div');
const $popupText = $('