Merge pull request #537 from mozilla/choo
a few changes to make A/B testing easier
@ -1,12 +1,8 @@
|
|||||||
node_modules
|
node_modules
|
||||||
.git
|
.git
|
||||||
.DS_Store
|
.DS_Store
|
||||||
static
|
|
||||||
test
|
|
||||||
scripts
|
|
||||||
docs
|
|
||||||
firefox
|
firefox
|
||||||
|
assets
|
||||||
|
docs
|
||||||
public
|
public
|
||||||
views
|
test
|
||||||
webpack
|
|
||||||
frontend
|
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
public
|
dist
|
||||||
test/frontend/bundle.js
|
assets
|
||||||
firefox
|
firefox
|
||||||
|
6
.gitignore
vendored
@ -1,6 +1,2 @@
|
|||||||
.DS_Store
|
|
||||||
dist
|
|
||||||
node_modules
|
node_modules
|
||||||
static/*
|
dist
|
||||||
!static/info.txt
|
|
||||||
test/frontend/bundle.js
|
|
||||||
|
@ -12,4 +12,4 @@ RUN npm install --production && npm cache clean --force
|
|||||||
ENV PORT=1443
|
ENV PORT=1443
|
||||||
EXPOSE $PORT
|
EXPOSE $PORT
|
||||||
|
|
||||||
CMD ["npm", "start"]
|
CMD ["npm", "run", "prod"]
|
||||||
|
@ -30,11 +30,12 @@ $ redis-server /usr/local/etc/redis.conf
|
|||||||
|
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
|------------------|-------------|
|
|------------------|-------------|
|
||||||
| `npm run dev` | Builds and starts the web server locally for development.
|
|
||||||
| `npm run format` | Formats the frontend and server code using **prettier**.
|
| `npm run format` | Formats the frontend and server code using **prettier**.
|
||||||
| `npm run lint` | Lints the CSS and JavaScript code.
|
| `npm run lint` | Lints the CSS and JavaScript code.
|
||||||
| `npm start` | Starts the Express web server.
|
|
||||||
| `npm test` | Runs the suite of mocha tests.
|
| `npm test` | Runs the suite of mocha tests.
|
||||||
|
| `npm start` | Runs the server in development configuration.
|
||||||
|
| `npm run build` | Builds the production assets.
|
||||||
|
| `npm run prod` | Runs the server in production configuration.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
env:
|
env:
|
||||||
browser: true
|
browser: true
|
||||||
node: false
|
node: true
|
||||||
|
|
||||||
parserOptions:
|
parserOptions:
|
||||||
sourceType: module
|
sourceType: module
|
24
app/dragManager.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
export default function(state, emitter) {
|
||||||
|
emitter.on('DOMContentLoaded', () => {
|
||||||
|
document.body.addEventListener('dragover', event => {
|
||||||
|
if (state.route === '/') {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
document.body.addEventListener('drop', event => {
|
||||||
|
if (state.route === '/' && !state.transfer) {
|
||||||
|
event.preventDefault();
|
||||||
|
document.querySelector('.upload-window').classList.remove('ondrag');
|
||||||
|
const target = event.dataTransfer;
|
||||||
|
if (target.files.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (target.files.length > 1 || target.files[0].size === 0) {
|
||||||
|
return alert(state.translate('uploadPageMultipleFilesAlert'));
|
||||||
|
}
|
||||||
|
const file = target.files[0];
|
||||||
|
emitter.emit('upload', { file, type: 'drop' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
202
app/fileManager.js
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
/* global EXPIRE_SECONDS */
|
||||||
|
import FileSender from './fileSender';
|
||||||
|
import FileReceiver from './fileReceiver';
|
||||||
|
import { copyToClipboard, delay, fadeOut } from './utils';
|
||||||
|
import * as metrics from './metrics';
|
||||||
|
|
||||||
|
function saveFile(file) {
|
||||||
|
const dataView = new DataView(file.plaintext);
|
||||||
|
const blob = new Blob([dataView], { type: file.type });
|
||||||
|
const downloadUrl = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
if (window.navigator.msSaveBlob) {
|
||||||
|
return window.navigator.msSaveBlob(blob, file.name);
|
||||||
|
}
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = downloadUrl;
|
||||||
|
a.download = file.name;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(downloadUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openLinksInNewTab(links, should = true) {
|
||||||
|
links = links || Array.from(document.querySelectorAll('a:not([target])'));
|
||||||
|
if (should) {
|
||||||
|
links.forEach(l => {
|
||||||
|
l.setAttribute('target', '_blank');
|
||||||
|
l.setAttribute('rel', 'noopener noreferrer');
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
links.forEach(l => {
|
||||||
|
l.removeAttribute('target');
|
||||||
|
l.removeAttribute('rel');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return links;
|
||||||
|
}
|
||||||
|
|
||||||
|
function exists(id) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.onreadystatechange = () => {
|
||||||
|
if (xhr.readyState === XMLHttpRequest.HEADERS_RECEIVED) {
|
||||||
|
resolve(xhr.status === 200);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhr.onerror = () => resolve(false);
|
||||||
|
xhr.ontimeout = () => resolve(false);
|
||||||
|
xhr.open('get', '/api/exists/' + id);
|
||||||
|
xhr.timeout = 2000;
|
||||||
|
xhr.send();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function(state, emitter) {
|
||||||
|
let lastRender = 0;
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
emitter.emit('render');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkFiles() {
|
||||||
|
const files = state.storage.files;
|
||||||
|
let rerender = false;
|
||||||
|
for (const file of files) {
|
||||||
|
const ok = await exists(file.id);
|
||||||
|
if (!ok) {
|
||||||
|
state.storage.remove(file.id);
|
||||||
|
rerender = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (rerender) {
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
emitter.on('DOMContentLoaded', checkFiles);
|
||||||
|
|
||||||
|
emitter.on('navigate', checkFiles);
|
||||||
|
|
||||||
|
emitter.on('render', () => {
|
||||||
|
lastRender = Date.now();
|
||||||
|
});
|
||||||
|
|
||||||
|
emitter.on('delete', async ({ file, location }) => {
|
||||||
|
try {
|
||||||
|
metrics.deletedUpload({
|
||||||
|
size: file.size,
|
||||||
|
time: file.time,
|
||||||
|
speed: file.speed,
|
||||||
|
type: file.type,
|
||||||
|
ttl: file.expiresAt - Date.now(),
|
||||||
|
location
|
||||||
|
});
|
||||||
|
state.storage.remove(file.id);
|
||||||
|
await FileSender.delete(file.id, file.deleteToken);
|
||||||
|
} catch (e) {
|
||||||
|
state.raven.captureException(e);
|
||||||
|
}
|
||||||
|
state.fileInfo = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
emitter.on('cancel', () => {
|
||||||
|
state.transfer.cancel();
|
||||||
|
});
|
||||||
|
|
||||||
|
emitter.on('upload', async ({ file, type }) => {
|
||||||
|
const size = file.size;
|
||||||
|
const sender = new FileSender(file);
|
||||||
|
sender.on('progress', render);
|
||||||
|
sender.on('encrypting', render);
|
||||||
|
state.transfer = sender;
|
||||||
|
render();
|
||||||
|
const links = openLinksInNewTab();
|
||||||
|
await delay(200);
|
||||||
|
try {
|
||||||
|
const start = Date.now();
|
||||||
|
metrics.startedUpload({ size, type });
|
||||||
|
const info = await sender.upload();
|
||||||
|
const time = Date.now() - start;
|
||||||
|
const speed = size / (time / 1000);
|
||||||
|
metrics.completedUpload({ size, time, speed, type });
|
||||||
|
await delay(1000);
|
||||||
|
await fadeOut('upload-progress');
|
||||||
|
info.name = file.name;
|
||||||
|
info.size = size;
|
||||||
|
info.type = type;
|
||||||
|
info.time = time;
|
||||||
|
info.speed = speed;
|
||||||
|
info.createdAt = Date.now();
|
||||||
|
info.url = `${info.url}#${info.secretKey}`;
|
||||||
|
info.expiresAt = Date.now() + EXPIRE_SECONDS * 1000;
|
||||||
|
state.fileInfo = info;
|
||||||
|
state.storage.addFile(state.fileInfo);
|
||||||
|
openLinksInNewTab(links, false);
|
||||||
|
state.transfer = null;
|
||||||
|
state.storage.totalUploads += 1;
|
||||||
|
emitter.emit('pushState', `/share/${info.id}`);
|
||||||
|
} catch (err) {
|
||||||
|
state.transfer = null;
|
||||||
|
if (err.message === '0') {
|
||||||
|
//cancelled. do nothing
|
||||||
|
metrics.cancelledUpload({ size, type });
|
||||||
|
return render();
|
||||||
|
}
|
||||||
|
state.raven.captureException(err);
|
||||||
|
metrics.stoppedUpload({ size, type, err });
|
||||||
|
emitter.emit('replaceState', '/error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
emitter.on('download', async file => {
|
||||||
|
const size = file.size;
|
||||||
|
const url = `/api/download/${file.id}`;
|
||||||
|
const receiver = new FileReceiver(url, file.key);
|
||||||
|
receiver.on('progress', render);
|
||||||
|
receiver.on('decrypting', render);
|
||||||
|
state.transfer = receiver;
|
||||||
|
const links = openLinksInNewTab();
|
||||||
|
render();
|
||||||
|
try {
|
||||||
|
const start = Date.now();
|
||||||
|
metrics.startedDownload({ size: file.size, ttl: file.ttl });
|
||||||
|
const f = await receiver.download();
|
||||||
|
const time = Date.now() - start;
|
||||||
|
const speed = size / (time / 1000);
|
||||||
|
await delay(1000);
|
||||||
|
await fadeOut('download-progress');
|
||||||
|
saveFile(f);
|
||||||
|
state.storage.totalDownloads += 1;
|
||||||
|
metrics.completedDownload({ size, time, speed });
|
||||||
|
emitter.emit('pushState', '/completed');
|
||||||
|
} catch (err) {
|
||||||
|
// TODO cancelled download
|
||||||
|
const location = err.message === 'notfound' ? '/404' : '/error';
|
||||||
|
if (location === '/error') {
|
||||||
|
state.raven.captureException(err);
|
||||||
|
metrics.stoppedDownload({ size, err });
|
||||||
|
}
|
||||||
|
emitter.emit('replaceState', location);
|
||||||
|
} finally {
|
||||||
|
state.transfer = null;
|
||||||
|
openLinksInNewTab(links, false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
emitter.on('copy', ({ url, location }) => {
|
||||||
|
copyToClipboard(url);
|
||||||
|
metrics.copiedLink({ location });
|
||||||
|
});
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
// poll for rerendering the file list countdown timers
|
||||||
|
if (
|
||||||
|
state.route === '/' &&
|
||||||
|
state.storage.files.length > 0 &&
|
||||||
|
Date.now() - lastRender > 30000
|
||||||
|
) {
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
}, 60000);
|
||||||
|
}
|
@ -1,9 +1,9 @@
|
|||||||
import EventEmitter from 'events';
|
import Nanobus from 'nanobus';
|
||||||
import { hexToArray } from './utils';
|
import { hexToArray, bytes } from './utils';
|
||||||
|
|
||||||
export default class FileReceiver extends EventEmitter {
|
export default class FileReceiver extends Nanobus {
|
||||||
constructor(url, k) {
|
constructor(url, k) {
|
||||||
super();
|
super('FileReceiver');
|
||||||
this.key = window.crypto.subtle.importKey(
|
this.key = window.crypto.subtle.importKey(
|
||||||
'jwk',
|
'jwk',
|
||||||
{
|
{
|
||||||
@ -19,6 +19,23 @@ export default class FileReceiver extends EventEmitter {
|
|||||||
['decrypt']
|
['decrypt']
|
||||||
);
|
);
|
||||||
this.url = url;
|
this.url = url;
|
||||||
|
this.msg = 'fileSizeProgress';
|
||||||
|
this.progress = [0, 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
get progressRatio() {
|
||||||
|
return this.progress[0] / this.progress[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
get sizes() {
|
||||||
|
return {
|
||||||
|
partialSize: bytes(this.progress[0]),
|
||||||
|
totalSize: bytes(this.progress[1])
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel() {
|
||||||
|
// TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadFile() {
|
downloadFile() {
|
||||||
@ -27,7 +44,8 @@ export default class FileReceiver extends EventEmitter {
|
|||||||
|
|
||||||
xhr.onprogress = event => {
|
xhr.onprogress = event => {
|
||||||
if (event.lengthComputable && event.target.status !== 404) {
|
if (event.lengthComputable && event.target.status !== 404) {
|
||||||
this.emit('progress', [event.loaded, event.total]);
|
this.progress = [event.loaded, event.total];
|
||||||
|
this.emit('progress', this.progress);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -61,6 +79,7 @@ export default class FileReceiver extends EventEmitter {
|
|||||||
async download() {
|
async download() {
|
||||||
const key = await this.key;
|
const key = await this.key;
|
||||||
const file = await this.downloadFile();
|
const file = await this.downloadFile();
|
||||||
|
this.msg = 'decryptingFile';
|
||||||
this.emit('decrypting');
|
this.emit('decrypting');
|
||||||
const plaintext = await window.crypto.subtle.decrypt(
|
const plaintext = await window.crypto.subtle.decrypt(
|
||||||
{
|
{
|
||||||
@ -71,6 +90,7 @@ export default class FileReceiver extends EventEmitter {
|
|||||||
key,
|
key,
|
||||||
file.data
|
file.data
|
||||||
);
|
);
|
||||||
|
this.msg = 'downloadFinish';
|
||||||
return {
|
return {
|
||||||
plaintext,
|
plaintext,
|
||||||
name: decodeURIComponent(file.name),
|
name: decodeURIComponent(file.name),
|
@ -1,10 +1,13 @@
|
|||||||
import EventEmitter from 'events';
|
import Nanobus from 'nanobus';
|
||||||
import { arrayToHex } from './utils';
|
import { arrayToHex, bytes } from './utils';
|
||||||
|
|
||||||
export default class FileSender extends EventEmitter {
|
export default class FileSender extends Nanobus {
|
||||||
constructor(file) {
|
constructor(file) {
|
||||||
super();
|
super('FileSender');
|
||||||
this.file = file;
|
this.file = file;
|
||||||
|
this.msg = 'importingFile';
|
||||||
|
this.progress = [0, 1];
|
||||||
|
this.cancelled = false;
|
||||||
this.iv = window.crypto.getRandomValues(new Uint8Array(12));
|
this.iv = window.crypto.getRandomValues(new Uint8Array(12));
|
||||||
this.uploadXHR = new XMLHttpRequest();
|
this.uploadXHR = new XMLHttpRequest();
|
||||||
this.key = window.crypto.subtle.generateKey(
|
this.key = window.crypto.subtle.generateKey(
|
||||||
@ -17,13 +20,13 @@ export default class FileSender extends EventEmitter {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static delete(fileId, token) {
|
static delete(id, token) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (!fileId || !token) {
|
if (!id || !token) {
|
||||||
return reject();
|
return reject();
|
||||||
}
|
}
|
||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
xhr.open('post', '/delete/' + fileId, true);
|
xhr.open('POST', `/api/delete/${id}`);
|
||||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||||
|
|
||||||
xhr.onreadystatechange = () => {
|
xhr.onreadystatechange = () => {
|
||||||
@ -36,9 +39,23 @@ export default class FileSender extends EventEmitter {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get progressRatio() {
|
||||||
|
return this.progress[0] / this.progress[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
get sizes() {
|
||||||
|
return {
|
||||||
|
partialSize: bytes(this.progress[0]),
|
||||||
|
totalSize: bytes(this.progress[1])
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
cancel() {
|
cancel() {
|
||||||
|
this.cancelled = true;
|
||||||
|
if (this.msg === 'fileSizeProgress') {
|
||||||
this.uploadXHR.abort();
|
this.uploadXHR.abort();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
readFile() {
|
readFile() {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@ -57,7 +74,7 @@ export default class FileSender extends EventEmitter {
|
|||||||
uploadFile(encrypted, keydata) {
|
uploadFile(encrypted, keydata) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const file = this.file;
|
const file = this.file;
|
||||||
const fileId = arrayToHex(this.iv);
|
const id = arrayToHex(this.iv);
|
||||||
const dataView = new DataView(encrypted);
|
const dataView = new DataView(encrypted);
|
||||||
const blob = new Blob([dataView], { type: file.type });
|
const blob = new Blob([dataView], { type: file.type });
|
||||||
const fd = new FormData();
|
const fd = new FormData();
|
||||||
@ -67,41 +84,49 @@ export default class FileSender extends EventEmitter {
|
|||||||
|
|
||||||
xhr.upload.addEventListener('progress', e => {
|
xhr.upload.addEventListener('progress', e => {
|
||||||
if (e.lengthComputable) {
|
if (e.lengthComputable) {
|
||||||
this.emit('progress', [e.loaded, e.total]);
|
this.progress = [e.loaded, e.total];
|
||||||
|
this.emit('progress', this.progress);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
xhr.onreadystatechange = () => {
|
xhr.onreadystatechange = () => {
|
||||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||||
if (xhr.status === 200) {
|
if (xhr.status === 200) {
|
||||||
|
this.progress = [1, 1];
|
||||||
|
this.msg = 'notifyUploadDone';
|
||||||
const responseObj = JSON.parse(xhr.responseText);
|
const responseObj = JSON.parse(xhr.responseText);
|
||||||
return resolve({
|
return resolve({
|
||||||
url: responseObj.url,
|
url: responseObj.url,
|
||||||
fileId: responseObj.id,
|
id: responseObj.id,
|
||||||
secretKey: keydata.k,
|
secretKey: keydata.k,
|
||||||
deleteToken: responseObj.delete
|
deleteToken: responseObj.delete
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
reject(xhr.status);
|
this.msg = 'errorPageHeader';
|
||||||
|
reject(new Error(xhr.status));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
xhr.open('post', '/upload', true);
|
xhr.open('post', '/api/upload', true);
|
||||||
xhr.setRequestHeader(
|
xhr.setRequestHeader(
|
||||||
'X-File-Metadata',
|
'X-File-Metadata',
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
id: fileId,
|
id: id,
|
||||||
filename: encodeURIComponent(file.name)
|
filename: encodeURIComponent(file.name)
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
xhr.send(fd);
|
xhr.send(fd);
|
||||||
|
this.msg = 'fileSizeProgress';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async upload() {
|
async upload() {
|
||||||
this.emit('loading');
|
|
||||||
const key = await this.key;
|
const key = await this.key;
|
||||||
const plaintext = await this.readFile();
|
const plaintext = await this.readFile();
|
||||||
|
if (this.cancelled) {
|
||||||
|
throw new Error(0);
|
||||||
|
}
|
||||||
|
this.msg = 'encryptingFile';
|
||||||
this.emit('encrypting');
|
this.emit('encrypting');
|
||||||
const encrypted = await window.crypto.subtle.encrypt(
|
const encrypted = await window.crypto.subtle.encrypt(
|
||||||
{
|
{
|
||||||
@ -112,6 +137,9 @@ export default class FileSender extends EventEmitter {
|
|||||||
key,
|
key,
|
||||||
plaintext
|
plaintext
|
||||||
);
|
);
|
||||||
|
if (this.cancelled) {
|
||||||
|
throw new Error(0);
|
||||||
|
}
|
||||||
const keydata = await window.crypto.subtle.exportKey('jwk', key);
|
const keydata = await window.crypto.subtle.exportKey('jwk', key);
|
||||||
return this.uploadFile(encrypted, keydata);
|
return this.uploadFile(encrypted, keydata);
|
||||||
}
|
}
|
38
app/main.js
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import app from './routes';
|
||||||
|
import log from 'choo-log';
|
||||||
|
import locale from '../common/locales';
|
||||||
|
import fileManager from './fileManager';
|
||||||
|
import dragManager from './dragManager';
|
||||||
|
import { canHasSend } from './utils';
|
||||||
|
import assets from '../common/assets';
|
||||||
|
import storage from './storage';
|
||||||
|
import metrics from './metrics';
|
||||||
|
import Raven from 'raven-js';
|
||||||
|
|
||||||
|
if (navigator.doNotTrack !== '1' && window.RAVEN_CONFIG) {
|
||||||
|
Raven.config(window.SENTRY_ID, window.RAVEN_CONFIG).install();
|
||||||
|
}
|
||||||
|
|
||||||
|
app.use(log());
|
||||||
|
|
||||||
|
app.use((state, emitter) => {
|
||||||
|
// init state
|
||||||
|
state.transfer = null;
|
||||||
|
state.fileInfo = null;
|
||||||
|
state.translate = locale.getTranslator();
|
||||||
|
state.storage = storage;
|
||||||
|
state.raven = Raven;
|
||||||
|
emitter.on('DOMContentLoaded', async () => {
|
||||||
|
const ok = await canHasSend(assets.get('cryptofill.js'));
|
||||||
|
if (!ok) {
|
||||||
|
const reason = /firefox/i.test(navigator.userAgent) ? 'outdated' : 'gcm';
|
||||||
|
emitter.emit('replaceState', `/unsupported/${reason}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use(metrics);
|
||||||
|
app.use(fileManager);
|
||||||
|
app.use(dragManager);
|
||||||
|
|
||||||
|
app.mount('#page-one');
|
@ -1,12 +1,11 @@
|
|||||||
import testPilotGA from 'testpilot-ga/src/TestPilotGA';
|
import testPilotGA from 'testpilot-ga/src/TestPilotGA';
|
||||||
import Storage from './storage';
|
import storage from './storage';
|
||||||
const storage = new Storage();
|
|
||||||
|
|
||||||
let hasLocalStorage = false;
|
let hasLocalStorage = false;
|
||||||
try {
|
try {
|
||||||
hasLocalStorage = !!localStorage;
|
hasLocalStorage = typeof localStorage !== 'undefined';
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// don't care
|
// when disabled, any mention of localStorage throws an error
|
||||||
}
|
}
|
||||||
|
|
||||||
const analytics = new testPilotGA({
|
const analytics = new testPilotGA({
|
||||||
@ -15,14 +14,19 @@ const analytics = new testPilotGA({
|
|||||||
tid: window.GOOGLE_ANALYTICS_ID
|
tid: window.GOOGLE_ANALYTICS_ID
|
||||||
});
|
});
|
||||||
|
|
||||||
const category = location.pathname.includes('/download')
|
let appState = null;
|
||||||
? 'recipient'
|
|
||||||
: 'sender';
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
export default function initialize(state, emitter) {
|
||||||
|
appState = state;
|
||||||
|
emitter.on('DOMContentLoaded', () => {
|
||||||
addExitHandlers();
|
addExitHandlers();
|
||||||
addRestartHandlers();
|
//TODO restart handlers... somewhere
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function category() {
|
||||||
|
return appState.route === '/' ? 'sender' : 'recipient';
|
||||||
|
}
|
||||||
|
|
||||||
function sendEvent() {
|
function sendEvent() {
|
||||||
return (
|
return (
|
||||||
@ -62,11 +66,11 @@ function urlToMetric(url) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setReferrer(state) {
|
function setReferrer(state) {
|
||||||
if (category === 'sender') {
|
if (category() === 'sender') {
|
||||||
if (state) {
|
if (state) {
|
||||||
storage.referrer = `${state}-upload`;
|
storage.referrer = `${state}-upload`;
|
||||||
}
|
}
|
||||||
} else if (category === 'recipient') {
|
} else if (category() === 'recipient') {
|
||||||
if (state) {
|
if (state) {
|
||||||
storage.referrer = `${state}-download`;
|
storage.referrer = `${state}-download`;
|
||||||
}
|
}
|
||||||
@ -87,10 +91,10 @@ function takeReferrer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function startedUpload(params) {
|
function startedUpload(params) {
|
||||||
return sendEvent(category, 'upload-started', {
|
return sendEvent('sender', 'upload-started', {
|
||||||
cm1: params.size,
|
cm1: params.size,
|
||||||
cm5: storage.totalUploads,
|
cm5: storage.totalUploads,
|
||||||
cm6: storage.numFiles + 1,
|
cm6: storage.files.length + 1,
|
||||||
cm7: storage.totalDownloads,
|
cm7: storage.totalDownloads,
|
||||||
cd1: params.type,
|
cd1: params.type,
|
||||||
cd5: takeReferrer()
|
cd5: takeReferrer()
|
||||||
@ -99,10 +103,10 @@ function startedUpload(params) {
|
|||||||
|
|
||||||
function cancelledUpload(params) {
|
function cancelledUpload(params) {
|
||||||
setReferrer('cancelled');
|
setReferrer('cancelled');
|
||||||
return sendEvent(category, 'upload-stopped', {
|
return sendEvent('sender', 'upload-stopped', {
|
||||||
cm1: params.size,
|
cm1: params.size,
|
||||||
cm5: storage.totalUploads,
|
cm5: storage.totalUploads,
|
||||||
cm6: storage.numFiles,
|
cm6: storage.files.length,
|
||||||
cm7: storage.totalDownloads,
|
cm7: storage.totalDownloads,
|
||||||
cd1: params.type,
|
cd1: params.type,
|
||||||
cd2: 'cancelled'
|
cd2: 'cancelled'
|
||||||
@ -110,12 +114,12 @@ function cancelledUpload(params) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function completedUpload(params) {
|
function completedUpload(params) {
|
||||||
return sendEvent(category, 'upload-stopped', {
|
return sendEvent('sender', 'upload-stopped', {
|
||||||
cm1: params.size,
|
cm1: params.size,
|
||||||
cm2: params.time,
|
cm2: params.time,
|
||||||
cm3: params.speed,
|
cm3: params.speed,
|
||||||
cm5: storage.totalUploads,
|
cm5: storage.totalUploads,
|
||||||
cm6: storage.numFiles,
|
cm6: storage.files.length,
|
||||||
cm7: storage.totalDownloads,
|
cm7: storage.totalDownloads,
|
||||||
cd1: params.type,
|
cd1: params.type,
|
||||||
cd2: 'completed'
|
cd2: 'completed'
|
||||||
@ -123,20 +127,20 @@ function completedUpload(params) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function startedDownload(params) {
|
function startedDownload(params) {
|
||||||
return sendEvent(category, 'download-started', {
|
return sendEvent('recipient', 'download-started', {
|
||||||
cm1: params.size,
|
cm1: params.size,
|
||||||
cm4: params.ttl,
|
cm4: params.ttl,
|
||||||
cm5: storage.totalUploads,
|
cm5: storage.totalUploads,
|
||||||
cm6: storage.numFiles,
|
cm6: storage.files.length,
|
||||||
cm7: storage.totalDownloads
|
cm7: storage.totalDownloads
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function stoppedDownload(params) {
|
function stoppedDownload(params) {
|
||||||
return sendEvent(category, 'download-stopped', {
|
return sendEvent('recipient', 'download-stopped', {
|
||||||
cm1: params.size,
|
cm1: params.size,
|
||||||
cm5: storage.totalUploads,
|
cm5: storage.totalUploads,
|
||||||
cm6: storage.numFiles,
|
cm6: storage.files.length,
|
||||||
cm7: storage.totalDownloads,
|
cm7: storage.totalDownloads,
|
||||||
cd2: 'errored',
|
cd2: 'errored',
|
||||||
cd6: params.err
|
cd6: params.err
|
||||||
@ -145,20 +149,20 @@ function stoppedDownload(params) {
|
|||||||
|
|
||||||
function cancelledDownload(params) {
|
function cancelledDownload(params) {
|
||||||
setReferrer('cancelled');
|
setReferrer('cancelled');
|
||||||
return sendEvent(category, 'download-stopped', {
|
return sendEvent('recipient', 'download-stopped', {
|
||||||
cm1: params.size,
|
cm1: params.size,
|
||||||
cm5: storage.totalUploads,
|
cm5: storage.totalUploads,
|
||||||
cm6: storage.numFiles,
|
cm6: storage.files.length,
|
||||||
cm7: storage.totalDownloads,
|
cm7: storage.totalDownloads,
|
||||||
cd2: 'cancelled'
|
cd2: 'cancelled'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function stoppedUpload(params) {
|
function stoppedUpload(params) {
|
||||||
return sendEvent(category, 'upload-stopped', {
|
return sendEvent('sender', 'upload-stopped', {
|
||||||
cm1: params.size,
|
cm1: params.size,
|
||||||
cm5: storage.totalUploads,
|
cm5: storage.totalUploads,
|
||||||
cm6: storage.numFiles,
|
cm6: storage.files.length,
|
||||||
cm7: storage.totalDownloads,
|
cm7: storage.totalDownloads,
|
||||||
cd1: params.type,
|
cd1: params.type,
|
||||||
cd2: 'errored',
|
cd2: 'errored',
|
||||||
@ -167,25 +171,25 @@ function stoppedUpload(params) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function completedDownload(params) {
|
function completedDownload(params) {
|
||||||
return sendEvent(category, 'download-stopped', {
|
return sendEvent('recipient', 'download-stopped', {
|
||||||
cm1: params.size,
|
cm1: params.size,
|
||||||
cm2: params.time,
|
cm2: params.time,
|
||||||
cm3: params.speed,
|
cm3: params.speed,
|
||||||
cm5: storage.totalUploads,
|
cm5: storage.totalUploads,
|
||||||
cm6: storage.numFiles,
|
cm6: storage.files.length,
|
||||||
cm7: storage.totalDownloads,
|
cm7: storage.totalDownloads,
|
||||||
cd2: 'completed'
|
cd2: 'completed'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function deletedUpload(params) {
|
function deletedUpload(params) {
|
||||||
return sendEvent(category, 'upload-deleted', {
|
return sendEvent(category(), 'upload-deleted', {
|
||||||
cm1: params.size,
|
cm1: params.size,
|
||||||
cm2: params.time,
|
cm2: params.time,
|
||||||
cm3: params.speed,
|
cm3: params.speed,
|
||||||
cm4: params.ttl,
|
cm4: params.ttl,
|
||||||
cm5: storage.totalUploads,
|
cm5: storage.totalUploads,
|
||||||
cm6: storage.numFiles,
|
cm6: storage.files.length,
|
||||||
cm7: storage.totalDownloads,
|
cm7: storage.totalDownloads,
|
||||||
cd1: params.type,
|
cd1: params.type,
|
||||||
cd4: params.location
|
cd4: params.location
|
||||||
@ -193,19 +197,19 @@ function deletedUpload(params) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function unsupported(params) {
|
function unsupported(params) {
|
||||||
return sendEvent(category, 'unsupported', {
|
return sendEvent(category(), 'unsupported', {
|
||||||
cd6: params.err
|
cd6: params.err
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function copiedLink(params) {
|
function copiedLink(params) {
|
||||||
return sendEvent(category, 'copied', {
|
return sendEvent('sender', 'copied', {
|
||||||
cd4: params.location
|
cd4: params.location
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function exitEvent(target) {
|
function exitEvent(target) {
|
||||||
return sendEvent(category, 'exited', {
|
return sendEvent(category(), 'exited', {
|
||||||
cd3: urlToMetric(target.currentTarget.href)
|
cd3: urlToMetric(target.currentTarget.href)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -219,21 +223,13 @@ function addExitHandlers() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function restartEvent(state) {
|
function restart(state) {
|
||||||
setReferrer(state);
|
setReferrer(state);
|
||||||
return sendEvent(category, 'restarted', {
|
return sendEvent(category(), 'restarted', {
|
||||||
cd2: state
|
cd2: state
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function addRestartHandlers() {
|
|
||||||
const elements = Array.from(document.querySelectorAll('.send-new'));
|
|
||||||
elements.forEach(el => {
|
|
||||||
const state = el.getAttribute('data-state');
|
|
||||||
el.addEventListener('click', restartEvent.bind(null, state));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
copiedLink,
|
copiedLink,
|
||||||
startedUpload,
|
startedUpload,
|
||||||
@ -245,5 +241,6 @@ export {
|
|||||||
cancelledDownload,
|
cancelledDownload,
|
||||||
stoppedDownload,
|
stoppedDownload,
|
||||||
completedDownload,
|
completedDownload,
|
||||||
|
restart,
|
||||||
unsupported
|
unsupported
|
||||||
};
|
};
|
9
app/routes/download.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
const preview = require('../templates/preview');
|
||||||
|
const download = require('../templates/download');
|
||||||
|
|
||||||
|
module.exports = function(state, emit) {
|
||||||
|
if (state.transfer) {
|
||||||
|
return download(state, emit);
|
||||||
|
}
|
||||||
|
return preview(state, emit);
|
||||||
|
};
|
9
app/routes/home.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
const welcome = require('../templates/welcome');
|
||||||
|
const upload = require('../templates/upload');
|
||||||
|
|
||||||
|
module.exports = function(state, emit) {
|
||||||
|
if (state.transfer) {
|
||||||
|
return upload(state, emit);
|
||||||
|
}
|
||||||
|
return welcome(state, emit);
|
||||||
|
};
|
17
app/routes/index.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
const choo = require('choo');
|
||||||
|
const download = require('./download');
|
||||||
|
|
||||||
|
const app = choo();
|
||||||
|
|
||||||
|
app.route('/', require('./home'));
|
||||||
|
app.route('/share/:id', require('../templates/share'));
|
||||||
|
app.route('/download/:id', download);
|
||||||
|
app.route('/download/:id/:key', download);
|
||||||
|
app.route('/completed', require('../templates/completed'));
|
||||||
|
app.route('/unsupported/:reason', require('../templates/unsupported'));
|
||||||
|
app.route('/legal', require('../templates/legal'));
|
||||||
|
app.route('/error', require('../templates/error'));
|
||||||
|
app.route('/blank', require('../templates/blank'));
|
||||||
|
app.route('*', require('../templates/notFound'));
|
||||||
|
|
||||||
|
module.exports = app;
|
@ -26,13 +26,30 @@ class Mem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class Storage {
|
class Storage {
|
||||||
constructor() {
|
constructor() {
|
||||||
try {
|
try {
|
||||||
this.engine = localStorage || new Mem();
|
this.engine = localStorage || new Mem();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.engine = new Mem();
|
this.engine = new Mem();
|
||||||
}
|
}
|
||||||
|
this._files = this.loadFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
loadFiles() {
|
||||||
|
const fs = [];
|
||||||
|
for (let i = 0; i < this.engine.length; i++) {
|
||||||
|
const k = this.engine.key(i);
|
||||||
|
if (isFile(k)) {
|
||||||
|
try {
|
||||||
|
fs.push(JSON.parse(this.engine.getItem(k)));
|
||||||
|
} catch (err) {
|
||||||
|
// obviously you're not a golfer
|
||||||
|
this.engine.removeItem(k);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fs.sort((a, b) => a.createdAt - b.createdAt);
|
||||||
}
|
}
|
||||||
|
|
||||||
get totalDownloads() {
|
get totalDownloads() {
|
||||||
@ -55,34 +72,7 @@ export default class Storage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get files() {
|
get files() {
|
||||||
const fs = [];
|
return this._files;
|
||||||
for (let i = 0; i < this.engine.length; i++) {
|
|
||||||
const k = this.engine.key(i);
|
|
||||||
if (isFile(k)) {
|
|
||||||
try {
|
|
||||||
fs.push(JSON.parse(this.engine.getItem(k)));
|
|
||||||
} catch (err) {
|
|
||||||
// obviously you're not a golfer
|
|
||||||
this.engine.removeItem(k);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return fs.sort((file1, file2) => {
|
|
||||||
const creationDate1 = new Date(file1.creationDate);
|
|
||||||
const creationDate2 = new Date(file2.creationDate);
|
|
||||||
return creationDate1 - creationDate2;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
getFileById(id) {
|
||||||
@ -94,10 +84,16 @@ export default class Storage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
remove(property) {
|
remove(property) {
|
||||||
|
if (isFile(property)) {
|
||||||
|
this._files.splice(this._files.findIndex(f => f.id === property), 1);
|
||||||
|
}
|
||||||
this.engine.removeItem(property);
|
this.engine.removeItem(property);
|
||||||
}
|
}
|
||||||
|
|
||||||
addFile(id, file) {
|
addFile(file) {
|
||||||
this.engine.setItem(id, JSON.stringify(file));
|
this._files.push(file);
|
||||||
|
this.engine.setItem(file.id, JSON.stringify(file));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default new Storage();
|
9
app/templates/blank.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
|
||||||
|
module.exports = function(state) {
|
||||||
|
const div = html`<div id="page-one"></div>`;
|
||||||
|
if (state.layout) {
|
||||||
|
return state.layout(state, div);
|
||||||
|
}
|
||||||
|
return div;
|
||||||
|
};
|
31
app/templates/completed.js
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
const progress = require('./progress');
|
||||||
|
const { fadeOut } = require('../utils');
|
||||||
|
|
||||||
|
module.exports = function(state, emit) {
|
||||||
|
const div = html`
|
||||||
|
<div id="download" class="fadeIn">
|
||||||
|
<div id="download-progress">
|
||||||
|
<div id="dl-title" class="title">${state.translate(
|
||||||
|
'downloadFinish'
|
||||||
|
)}</div>
|
||||||
|
<div class="description"></div>
|
||||||
|
${progress(1)}
|
||||||
|
<div class="upload">
|
||||||
|
<div class="progress-text"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a class="send-new" data-state="completed" href="/" onclick=${sendNew}>${state.translate(
|
||||||
|
'sendYourFilesLink'
|
||||||
|
)}</a>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
async function sendNew(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
await fadeOut('download');
|
||||||
|
emit('pushState', '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
return div;
|
||||||
|
};
|
28
app/templates/download.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
const progress = require('./progress');
|
||||||
|
const { bytes } = require('../utils');
|
||||||
|
|
||||||
|
module.exports = function(state) {
|
||||||
|
const transfer = state.transfer;
|
||||||
|
const div = html`
|
||||||
|
<div id="download-progress" class="fadeIn">
|
||||||
|
<div id="dl-title" class="title">${state.translate(
|
||||||
|
'downloadingPageProgress',
|
||||||
|
{
|
||||||
|
filename: state.fileInfo.name,
|
||||||
|
size: bytes(state.fileInfo.size)
|
||||||
|
}
|
||||||
|
)}</div>
|
||||||
|
<div class="description">${state.translate('downloadingPageMessage')}</div>
|
||||||
|
${progress(transfer.progressRatio)}
|
||||||
|
<div class="upload">
|
||||||
|
<div class="progress-text">${state.translate(
|
||||||
|
transfer.msg,
|
||||||
|
transfer.sizes
|
||||||
|
)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
return div;
|
||||||
|
};
|
12
app/templates/error.js
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
const assets = require('../../common/assets');
|
||||||
|
|
||||||
|
module.exports = function(state) {
|
||||||
|
return html`
|
||||||
|
<div id="upload-error">
|
||||||
|
<div class="title">${state.translate('errorPageHeader')}</div>
|
||||||
|
<img id="upload-error-img" data-l10n-id="errorAltText" src="${assets.get(
|
||||||
|
'illustration_error.svg'
|
||||||
|
)}"/>
|
||||||
|
</div>`;
|
||||||
|
};
|
84
app/templates/file.js
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
const assets = require('../../common/assets');
|
||||||
|
|
||||||
|
function timeLeft(milliseconds) {
|
||||||
|
const minutes = Math.floor(milliseconds / 1000 / 60);
|
||||||
|
const hours = Math.floor(minutes / 60);
|
||||||
|
const seconds = Math.floor(milliseconds / 1000 % 60);
|
||||||
|
if (hours >= 1) {
|
||||||
|
return `${hours}h ${minutes % 60}m`;
|
||||||
|
} else if (hours === 0) {
|
||||||
|
return `${minutes}m ${seconds}s`;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = function(file, state, emit) {
|
||||||
|
const ttl = file.expiresAt - Date.now();
|
||||||
|
const remaining = timeLeft(ttl) || state.translate('linkExpiredAlt');
|
||||||
|
const row = html`
|
||||||
|
<tr id="${file.id}">
|
||||||
|
<td>${file.name}</td>
|
||||||
|
<td>
|
||||||
|
<img onclick=${copyClick} src="${assets.get(
|
||||||
|
'copy-16.svg'
|
||||||
|
)}" class="icon-copy" title="${state.translate('copyUrlHover')}">
|
||||||
|
<span class="text-copied" hidden="true">${state.translate(
|
||||||
|
'copiedUrl'
|
||||||
|
)}</span>
|
||||||
|
</td>
|
||||||
|
<td>${remaining}</td>
|
||||||
|
<td>
|
||||||
|
<img onclick=${showPopup} src="${assets.get(
|
||||||
|
'close-16.svg'
|
||||||
|
)}" class="icon-delete" title="${state.translate('deleteButtonHover')}">
|
||||||
|
<div class="popup">
|
||||||
|
<div class="popuptext" onblur=${cancel} tabindex="-1">
|
||||||
|
<div class="popup-message">${state.translate('deletePopupText')}</div>
|
||||||
|
<div class="popup-action">
|
||||||
|
<span class="popup-no" onclick=${cancel}>${state.translate(
|
||||||
|
'deletePopupCancel'
|
||||||
|
)}</span>
|
||||||
|
<span class="popup-yes" onclick=${deleteFile}>${state.translate(
|
||||||
|
'deletePopupYes'
|
||||||
|
)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
|
||||||
|
function copyClick(e) {
|
||||||
|
emit('copy', { url: file.url, location: 'upload-list' });
|
||||||
|
const icon = e.target;
|
||||||
|
const text = e.target.nextSibling;
|
||||||
|
icon.hidden = true;
|
||||||
|
text.hidden = false;
|
||||||
|
setTimeout(() => {
|
||||||
|
icon.hidden = false;
|
||||||
|
text.hidden = true;
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showPopup() {
|
||||||
|
const tr = document.getElementById(file.id);
|
||||||
|
const popup = tr.querySelector('.popuptext');
|
||||||
|
popup.classList.add('show');
|
||||||
|
popup.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancel(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
const tr = document.getElementById(file.id);
|
||||||
|
const popup = tr.querySelector('.popuptext');
|
||||||
|
popup.classList.remove('show');
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteFile() {
|
||||||
|
emit('delete', { file, location: 'upload-list' });
|
||||||
|
emit('render');
|
||||||
|
}
|
||||||
|
|
||||||
|
return row;
|
||||||
|
};
|
28
app/templates/fileList.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
const file = require('./file');
|
||||||
|
|
||||||
|
module.exports = function(state, emit) {
|
||||||
|
let table = '';
|
||||||
|
if (state.storage.files.length) {
|
||||||
|
table = html`
|
||||||
|
<table id="uploaded-files">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th id="uploaded-file">${state.translate('uploadedFile')}</th>
|
||||||
|
<th id="copy-file-list">${state.translate('copyFileList')}</th>
|
||||||
|
<th id="expiry-file-list">${state.translate('expiryFileList')}</th>
|
||||||
|
<th id="delete-file-list">${state.translate('deleteFileList')}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
${state.storage.files.map(f => file(f, state, emit))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
return html`
|
||||||
|
<div id="file-list">
|
||||||
|
${table}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
};
|
38
app/templates/legal.js
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
|
||||||
|
function replaceLinks(str, urls) {
|
||||||
|
let i = -1;
|
||||||
|
const s = str.replace(/<a>([^<]+)<\/a>/g, (m, v) => {
|
||||||
|
i++;
|
||||||
|
return `<a href="${urls[i]}">${v}</a>`;
|
||||||
|
});
|
||||||
|
return [`<div class="description">${s}</div>`];
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = function(state) {
|
||||||
|
const div = html`
|
||||||
|
<div id="page-one">
|
||||||
|
<div id="legal">
|
||||||
|
<div class="title">${state.translate('legalHeader')}</div>
|
||||||
|
${html(
|
||||||
|
replaceLinks(state.translate('legalNoticeTestPilot'), [
|
||||||
|
'https://testpilot.firefox.com/terms',
|
||||||
|
'https://testpilot.firefox.com/privacy',
|
||||||
|
'https://testpilot.firefox.com/experiments/send'
|
||||||
|
])
|
||||||
|
)}
|
||||||
|
${html(
|
||||||
|
replaceLinks(state.translate('legalNoticeMozilla'), [
|
||||||
|
'https://www.mozilla.org/privacy/websites/',
|
||||||
|
'https://www.mozilla.org/about/legal/terms/mozilla/'
|
||||||
|
])
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (state.layout) {
|
||||||
|
return state.layout(state, div);
|
||||||
|
}
|
||||||
|
return div;
|
||||||
|
};
|
27
app/templates/notFound.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
const assets = require('../../common/assets');
|
||||||
|
|
||||||
|
module.exports = function(state) {
|
||||||
|
const div = html`
|
||||||
|
<div id="page-one">
|
||||||
|
<div id="download">
|
||||||
|
<div class="title">${state.translate('expiredPageHeader')}</div>
|
||||||
|
<div class="share-window">
|
||||||
|
<img src="${assets.get(
|
||||||
|
'illustration_expired.svg'
|
||||||
|
)}" id="expired-img" data-l10n-id="linkExpiredAlt"/>
|
||||||
|
</div>
|
||||||
|
<div class="expired-description">${state.translate(
|
||||||
|
'uploadPageExplainer'
|
||||||
|
)}</div>
|
||||||
|
<a class="send-new" href="/" data-state="notfound">${state.translate(
|
||||||
|
'sendYourFilesLink'
|
||||||
|
)}</a>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
if (state.layout) {
|
||||||
|
return state.layout(state, div);
|
||||||
|
}
|
||||||
|
return div;
|
||||||
|
};
|
65
app/templates/preview.js
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
const assets = require('../../common/assets');
|
||||||
|
const notFound = require('./notFound');
|
||||||
|
const { bytes } = require('../utils');
|
||||||
|
|
||||||
|
function getFileFromDOM() {
|
||||||
|
const el = document.getElementById('dl-file');
|
||||||
|
if (!el) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const data = el.dataset;
|
||||||
|
return {
|
||||||
|
name: data.name,
|
||||||
|
size: parseInt(data.size, 10),
|
||||||
|
ttl: parseInt(data.ttl, 10)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = function(state, emit) {
|
||||||
|
state.fileInfo = state.fileInfo || getFileFromDOM();
|
||||||
|
if (!state.fileInfo) {
|
||||||
|
return notFound(state, emit);
|
||||||
|
}
|
||||||
|
state.fileInfo.id = state.params.id;
|
||||||
|
state.fileInfo.key = state.params.key;
|
||||||
|
const fileInfo = state.fileInfo;
|
||||||
|
const size = bytes(fileInfo.size);
|
||||||
|
const div = html`
|
||||||
|
<div id="page-one">
|
||||||
|
<div id="download">
|
||||||
|
<div id="download-page-one">
|
||||||
|
<div class="title">
|
||||||
|
<span id="dl-file"
|
||||||
|
data-name="${fileInfo.name}"
|
||||||
|
data-size="${fileInfo.size}"
|
||||||
|
data-ttl="${fileInfo.ttl}">${state.translate('downloadFileName', {
|
||||||
|
filename: fileInfo.name
|
||||||
|
})}</span>
|
||||||
|
<span id="dl-filesize">${' ' +
|
||||||
|
state.translate('downloadFileSize', { size })}</span>
|
||||||
|
</div>
|
||||||
|
<div class="description">${state.translate('downloadMessage')}</div>
|
||||||
|
<img src="${assets.get(
|
||||||
|
'illustration_download.svg'
|
||||||
|
)}" id="download-img" alt="${state.translate('downloadAltText')}"/>
|
||||||
|
<div>
|
||||||
|
<button id="download-btn" class="btn" onclick=${download}>${state.translate(
|
||||||
|
'downloadButtonLabel'
|
||||||
|
)}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a class="send-new" href="/">${state.translate('sendYourFilesLink')}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
function download(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
emit('download', fileInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.layout) {
|
||||||
|
return state.layout(state, div);
|
||||||
|
}
|
||||||
|
return div;
|
||||||
|
};
|
21
app/templates/progress.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
|
||||||
|
const radius = 73;
|
||||||
|
const oRadius = radius + 10;
|
||||||
|
const oDiameter = oRadius * 2;
|
||||||
|
const circumference = 2 * Math.PI * radius;
|
||||||
|
|
||||||
|
module.exports = function(progressRatio) {
|
||||||
|
const dashOffset = (1 - progressRatio) * circumference;
|
||||||
|
const percent = Math.floor(progressRatio * 100);
|
||||||
|
const div = html`
|
||||||
|
<div class="progress-bar">
|
||||||
|
<svg id="progress" width="${oDiameter}" height="${oDiameter}" viewPort="0 0 ${oDiameter} ${oDiameter}" version="1.1">
|
||||||
|
<circle r="${radius}" cx="${oRadius}" cy="${oRadius}" fill="transparent"/>
|
||||||
|
<circle id="bar" r="${radius}" cx="${oRadius}" cy="${oRadius}" fill="transparent" transform="rotate(-90 ${oRadius} ${oRadius})" stroke-dasharray="${circumference}" stroke-dashoffset="${dashOffset}"/>
|
||||||
|
<text class="percentage" text-anchor="middle" x="50%" y="98"><tspan class="percent-number">${percent}</tspan><tspan class="percent-sign">%</tspan></text>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return div;
|
||||||
|
};
|
61
app/templates/share.js
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
const assets = require('../../common/assets');
|
||||||
|
const notFound = require('./notFound');
|
||||||
|
const { allowedCopy, delay, fadeOut } = require('../utils');
|
||||||
|
|
||||||
|
module.exports = function(state, emit) {
|
||||||
|
const file = state.storage.getFileById(state.params.id);
|
||||||
|
if (!file) {
|
||||||
|
return notFound(state, emit);
|
||||||
|
}
|
||||||
|
const div = html`
|
||||||
|
<div id="share-link" class="fadeIn">
|
||||||
|
<div class="title">${state.translate('uploadSuccessTimingHeader')}</div>
|
||||||
|
<div id="share-window">
|
||||||
|
<div id="copy-text">${state.translate('copyUrlFormLabelWithName', {
|
||||||
|
filename: file.name
|
||||||
|
})}</div>
|
||||||
|
<div id="copy">
|
||||||
|
<input id="link" type="url" value="${file.url}" readonly="true"/>
|
||||||
|
<button id="copy-btn" class="btn" onclick=${copyLink}>${state.translate(
|
||||||
|
'copyUrlFormButton'
|
||||||
|
)}</button>
|
||||||
|
</div>
|
||||||
|
<button id="delete-file" class="btn" onclick=${deleteFile}>${state.translate(
|
||||||
|
'deleteFileButton'
|
||||||
|
)}</button>
|
||||||
|
<a class="send-new" data-state="completed" href="/" onclick=${sendNew}>${state.translate(
|
||||||
|
'sendAnotherFileLink'
|
||||||
|
)}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
async function sendNew(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
await fadeOut('share-link');
|
||||||
|
emit('pushState', '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyLink() {
|
||||||
|
if (allowedCopy()) {
|
||||||
|
emit('copy', { url: file.url, location: 'success-screen' });
|
||||||
|
const copyBtn = document.getElementById('copy-btn');
|
||||||
|
copyBtn.disabled = true;
|
||||||
|
copyBtn.replaceChild(
|
||||||
|
html`<img src="${assets.get('check-16.svg')}" class="icon-check">`,
|
||||||
|
copyBtn.firstChild
|
||||||
|
);
|
||||||
|
await delay(2000);
|
||||||
|
copyBtn.disabled = false;
|
||||||
|
copyBtn.textContent = state.translate('copyUrlFormButton');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteFile() {
|
||||||
|
emit('delete', { file, location: 'success-screen' });
|
||||||
|
await fadeOut('share-link');
|
||||||
|
emit('pushState', '/');
|
||||||
|
}
|
||||||
|
return div;
|
||||||
|
};
|
50
app/templates/unsupported.js
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
const assets = require('../../common/assets');
|
||||||
|
|
||||||
|
module.exports = function(state) {
|
||||||
|
const msg =
|
||||||
|
state.params.reason === 'outdated'
|
||||||
|
? html`
|
||||||
|
<div id="unsupported-browser">
|
||||||
|
<div class="title">${state.translate('notSupportedHeader')}</div>
|
||||||
|
<div class="description">${state.translate(
|
||||||
|
'notSupportedOutdatedDetail'
|
||||||
|
)}</div>
|
||||||
|
<a id="update-firefox" href="https://support.mozilla.org/kb/update-firefox-latest-version">
|
||||||
|
<img src="${assets.get(
|
||||||
|
'firefox_logo-only.svg'
|
||||||
|
)}" class="firefox-logo" alt="Firefox"/>
|
||||||
|
<div class="unsupported-button-text">${state.translate(
|
||||||
|
'updateFirefox'
|
||||||
|
)}</div>
|
||||||
|
</a>
|
||||||
|
<div class="unsupported-description">${state.translate(
|
||||||
|
'uploadPageExplainer'
|
||||||
|
)}</div>
|
||||||
|
</div>`
|
||||||
|
: html`
|
||||||
|
<div id="unsupported-browser">
|
||||||
|
<div class="title">${state.translate('notSupportedHeader')}</div>
|
||||||
|
<div class="description">${state.translate('notSupportedDetail')}</div>
|
||||||
|
<div class="description"><a href="https://github.com/mozilla/send/blob/master/docs/faq.md#why-is-my-browser-not-supported">${state.translate(
|
||||||
|
'notSupportedLink'
|
||||||
|
)}</a></div>
|
||||||
|
<a id="dl-firefox" href="https://www.mozilla.org/firefox/new/?scene=2">
|
||||||
|
<img src="${assets.get(
|
||||||
|
'firefox_logo-only.svg'
|
||||||
|
)}" class="firefox-logo" alt="Firefox"/>
|
||||||
|
<div class="unsupported-button-text">Firefox<br>
|
||||||
|
<span>${state.translate('downloadFirefoxButtonSub')}</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<div class="unsupported-description">${state.translate(
|
||||||
|
'uploadPageExplainer'
|
||||||
|
)}</div>
|
||||||
|
</div>`;
|
||||||
|
const div = html`<div id="page-one">${msg}</div>`;
|
||||||
|
|
||||||
|
if (state.layout) {
|
||||||
|
return state.layout(state, div);
|
||||||
|
}
|
||||||
|
return div;
|
||||||
|
};
|
38
app/templates/upload.js
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
const progress = require('./progress');
|
||||||
|
const { bytes } = require('../utils');
|
||||||
|
|
||||||
|
module.exports = function(state, emit) {
|
||||||
|
const transfer = state.transfer;
|
||||||
|
|
||||||
|
const div = html`
|
||||||
|
<div id="upload-progress" class="fadeIn">
|
||||||
|
<div class="title" id="upload-filename">${state.translate(
|
||||||
|
'uploadingPageProgress',
|
||||||
|
{
|
||||||
|
filename: transfer.file.name,
|
||||||
|
size: bytes(transfer.file.size)
|
||||||
|
}
|
||||||
|
)}</div>
|
||||||
|
<div class="description"></div>
|
||||||
|
${progress(transfer.progressRatio)}
|
||||||
|
<div class="upload">
|
||||||
|
<div class="progress-text">${state.translate(
|
||||||
|
transfer.msg,
|
||||||
|
transfer.sizes
|
||||||
|
)}</div>
|
||||||
|
<button id="cancel-upload" onclick=${cancel}>${state.translate(
|
||||||
|
'uploadingPageCancel'
|
||||||
|
)}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
function cancel() {
|
||||||
|
const btn = document.getElementById('cancel-upload');
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = state.translate('uploadCancelNotification');
|
||||||
|
emit('cancel');
|
||||||
|
}
|
||||||
|
return div;
|
||||||
|
};
|
55
app/templates/welcome.js
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
const assets = require('../../common/assets');
|
||||||
|
const fileList = require('./fileList');
|
||||||
|
const { fadeOut } = require('../utils');
|
||||||
|
|
||||||
|
module.exports = function(state, emit) {
|
||||||
|
const div = html`
|
||||||
|
<div id="page-one" class="fadeIn">
|
||||||
|
<div class="title">${state.translate('uploadPageHeader')}</div>
|
||||||
|
<div class="description">
|
||||||
|
<div>${state.translate('uploadPageExplainer')}</div>
|
||||||
|
<a href="https://testpilot.firefox.com/experiments/send" class="link">${state.translate(
|
||||||
|
'uploadPageLearnMore'
|
||||||
|
)}</a>
|
||||||
|
</div>
|
||||||
|
<div class="upload-window" ondragover=${dragover} ondragleave=${dragleave}>
|
||||||
|
<div id="upload-img"><img src="${assets.get(
|
||||||
|
'upload.svg'
|
||||||
|
)}" title="${state.translate('uploadSvgAlt')}"/></div>
|
||||||
|
<div id="upload-text">${state.translate('uploadPageDropMessage')}</div>
|
||||||
|
<span id="file-size-msg"><em>${state.translate(
|
||||||
|
'uploadPageSizeMessage'
|
||||||
|
)}</em></span>
|
||||||
|
<form method="post" action="upload" enctype="multipart/form-data">
|
||||||
|
<label for="file-upload" id="browse" class="btn">${state.translate(
|
||||||
|
'uploadPageBrowseButton1'
|
||||||
|
)}</label>
|
||||||
|
<input id="file-upload" type="file" name="fileUploaded" onchange=${upload} />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
${fileList(state, emit)}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
function dragover(event) {
|
||||||
|
event.target.classList.add('ondrag');
|
||||||
|
}
|
||||||
|
|
||||||
|
function dragleave(event) {
|
||||||
|
event.target.classList.remove('ondrag');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function upload(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const target = event.target;
|
||||||
|
const file = target.files[0];
|
||||||
|
await fadeOut('page-one');
|
||||||
|
emit('upload', { file, type: 'click' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.layout) {
|
||||||
|
return state.layout(state, div);
|
||||||
|
}
|
||||||
|
return div;
|
||||||
|
};
|
@ -35,47 +35,39 @@ function notify(str) {
|
|||||||
*/
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
function gcmCompliant() {
|
function loadShim(polyfill) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const shim = document.createElement('script');
|
||||||
|
shim.src = polyfill;
|
||||||
|
shim.addEventListener('load', () => resolve(true));
|
||||||
|
shim.addEventListener('error', () => resolve(false));
|
||||||
|
document.head.appendChild(shim);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function canHasSend(polyfill) {
|
||||||
try {
|
try {
|
||||||
return window.crypto.subtle
|
const key = await window.crypto.subtle.generateKey(
|
||||||
.generateKey(
|
|
||||||
{
|
{
|
||||||
name: 'AES-GCM',
|
name: 'AES-GCM',
|
||||||
length: 128
|
length: 128
|
||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
['encrypt', 'decrypt']
|
['encrypt', 'decrypt']
|
||||||
)
|
);
|
||||||
.then(key => {
|
|
||||||
return window.crypto.subtle
|
await window.crypto.subtle.encrypt(
|
||||||
.encrypt(
|
|
||||||
{
|
{
|
||||||
name: 'AES-GCM',
|
name: 'AES-GCM',
|
||||||
iv: window.crypto.getRandomValues(new Uint8Array(12)),
|
iv: window.crypto.getRandomValues(new Uint8Array(12)),
|
||||||
additionalData: window.crypto.getRandomValues(new Uint8Array(6)),
|
|
||||||
tagLength: 128
|
tagLength: 128
|
||||||
},
|
},
|
||||||
key,
|
key,
|
||||||
new ArrayBuffer(8)
|
new ArrayBuffer(8)
|
||||||
)
|
);
|
||||||
.then(() => {
|
return true;
|
||||||
return Promise.resolve();
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
return loadShim();
|
|
||||||
});
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return loadShim();
|
return loadShim(polyfill);
|
||||||
}
|
|
||||||
function loadShim() {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const shim = document.createElement('script');
|
|
||||||
shim.src = '/cryptofill.js';
|
|
||||||
shim.addEventListener('load', resolve);
|
|
||||||
shim.addEventListener('error', reject);
|
|
||||||
document.head.appendChild(shim);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,7 +99,8 @@ function copyToClipboard(str) {
|
|||||||
const LOCALIZE_NUMBERS = !!(
|
const LOCALIZE_NUMBERS = !!(
|
||||||
typeof Intl === 'object' &&
|
typeof Intl === 'object' &&
|
||||||
Intl &&
|
Intl &&
|
||||||
typeof Intl.NumberFormat === 'function'
|
typeof Intl.NumberFormat === 'function' &&
|
||||||
|
typeof navigator === 'object'
|
||||||
);
|
);
|
||||||
|
|
||||||
const UNITS = ['B', 'kB', 'MB', 'GB'];
|
const UNITS = ['B', 'kB', 'MB', 'GB'];
|
||||||
@ -134,9 +127,22 @@ function allowedCopy() {
|
|||||||
return support ? document.queryCommandSupported('copy') : false;
|
return support ? document.queryCommandSupported('copy') : false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function delay(delay = 100) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, delay));
|
||||||
|
}
|
||||||
|
|
||||||
|
function fadeOut(id) {
|
||||||
|
const classes = document.getElementById(id).classList;
|
||||||
|
classes.remove('fadeIn');
|
||||||
|
classes.add('fadeOut');
|
||||||
|
return delay(300);
|
||||||
|
}
|
||||||
|
|
||||||
const ONE_DAY_IN_MS = 86400000;
|
const ONE_DAY_IN_MS = 86400000;
|
||||||
|
|
||||||
export {
|
module.exports = {
|
||||||
|
fadeOut,
|
||||||
|
delay,
|
||||||
allowedCopy,
|
allowedCopy,
|
||||||
bytes,
|
bytes,
|
||||||
percent,
|
percent,
|
||||||
@ -144,7 +150,7 @@ export {
|
|||||||
arrayToHex,
|
arrayToHex,
|
||||||
hexToArray,
|
hexToArray,
|
||||||
notify,
|
notify,
|
||||||
gcmCompliant,
|
canHasSend,
|
||||||
isFile,
|
isFile,
|
||||||
ONE_DAY_IN_MS
|
ONE_DAY_IN_MS
|
||||||
};
|
};
|
Before Width: | Height: | Size: 257 B After Width: | Height: | Size: 257 B |
@ -1 +1 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill="#4A4A4A" d="M9.414 8l5.293-5.293a1 1 0 0 0-1.414-1.414L8 6.586 2.707 1.293a1 1 0 0 0-1.414 1.414L6.586 8l-5.293 5.293a1 1 0 1 0 1.414 1.414L8 9.414l5.293 5.293a1 1 0 0 0 1.414-1.414z"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 16 16"><path fill="#4A4A4A" d="M9.414 8l5.293-5.293a1 1 0 0 0-1.414-1.414L8 6.586 2.707 1.293a1 1 0 0 0-1.414 1.414L6.586 8l-5.293 5.293a1 1 0 1 0 1.414 1.414L8 9.414l5.293 5.293a1 1 0 0 0 1.414-1.414z"/></svg>
|
Before Width: | Height: | Size: 286 B After Width: | Height: | Size: 287 B |
Before Width: | Height: | Size: 416 B After Width: | Height: | Size: 416 B |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 649 B After Width: | Height: | Size: 649 B |
Before Width: | Height: | Size: 239 KiB After Width: | Height: | Size: 239 KiB |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 7.3 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 6.3 KiB |
@ -1,6 +1,6 @@
|
|||||||
/*** index.html ***/
|
/*** index.html ***/
|
||||||
html {
|
html {
|
||||||
background: url('../../public/resources/send_bg.svg');
|
background: url('./send_bg.svg');
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'segoe ui',
|
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'segoe ui',
|
||||||
'helvetica neue', helvetica, ubuntu, roboto, noto, arial, sans-serif;
|
'helvetica neue', helvetica, ubuntu, roboto, noto, arial, sans-serif;
|
||||||
font-weight: 200;
|
font-weight: 200;
|
||||||
@ -89,7 +89,7 @@ body {
|
|||||||
|
|
||||||
.feedback {
|
.feedback {
|
||||||
background-color: #0297f8;
|
background-color: #0297f8;
|
||||||
background-image: url('../../public/resources/feedback.svg');
|
background-image: url('./feedback.svg');
|
||||||
background-position: 2px 4px;
|
background-position: 2px 4px;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-size: 18px;
|
background-size: 18px;
|
||||||
@ -154,6 +154,36 @@ a {
|
|||||||
|
|
||||||
/** page-one **/
|
/** page-one **/
|
||||||
|
|
||||||
|
.fadeOut {
|
||||||
|
opacity: 0;
|
||||||
|
animation: fadeout 200ms linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeout {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fadeIn {
|
||||||
|
opacity: 1;
|
||||||
|
animation: fadein 200ms linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadein {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
font-size: 33px;
|
font-size: 33px;
|
||||||
line-height: 40px;
|
line-height: 40px;
|
||||||
@ -431,12 +461,8 @@ tbody {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.percentage {
|
.percentage {
|
||||||
position: absolute;
|
|
||||||
letter-spacing: -0.78px;
|
letter-spacing: -0.78px;
|
||||||
font-family: 'Segoe UI', 'SF Pro Text', sans-serif;
|
font-family: 'Segoe UI', 'SF Pro Text', sans-serif;
|
||||||
top: 50px;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-45%);
|
|
||||||
-moz-user-select: none;
|
-moz-user-select: none;
|
||||||
-ms-user-select: none;
|
-ms-user-select: none;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
@ -449,7 +475,8 @@ tbody {
|
|||||||
|
|
||||||
.percent-sign {
|
.percent-sign {
|
||||||
font-size: 28.8px;
|
font-size: 28.8px;
|
||||||
color: rgb(104, 104, 104);
|
stroke: none;
|
||||||
|
fill: #686868;
|
||||||
}
|
}
|
||||||
|
|
||||||
.upload {
|
.upload {
|
||||||
@ -471,10 +498,18 @@ tbody {
|
|||||||
|
|
||||||
#cancel-upload {
|
#cancel-upload {
|
||||||
color: #d70022;
|
color: #d70022;
|
||||||
|
background: #fff;
|
||||||
|
font-size: 15px;
|
||||||
|
border: 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#cancel-upload:disabled {
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: auto;
|
||||||
|
}
|
||||||
|
|
||||||
/** share-link **/
|
/** share-link **/
|
||||||
#share-window {
|
#share-window {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
@ -624,7 +659,7 @@ tbody {
|
|||||||
#update-firefox {
|
#update-firefox {
|
||||||
margin-bottom: 181px;
|
margin-bottom: 181px;
|
||||||
height: 80px;
|
height: 80px;
|
||||||
background: #12bc00;
|
background: #98e02b;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border: 0;
|
border: 0;
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 312 KiB After Width: | Height: | Size: 312 KiB |
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 86 KiB |
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 8.4 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 873 B After Width: | Height: | Size: 873 B |
Before Width: | Height: | Size: 336 B After Width: | Height: | Size: 336 B |
38
build/fluent_loader.js
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
const { MessageContext } = require('fluent');
|
||||||
|
|
||||||
|
function toJSON(map) {
|
||||||
|
return JSON.stringify(Array.from(map));
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = function(source) {
|
||||||
|
const localeExp = this.options.locale || /([^/]+)\/[^/]+\.ftl$/;
|
||||||
|
const result = localeExp.exec(this.resourcePath);
|
||||||
|
const locale = result && result[1];
|
||||||
|
// pre-parse the ftl
|
||||||
|
const context = new MessageContext(locale);
|
||||||
|
context.addMessages(source);
|
||||||
|
if (!locale) {
|
||||||
|
throw new Error(`couldn't find locale in: ${this.resourcePath}`);
|
||||||
|
}
|
||||||
|
return `
|
||||||
|
module.exports = \`
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
var fluent = require('fluent');
|
||||||
|
}
|
||||||
|
var ctx = new fluent.MessageContext('${locale}', {useIsolating: false});
|
||||||
|
ctx._messages = new Map(${toJSON(context._messages)});
|
||||||
|
function translate(id, data) {
|
||||||
|
var msg = ctx.getMessage(id);
|
||||||
|
if (typeof(msg) !== 'string' && !msg.val && msg.attrs) {
|
||||||
|
msg = msg.attrs.title || msg.attrs.alt
|
||||||
|
}
|
||||||
|
return ctx.format(msg, data);
|
||||||
|
}
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
module.exports = translate;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
window.translate = translate;
|
||||||
|
}
|
||||||
|
\``;
|
||||||
|
};
|
19
build/generate_asset_map.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
function kv(f) {
|
||||||
|
return `"${f}": require('../assets/${f}')`;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = function() {
|
||||||
|
const files = fs.readdirSync(path.join(__dirname, '..', 'assets'));
|
||||||
|
const code = `module.exports = {
|
||||||
|
"package.json": require('../package.json'),
|
||||||
|
${files.map(kv).join(',\n')}
|
||||||
|
};`;
|
||||||
|
return {
|
||||||
|
code,
|
||||||
|
dependencies: files.map(f => require.resolve('../assets/' + f)),
|
||||||
|
cacheable: false
|
||||||
|
};
|
||||||
|
};
|
22
build/generate_l10n_map.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
function kv(d) {
|
||||||
|
return `"${d}": require('../public/locales/${d}/send.ftl')`;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = function() {
|
||||||
|
const dirs = fs.readdirSync(path.join(__dirname, '..', 'public', 'locales'));
|
||||||
|
const code = `
|
||||||
|
module.exports = {
|
||||||
|
translate: function (id, data) { return window.translate(id, data) },
|
||||||
|
${dirs.map(kv).join(',\n')}
|
||||||
|
};`;
|
||||||
|
return {
|
||||||
|
code,
|
||||||
|
dependencies: dirs.map(d =>
|
||||||
|
require.resolve(`../public/locales/${d}/send.ftl`)
|
||||||
|
),
|
||||||
|
cacheable: false
|
||||||
|
};
|
||||||
|
};
|
11
build/package_json_loader.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
const commit = require('git-rev-sync').short();
|
||||||
|
|
||||||
|
module.exports = function(source) {
|
||||||
|
const pkg = JSON.parse(source);
|
||||||
|
const version = {
|
||||||
|
commit,
|
||||||
|
source: pkg.homepage,
|
||||||
|
version: process.env.CIRCLE_TAG || `v${pkg.version}`
|
||||||
|
};
|
||||||
|
return `module.exports = '${JSON.stringify(version)}'`;
|
||||||
|
};
|
32
common/assets.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
const genmap = require('../build/generate_asset_map');
|
||||||
|
const isServer = typeof genmap === 'function';
|
||||||
|
const prefix = isServer ? '/' : '';
|
||||||
|
let manifest = {};
|
||||||
|
try {
|
||||||
|
//eslint-disable-next-line node/no-missing-require
|
||||||
|
manifest = require('../dist/manifest.json');
|
||||||
|
} catch (e) {
|
||||||
|
// use middleware
|
||||||
|
}
|
||||||
|
|
||||||
|
const assets = isServer ? manifest : genmap;
|
||||||
|
|
||||||
|
function getAsset(name) {
|
||||||
|
return prefix + assets[name];
|
||||||
|
}
|
||||||
|
|
||||||
|
const instance = {
|
||||||
|
get: getAsset,
|
||||||
|
setMiddleware: function(middleware) {
|
||||||
|
if (middleware) {
|
||||||
|
instance.get = function getAssetWithMiddleware(name) {
|
||||||
|
const f = middleware.fileSystem.readFileSync(
|
||||||
|
middleware.getFilenameFromUrl('/manifest.json')
|
||||||
|
);
|
||||||
|
return prefix + JSON.parse(f)[name];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = instance;
|
51
common/locales.js
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
const gen = require('../build/generate_l10n_map');
|
||||||
|
|
||||||
|
const isServer = typeof gen === 'function';
|
||||||
|
const prefix = isServer ? '/' : '';
|
||||||
|
let manifest = {};
|
||||||
|
try {
|
||||||
|
//eslint-disable-next-line node/no-missing-require
|
||||||
|
manifest = require('../dist/manifest.json');
|
||||||
|
} catch (e) {
|
||||||
|
// use middleware
|
||||||
|
}
|
||||||
|
|
||||||
|
const locales = isServer ? manifest : gen;
|
||||||
|
|
||||||
|
function getLocale(name) {
|
||||||
|
return prefix + locales[`public/locales/${name}/send.ftl`];
|
||||||
|
}
|
||||||
|
|
||||||
|
function serverTranslator(name) {
|
||||||
|
return require(`../dist/${locales[`public/locales/${name}/send.ftl`]}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function browserTranslator() {
|
||||||
|
return locales.translate;
|
||||||
|
}
|
||||||
|
|
||||||
|
const translator = isServer ? serverTranslator : browserTranslator;
|
||||||
|
|
||||||
|
const instance = {
|
||||||
|
get: getLocale,
|
||||||
|
getTranslator: translator,
|
||||||
|
setMiddleware: function(middleware) {
|
||||||
|
if (middleware) {
|
||||||
|
const _eval = require('require-from-string');
|
||||||
|
instance.get = function getLocaleWithMiddleware(name) {
|
||||||
|
const f = middleware.fileSystem.readFileSync(
|
||||||
|
middleware.getFilenameFromUrl('/manifest.json')
|
||||||
|
);
|
||||||
|
return prefix + JSON.parse(f)[`public/locales/${name}/send.ftl`];
|
||||||
|
};
|
||||||
|
instance.getTranslator = function(name) {
|
||||||
|
const f = middleware.fileSystem.readFileSync(
|
||||||
|
middleware.getFilenameFromUrl(instance.get(name))
|
||||||
|
);
|
||||||
|
return _eval(f.toString());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = instance;
|
@ -1,20 +0,0 @@
|
|||||||
import Raven from 'raven-js';
|
|
||||||
import { unsupported } from './metrics';
|
|
||||||
|
|
||||||
if (navigator.doNotTrack !== '1' && window.RAVEN_CONFIG) {
|
|
||||||
Raven.config(window.SENTRY_ID, window.RAVEN_CONFIG).install();
|
|
||||||
}
|
|
||||||
|
|
||||||
const ua = navigator.userAgent.toLowerCase();
|
|
||||||
if (
|
|
||||||
ua.indexOf('firefox') > -1 &&
|
|
||||||
parseInt(ua.match(/firefox\/*([^\n\r]*)\./)[1], 10) <= 49
|
|
||||||
) {
|
|
||||||
unsupported({
|
|
||||||
err: new Error('Firefox is outdated.')
|
|
||||||
}).then(() => {
|
|
||||||
location.replace('/unsupported/outdated');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Raven };
|
|
@ -1,119 +0,0 @@
|
|||||||
import { Raven } from './common';
|
|
||||||
import FileReceiver from './fileReceiver';
|
|
||||||
import { bytes, notify, gcmCompliant } from './utils';
|
|
||||||
import Storage from './storage';
|
|
||||||
import * as links from './links';
|
|
||||||
import * as metrics from './metrics';
|
|
||||||
import * as progress from './progress';
|
|
||||||
|
|
||||||
const storage = new Storage();
|
|
||||||
function onUnload(size) {
|
|
||||||
metrics.cancelledDownload({ size });
|
|
||||||
}
|
|
||||||
|
|
||||||
async function download() {
|
|
||||||
const downloadBtn = document.getElementById('download-btn');
|
|
||||||
const downloadPanel = document.getElementById('download-page-one');
|
|
||||||
const progressPanel = document.getElementById('download-progress');
|
|
||||||
const file = document.getElementById('dl-file');
|
|
||||||
const size = Number(file.getAttribute('data-size'));
|
|
||||||
const ttl = Number(file.getAttribute('data-ttl'));
|
|
||||||
const unloadHandler = onUnload.bind(null, size);
|
|
||||||
const startTime = Date.now();
|
|
||||||
const fileReceiver = new FileReceiver(
|
|
||||||
'/assets' + location.pathname.slice(0, -1),
|
|
||||||
location.hash.slice(1)
|
|
||||||
);
|
|
||||||
|
|
||||||
downloadBtn.disabled = true;
|
|
||||||
downloadPanel.hidden = true;
|
|
||||||
progressPanel.hidden = false;
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const file = await fileReceiver.download();
|
|
||||||
const endTime = Date.now();
|
|
||||||
const time = endTime - startTime;
|
|
||||||
const downloadTime = endTime - downloadEnd;
|
|
||||||
const speed = size / (downloadTime / 1000);
|
|
||||||
|
|
||||||
links.setOpenInNewTab(false);
|
|
||||||
storage.totalDownloads += 1;
|
|
||||||
metrics.completedDownload({ size, time, speed });
|
|
||||||
progress.setText(' ');
|
|
||||||
document.l10n
|
|
||||||
.formatValues('downloadNotification', 'downloadFinish')
|
|
||||||
.then(translated => {
|
|
||||||
notify(translated[0]);
|
|
||||||
document.getElementById('dl-title').textContent = translated[1];
|
|
||||||
document.querySelector('#download-progress .description').textContent =
|
|
||||||
' ';
|
|
||||||
});
|
|
||||||
const dataView = new DataView(file.plaintext);
|
|
||||||
const blob = new Blob([dataView], { type: file.type });
|
|
||||||
const downloadUrl = URL.createObjectURL(blob);
|
|
||||||
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = downloadUrl;
|
|
||||||
if (window.navigator.msSaveBlob) {
|
|
||||||
window.navigator.msSaveBlob(blob, file.name);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
a.download = file.name;
|
|
||||||
document.body.appendChild(a);
|
|
||||||
a.click();
|
|
||||||
URL.revokeObjectURL(downloadUrl);
|
|
||||||
} catch (err) {
|
|
||||||
metrics.stoppedDownload({ size, err });
|
|
||||||
|
|
||||||
if (err.message === 'notfound') {
|
|
||||||
location.reload();
|
|
||||||
} else {
|
|
||||||
progressPanel.hidden = true;
|
|
||||||
downloadPanel.hidden = true;
|
|
||||||
document.getElementById('upload-error').hidden = false;
|
|
||||||
}
|
|
||||||
Raven.captureException(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
const file = document.getElementById('dl-file');
|
|
||||||
const filename = file.getAttribute('data-filename');
|
|
||||||
const b = Number(file.getAttribute('data-size'));
|
|
||||||
const size = bytes(b);
|
|
||||||
document.l10n.formatValue('downloadFileSize', { size }).then(str => {
|
|
||||||
document.getElementById('dl-filesize').textContent = str;
|
|
||||||
});
|
|
||||||
document.l10n
|
|
||||||
.formatValue('downloadingPageProgress', { filename, size })
|
|
||||||
.then(str => {
|
|
||||||
document.getElementById('dl-title').textContent = str;
|
|
||||||
});
|
|
||||||
|
|
||||||
gcmCompliant()
|
|
||||||
.then(() => {
|
|
||||||
document
|
|
||||||
.getElementById('download-btn')
|
|
||||||
.addEventListener('click', download);
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
metrics.unsupported({ err }).then(() => {
|
|
||||||
location.replace('/unsupported/gcm');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,163 +0,0 @@
|
|||||||
import FileSender from './fileSender';
|
|
||||||
import Storage from './storage';
|
|
||||||
import * as metrics from './metrics';
|
|
||||||
import { allowedCopy, copyToClipboard, ONE_DAY_IN_MS } from './utils';
|
|
||||||
import bel from 'bel';
|
|
||||||
import copyImg from '../../public/resources/copy-16.svg';
|
|
||||||
import closeImg from '../../public/resources/close-16.svg';
|
|
||||||
|
|
||||||
const HOUR = 1000 * 60 * 60;
|
|
||||||
const storage = new Storage();
|
|
||||||
let fileList = null;
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
fileList = document.getElementById('file-list');
|
|
||||||
toggleHeader();
|
|
||||||
Promise.all(
|
|
||||||
storage.files.map(file => {
|
|
||||||
const id = file.fileId;
|
|
||||||
return checkExistence(id).then(exists => {
|
|
||||||
if (exists) {
|
|
||||||
addFile(storage.getFileById(id));
|
|
||||||
} else {
|
|
||||||
storage.remove(id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.catch(err => console.error(err))
|
|
||||||
.then(toggleHeader);
|
|
||||||
});
|
|
||||||
|
|
||||||
function toggleHeader() {
|
|
||||||
fileList.hidden = storage.files.length === 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function timeLeft(milliseconds) {
|
|
||||||
const minutes = Math.floor(milliseconds / 1000 / 60);
|
|
||||||
const hours = Math.floor(minutes / 60);
|
|
||||||
const seconds = Math.floor(milliseconds / 1000 % 60);
|
|
||||||
if (hours >= 1) {
|
|
||||||
return `${hours}h ${minutes % 60}m`;
|
|
||||||
} else if (hours === 0) {
|
|
||||||
return `${minutes}m ${seconds}s`;
|
|
||||||
}
|
|
||||||
return 'Expired';
|
|
||||||
}
|
|
||||||
|
|
||||||
function addFile(file) {
|
|
||||||
if (!file) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
file.creationDate = new Date(file.creationDate);
|
|
||||||
const url = `${file.url}#${file.secretKey}`;
|
|
||||||
const future = new Date();
|
|
||||||
future.setTime(file.creationDate.getTime() + file.expiry);
|
|
||||||
const countdown = future.getTime() - Date.now();
|
|
||||||
|
|
||||||
const row = bel`
|
|
||||||
<tr>
|
|
||||||
<td>${file.name}</td>
|
|
||||||
<td>
|
|
||||||
<span class="icon-docs" data-l10n-id="copyUrlHover"></span>
|
|
||||||
<img onclick=${copyClick} src="${copyImg}" class="icon-copy" data-l10n-id="copyUrlHover">
|
|
||||||
<span data-l10n-id="copiedUrl" class="text-copied" hidden="true"></span>
|
|
||||||
</td>
|
|
||||||
<td>${timeLeft(countdown)}</td>
|
|
||||||
<td>
|
|
||||||
<span class="icon-cancel-1" data-l10n-id="deleteButtonHover" title="Delete"></span>
|
|
||||||
<img onclick=${showPopup} src="${closeImg}" class="icon-delete" data-l10n-id="deleteButtonHover" title="Delete">
|
|
||||||
<div class="popup">
|
|
||||||
<div class="popuptext" onclick=${stopProp} onblur=${cancel} tabindex="-1">
|
|
||||||
<div class="popup-message" data-l10n-id="deletePopupText"></div>
|
|
||||||
<div class="popup-action">
|
|
||||||
<span class="popup-no" onclick=${cancel} data-l10n-id="deletePopupCancel"></span>
|
|
||||||
<span class="popup-yes" onclick=${deleteFile} data-l10n-id="deletePopupYes"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
`;
|
|
||||||
const popup = row.querySelector('.popuptext');
|
|
||||||
const timeCol = row.querySelectorAll('td')[2];
|
|
||||||
if (!allowedCopy()) {
|
|
||||||
row.querySelector('.icon-copy').disabled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
fileList.querySelector('tbody').appendChild(row);
|
|
||||||
toggleHeader();
|
|
||||||
poll();
|
|
||||||
|
|
||||||
function copyClick(e) {
|
|
||||||
metrics.copiedLink({ location: 'upload-list' });
|
|
||||||
copyToClipboard(url);
|
|
||||||
const icon = e.target;
|
|
||||||
const text = e.target.nextSibling;
|
|
||||||
icon.hidden = true;
|
|
||||||
text.hidden = false;
|
|
||||||
setTimeout(() => {
|
|
||||||
icon.hidden = false;
|
|
||||||
text.hidden = true;
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
function poll() {
|
|
||||||
const countdown = future.getTime() - Date.now();
|
|
||||||
if (countdown <= 0) {
|
|
||||||
storage.remove(file.fileId);
|
|
||||||
row.parentNode.removeChild(row);
|
|
||||||
toggleHeader();
|
|
||||||
}
|
|
||||||
timeCol.textContent = timeLeft(countdown);
|
|
||||||
setTimeout(poll, countdown >= HOUR ? 60000 : 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteFile() {
|
|
||||||
FileSender.delete(file.fileId, file.deleteToken);
|
|
||||||
const ttl = ONE_DAY_IN_MS - (Date.now() - file.creationDate.getTime());
|
|
||||||
metrics.deletedUpload({
|
|
||||||
size: file.size,
|
|
||||||
time: file.totalTime,
|
|
||||||
speed: file.uploadSpeed,
|
|
||||||
type: file.typeOfUpload,
|
|
||||||
location: 'upload-list',
|
|
||||||
ttl
|
|
||||||
});
|
|
||||||
row.parentNode.removeChild(row);
|
|
||||||
storage.remove(file.fileId);
|
|
||||||
toggleHeader();
|
|
||||||
}
|
|
||||||
|
|
||||||
function showPopup() {
|
|
||||||
popup.classList.add('show');
|
|
||||||
popup.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
function cancel(e) {
|
|
||||||
e.stopPropagation();
|
|
||||||
popup.classList.remove('show');
|
|
||||||
}
|
|
||||||
|
|
||||||
function stopProp(e) {
|
|
||||||
e.stopPropagation();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function checkExistence(id) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const xhr = new XMLHttpRequest();
|
|
||||||
xhr.onreadystatechange = () => {
|
|
||||||
if (xhr.readyState === XMLHttpRequest.HEADERS_RECEIVED) {
|
|
||||||
resolve(xhr.status === 200);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
xhr.onerror = reject;
|
|
||||||
xhr.ontimeout = reject;
|
|
||||||
xhr.open('get', '/exists/' + id);
|
|
||||||
xhr.timeout = 2000;
|
|
||||||
xhr.send();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export { addFile };
|
|
@ -1,21 +0,0 @@
|
|||||||
let links = [];
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
links = Array.from(document.querySelectorAll('a:not([target])'));
|
|
||||||
});
|
|
||||||
|
|
||||||
function setOpenInNewTab(bool) {
|
|
||||||
if (bool === false) {
|
|
||||||
links.forEach(l => {
|
|
||||||
l.removeAttribute('target');
|
|
||||||
l.removeAttribute('rel');
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
links.forEach(l => {
|
|
||||||
l.setAttribute('target', '_blank');
|
|
||||||
l.setAttribute('rel', 'noopener noreferrer');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export { setOpenInNewTab };
|
|
@ -1,47 +0,0 @@
|
|||||||
import { bytes, percent } from './utils';
|
|
||||||
|
|
||||||
let percentText = null;
|
|
||||||
let text = null;
|
|
||||||
let title = null;
|
|
||||||
let bar = null;
|
|
||||||
let updateTitle = false;
|
|
||||||
|
|
||||||
const radius = 73;
|
|
||||||
const circumference = 2 * Math.PI * radius;
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
percentText = document.querySelector('.percent-number');
|
|
||||||
text = document.querySelector('.progress-text');
|
|
||||||
bar = document.getElementById('bar');
|
|
||||||
title = document.querySelector('title');
|
|
||||||
});
|
|
||||||
|
|
||||||
document.addEventListener('blur', function() {
|
|
||||||
updateTitle = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
document.addEventListener('focus', function() {
|
|
||||||
updateTitle = false;
|
|
||||||
return title && (title.textContent = 'Firefox Send');
|
|
||||||
});
|
|
||||||
|
|
||||||
function setProgress(params) {
|
|
||||||
const ratio = params.complete / params.total;
|
|
||||||
bar.setAttribute('stroke-dashoffset', (1 - ratio) * circumference);
|
|
||||||
percentText.textContent = Math.floor(ratio * 100);
|
|
||||||
if (updateTitle) {
|
|
||||||
title.textContent = percent(ratio);
|
|
||||||
}
|
|
||||||
document.l10n
|
|
||||||
.formatValue('fileSizeProgress', {
|
|
||||||
partialSize: bytes(params.complete),
|
|
||||||
totalSize: bytes(params.total)
|
|
||||||
})
|
|
||||||
.then(setText);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setText(str) {
|
|
||||||
text.textContent = str;
|
|
||||||
}
|
|
||||||
|
|
||||||
export { setProgress, setText };
|
|
@ -1,257 +0,0 @@
|
|||||||
/* global MAXFILESIZE EXPIRE_SECONDS */
|
|
||||||
import { Raven } from './common';
|
|
||||||
import FileSender from './fileSender';
|
|
||||||
import {
|
|
||||||
allowedCopy,
|
|
||||||
bytes,
|
|
||||||
copyToClipboard,
|
|
||||||
notify,
|
|
||||||
gcmCompliant,
|
|
||||||
ONE_DAY_IN_MS
|
|
||||||
} from './utils';
|
|
||||||
import Storage from './storage';
|
|
||||||
import * as metrics from './metrics';
|
|
||||||
import * as progress from './progress';
|
|
||||||
import * as fileList from './fileList';
|
|
||||||
import checkImg from '../../public/resources/check-16.svg';
|
|
||||||
|
|
||||||
const storage = new Storage();
|
|
||||||
|
|
||||||
async function upload(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
const pageOne = document.getElementById('page-one');
|
|
||||||
const link = document.getElementById('link');
|
|
||||||
const uploadWindow = document.querySelector('.upload-window');
|
|
||||||
const uploadError = document.getElementById('upload-error');
|
|
||||||
const uploadProgress = document.getElementById('upload-progress');
|
|
||||||
const clickOrDrop = event.type === 'drop' ? 'drop' : 'click';
|
|
||||||
|
|
||||||
// don't allow upload if not on upload page
|
|
||||||
if (pageOne.hidden) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
storage.totalUploads += 1;
|
|
||||||
|
|
||||||
let file = '';
|
|
||||||
if (clickOrDrop === 'drop') {
|
|
||||||
if (!event.dataTransfer.files[0]) {
|
|
||||||
uploadWindow.classList.remove('ondrag');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
event.dataTransfer.files.length > 1 ||
|
|
||||||
event.dataTransfer.files[0].size === 0
|
|
||||||
) {
|
|
||||||
uploadWindow.classList.remove('ondrag');
|
|
||||||
document.l10n.formatValue('uploadPageMultipleFilesAlert').then(str => {
|
|
||||||
alert(str);
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
file = event.dataTransfer.files[0];
|
|
||||||
} else {
|
|
||||||
file = event.target.files[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file.size > MAXFILESIZE) {
|
|
||||||
return document.l10n
|
|
||||||
.formatValue('fileTooBig', { size: bytes(MAXFILESIZE) })
|
|
||||||
.then(alert);
|
|
||||||
}
|
|
||||||
|
|
||||||
pageOne.hidden = true;
|
|
||||||
uploadError.hidden = true;
|
|
||||||
uploadProgress.hidden = false;
|
|
||||||
document.l10n
|
|
||||||
.formatValue('uploadingPageProgress', {
|
|
||||||
size: bytes(file.size),
|
|
||||||
filename: file.name
|
|
||||||
})
|
|
||||||
.then(str => {
|
|
||||||
document.getElementById('upload-filename').textContent = str;
|
|
||||||
});
|
|
||||||
document.l10n.formatValue('importingFile').then(progress.setText);
|
|
||||||
//don't allow drag and drop when not on page-one
|
|
||||||
document.body.removeEventListener('drop', upload);
|
|
||||||
|
|
||||||
const fileSender = new FileSender(file);
|
|
||||||
document.getElementById('cancel-upload').addEventListener('click', () => {
|
|
||||||
fileSender.cancel();
|
|
||||||
metrics.cancelledUpload({
|
|
||||||
size: file.size,
|
|
||||||
type: clickOrDrop
|
|
||||||
});
|
|
||||||
location.reload();
|
|
||||||
});
|
|
||||||
|
|
||||||
let uploadStart;
|
|
||||||
fileSender.on('progress', data => {
|
|
||||||
uploadStart = uploadStart || Date.now();
|
|
||||||
progress.setProgress({
|
|
||||||
complete: data[0],
|
|
||||||
total: data[1]
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
fileSender.on('encrypting', () => {
|
|
||||||
document.l10n.formatValue('encryptingFile').then(progress.setText);
|
|
||||||
});
|
|
||||||
|
|
||||||
let t;
|
|
||||||
const startTime = Date.now();
|
|
||||||
metrics.startedUpload({
|
|
||||||
size: file.size,
|
|
||||||
type: clickOrDrop
|
|
||||||
});
|
|
||||||
// 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 time = endTime - startTime;
|
|
||||||
const uploadTime = endTime - uploadStart;
|
|
||||||
const speed = file.size / (uploadTime / 1000);
|
|
||||||
const expiration = EXPIRE_SECONDS * 1000;
|
|
||||||
|
|
||||||
link.setAttribute('value', `${info.url}#${info.secretKey}`);
|
|
||||||
|
|
||||||
const copyText = document.getElementById('copy-text');
|
|
||||||
copyText.setAttribute(
|
|
||||||
'data-l10n-args',
|
|
||||||
JSON.stringify({ filename: file.name })
|
|
||||||
);
|
|
||||||
copyText.setAttribute('data-l10n-id', 'copyUrlFormLabelWithName');
|
|
||||||
|
|
||||||
metrics.completedUpload({
|
|
||||||
size: file.size,
|
|
||||||
time,
|
|
||||||
speed,
|
|
||||||
type: clickOrDrop
|
|
||||||
});
|
|
||||||
|
|
||||||
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: time,
|
|
||||||
typeOfUpload: clickOrDrop,
|
|
||||||
uploadSpeed: speed
|
|
||||||
};
|
|
||||||
|
|
||||||
document.getElementById('delete-file').addEventListener('click', () => {
|
|
||||||
FileSender.delete(fileData.fileId, fileData.deleteToken).then(() => {
|
|
||||||
const ttl =
|
|
||||||
ONE_DAY_IN_MS - (Date.now() - fileData.creationDate.getTime());
|
|
||||||
metrics
|
|
||||||
.deletedUpload({
|
|
||||||
size: fileData.size,
|
|
||||||
time: fileData.totalTime,
|
|
||||||
speed: fileData.uploadSpeed,
|
|
||||||
type: fileData.typeOfUpload,
|
|
||||||
location: 'success-screen',
|
|
||||||
ttl
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
storage.remove(fileData.fileId);
|
|
||||||
location.reload();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
storage.addFile(info.fileId, fileData);
|
|
||||||
|
|
||||||
pageOne.hidden = true;
|
|
||||||
uploadProgress.hidden = true;
|
|
||||||
uploadError.hidden = true;
|
|
||||||
document.getElementById('share-link').hidden = false;
|
|
||||||
|
|
||||||
fileList.addFile(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);
|
|
||||||
pageOne.hidden = true;
|
|
||||||
uploadProgress.hidden = true;
|
|
||||||
uploadError.hidden = false;
|
|
||||||
window.clearTimeout(t);
|
|
||||||
|
|
||||||
metrics.stoppedUpload({
|
|
||||||
size: file.size,
|
|
||||||
type: clickOrDrop,
|
|
||||||
err
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}, 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
gcmCompliant()
|
|
||||||
.then(function() {
|
|
||||||
const pageOne = document.getElementById('page-one');
|
|
||||||
const copyBtn = document.getElementById('copy-btn');
|
|
||||||
const link = document.getElementById('link');
|
|
||||||
const uploadWindow = document.querySelector('.upload-window');
|
|
||||||
|
|
||||||
pageOne.hidden = false;
|
|
||||||
document.getElementById('file-upload').addEventListener('change', upload);
|
|
||||||
|
|
||||||
document.body.addEventListener('dragover', allowDrop);
|
|
||||||
document.body.addEventListener('drop', upload);
|
|
||||||
|
|
||||||
// reset copy button
|
|
||||||
copyBtn.disabled = !allowedCopy();
|
|
||||||
copyBtn.setAttribute('data-l10n-id', 'copyUrlFormButton');
|
|
||||||
|
|
||||||
link.disabled = false;
|
|
||||||
|
|
||||||
// copy link to clipboard
|
|
||||||
copyBtn.addEventListener('click', () => {
|
|
||||||
if (allowedCopy() && copyToClipboard(link.getAttribute('value'))) {
|
|
||||||
metrics.copiedLink({ location: 'success-screen' });
|
|
||||||
|
|
||||||
//disable button for 3s
|
|
||||||
copyBtn.disabled = true;
|
|
||||||
link.disabled = true;
|
|
||||||
copyBtn.innerHTML = `<img src="${checkImg}" class="icon-check"></img>`;
|
|
||||||
setTimeout(() => {
|
|
||||||
copyBtn.disabled = !allowedCopy();
|
|
||||||
copyBtn.setAttribute('data-l10n-id', 'copyUrlFormButton');
|
|
||||||
link.disabled = false;
|
|
||||||
}, 3000);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
uploadWindow.addEventListener('dragover', () =>
|
|
||||||
uploadWindow.classList.add('ondrag')
|
|
||||||
);
|
|
||||||
uploadWindow.addEventListener('dragleave', () =>
|
|
||||||
uploadWindow.classList.remove('ondrag')
|
|
||||||
);
|
|
||||||
|
|
||||||
// on file upload by browse or drag & drop
|
|
||||||
|
|
||||||
function allowDrop(ev) {
|
|
||||||
ev.preventDefault();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
metrics.unsupported({ err }).then(() => {
|
|
||||||
location.replace('/unsupported/gcm');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
4941
package-lock.json
generated
196
package.json
@ -3,73 +3,109 @@
|
|||||||
"description": "File Sharing Experiment",
|
"description": "File Sharing Experiment",
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"author": "Mozilla (https://mozilla.org)",
|
"author": "Mozilla (https://mozilla.org)",
|
||||||
"dependencies": {
|
"repository": "mozilla/send",
|
||||||
"aws-sdk": "^2.98.0",
|
"homepage": "https://github.com/mozilla/send/",
|
||||||
"body-parser": "^1.17.2",
|
"license": "MPL-2.0",
|
||||||
"connect-busboy": "0.0.2",
|
"private": true,
|
||||||
"convict": "^3.0.0",
|
"scripts": {
|
||||||
"express": "^4.15.3",
|
"precommit": "lint-staged",
|
||||||
"express-handlebars": "^3.0.0",
|
"clean": "rimraf dist",
|
||||||
"helmet": "^3.8.0",
|
"build": "npm run clean && webpack -p",
|
||||||
"mozlog": "^2.1.1",
|
"lint": "npm-run-all lint:*",
|
||||||
"raven": "^2.1.0",
|
"lint:css": "stylelint 'assets/*.css'",
|
||||||
"redis": "^2.8.0"
|
"lint:js": "eslint .",
|
||||||
|
"lint-locales": "node scripts/lint-locales",
|
||||||
|
"lint-locales:dev": "npm run lint-locales",
|
||||||
|
"lint-locales:prod": "npm run lint-locales -- --production",
|
||||||
|
"format": "prettier '!(dist|node_modules|assets)/**/*.js' 'assets/*.css' --single-quote --write",
|
||||||
|
"get-prod-locales": "node scripts/get-prod-locales",
|
||||||
|
"get-prod-locales:write": "npm run get-prod-locales -- --write",
|
||||||
|
"changelog": "github-changes -o mozilla -r send --only-pulls --use-commit-body --no-merges",
|
||||||
|
"contributors": "git shortlog -s | awk -F\\t '{print $2}' > CONTRIBUTORS",
|
||||||
|
"release": "npm-run-all contributors changelog",
|
||||||
|
"test": "mocha test/unit",
|
||||||
|
"start": "cross-env NODE_ENV=development webpack-dev-server",
|
||||||
|
"prod": "node server/prod.js"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"lint-staged": {
|
||||||
"asmcrypto.js": "0.0.11",
|
"*.js": [
|
||||||
"autoprefixer": "^7.1.2",
|
"prettier --single-quote --write",
|
||||||
"babel-core": "^6.25.0",
|
"eslint",
|
||||||
"babel-loader": "^7.1.1",
|
"git add"
|
||||||
"babel-polyfill": "^6.23.0",
|
],
|
||||||
"babel-preset-es2015": "^6.24.1",
|
"*.css": [
|
||||||
"babel-preset-stage-2": "^6.24.1",
|
"prettier --single-quote --write",
|
||||||
"bel": "^5.0.3",
|
"stylelint",
|
||||||
"browserify": "^14.4.0",
|
"git add"
|
||||||
"copy-webpack-plugin": "^4.0.1",
|
]
|
||||||
"cross-env": "^5.0.5",
|
|
||||||
"css-loader": "^0.28.4",
|
|
||||||
"css-mqpacker": "^6.0.1",
|
|
||||||
"cssnano": "^3.10.0",
|
|
||||||
"eslint": "^4.3.0",
|
|
||||||
"eslint-plugin-mocha": "^4.11.0",
|
|
||||||
"eslint-plugin-node": "^5.1.1",
|
|
||||||
"eslint-plugin-security": "^1.4.0",
|
|
||||||
"extract-loader": "^1.0.0",
|
|
||||||
"file-loader": "^0.11.2",
|
|
||||||
"git-rev-sync": "^1.9.1",
|
|
||||||
"github-changes": "^1.1.0",
|
|
||||||
"html-loader": "^0.5.1",
|
|
||||||
"html-webpack-plugin": "^2.30.1",
|
|
||||||
"husky": "^0.14.3",
|
|
||||||
"l20n": "^5.0.0",
|
|
||||||
"lint-staged": "^4.0.3",
|
|
||||||
"mkdirp": "^0.5.1",
|
|
||||||
"mocha": "^3.4.2",
|
|
||||||
"npm-run-all": "^4.0.2",
|
|
||||||
"postcss-cli": "^4.1.0",
|
|
||||||
"postcss-loader": "^2.0.6",
|
|
||||||
"prettier": "^1.5.3",
|
|
||||||
"proxyquire": "^1.8.0",
|
|
||||||
"raven-js": "^3.17.0",
|
|
||||||
"rimraf": "^2.6.1",
|
|
||||||
"selenium-webdriver": "^3.5.0",
|
|
||||||
"sinon": "^2.3.8",
|
|
||||||
"stylelint": "^8.0.0",
|
|
||||||
"stylelint-config-standard": "^17.0.0",
|
|
||||||
"stylelint-no-unsupported-browser-features": "^1.0.0",
|
|
||||||
"supertest": "^3.0.0",
|
|
||||||
"testpilot-ga": "^0.3.0",
|
|
||||||
"webcrypto-liner": "^0.1.25",
|
|
||||||
"webpack": "^3.5.4",
|
|
||||||
"webpack-dev-middleware": "^1.12.0"
|
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8.2.0"
|
"node": ">=8.2.0"
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/mozilla/send/",
|
"devDependencies": {
|
||||||
"license": "MPL-2.0",
|
"autoprefixer": "^7.1.2",
|
||||||
"repository": "mozilla/send",
|
"babel-core": "^6.26.0",
|
||||||
|
"babel-loader": "^7.1.2",
|
||||||
|
"babel-plugin-yo-yoify": "^0.7.0",
|
||||||
|
"babel-polyfill": "^6.26.0",
|
||||||
|
"babel-preset-env": "^1.6.0",
|
||||||
|
"babel-preset-es2015": "^6.24.1",
|
||||||
|
"babel-preset-stage-2": "^6.24.1",
|
||||||
|
"choo-log": "^7.2.1",
|
||||||
|
"copy-webpack-plugin": "^4.0.1",
|
||||||
|
"cross-env": "^5.0.5",
|
||||||
|
"css-loader": "^0.28.5",
|
||||||
|
"css-mqpacker": "^6.0.1",
|
||||||
|
"cssnano": "^3.10.0",
|
||||||
|
"eslint": "^4.5.0",
|
||||||
|
"eslint-plugin-mocha": "^4.11.0",
|
||||||
|
"eslint-plugin-node": "^5.1.1",
|
||||||
|
"eslint-plugin-security": "^1.4.0",
|
||||||
|
"expose-loader": "^0.7.3",
|
||||||
|
"extract-loader": "^1.0.1",
|
||||||
|
"file-loader": "^0.11.2",
|
||||||
|
"git-rev-sync": "^1.9.1",
|
||||||
|
"github-changes": "^1.1.0",
|
||||||
|
"html-loader": "^0.5.1",
|
||||||
|
"husky": "^0.14.3",
|
||||||
|
"lint-staged": "^4.0.4",
|
||||||
|
"mocha": "^3.5.0",
|
||||||
|
"nanobus": "^4.2.0",
|
||||||
|
"npm-run-all": "^4.0.2",
|
||||||
|
"postcss-loader": "^2.0.6",
|
||||||
|
"prettier": "^1.5.3",
|
||||||
|
"proxyquire": "^1.8.0",
|
||||||
|
"raven-js": "^3.17.0",
|
||||||
|
"require-from-string": "^1.2.1",
|
||||||
|
"rimraf": "^2.6.1",
|
||||||
|
"selenium-webdriver": "^3.5.0",
|
||||||
|
"sinon": "^3.2.1",
|
||||||
|
"stylelint-config-standard": "^17.0.0",
|
||||||
|
"stylelint-no-unsupported-browser-features": "^1.0.0",
|
||||||
|
"supertest": "^3.0.0",
|
||||||
|
"testpilot-ga": "^0.3.0",
|
||||||
|
"val-loader": "^1.0.2",
|
||||||
|
"webpack": "^3.5.5",
|
||||||
|
"webpack-dev-server": "^2.7.1",
|
||||||
|
"webpack-manifest-plugin": "^1.3.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"aws-sdk": "^2.103.0",
|
||||||
|
"body-parser": "^1.17.2",
|
||||||
|
"choo": "^6.0.0",
|
||||||
|
"connect-busboy": "0.0.2",
|
||||||
|
"convict": "^4.0.0",
|
||||||
|
"express": "^4.15.4",
|
||||||
|
"express-request-language": "^1.1.12",
|
||||||
|
"fluent": "^0.4.1",
|
||||||
|
"fluent-langneg": "^0.1.0",
|
||||||
|
"helmet": "^3.8.1",
|
||||||
|
"mozlog": "^2.1.1",
|
||||||
|
"raven": "^2.1.2",
|
||||||
|
"redis": "^2.8.0"
|
||||||
|
},
|
||||||
"availableLanguages": [
|
"availableLanguages": [
|
||||||
|
"en-US",
|
||||||
"ast",
|
"ast",
|
||||||
"az",
|
"az",
|
||||||
"ca",
|
"ca",
|
||||||
@ -78,7 +114,6 @@
|
|||||||
"de",
|
"de",
|
||||||
"dsb",
|
"dsb",
|
||||||
"el",
|
"el",
|
||||||
"en-US",
|
|
||||||
"es-AR",
|
"es-AR",
|
||||||
"es-CL",
|
"es-CL",
|
||||||
"es-ES",
|
"es-ES",
|
||||||
@ -108,42 +143,5 @@
|
|||||||
"vi",
|
"vi",
|
||||||
"zh-CN",
|
"zh-CN",
|
||||||
"zh-TW"
|
"zh-TW"
|
||||||
],
|
|
||||||
"scripts": {
|
|
||||||
"precommit": "lint-staged",
|
|
||||||
"clean": "rimraf dist",
|
|
||||||
"build": "npm-run-all build:*",
|
|
||||||
"build:js": "webpack -p",
|
|
||||||
"build:version": "node scripts/version",
|
|
||||||
"changelog": "github-changes -o mozilla -r send --only-pulls --use-commit-body --no-merges",
|
|
||||||
"contributors": "git shortlog -s | awk -F\\t '{print $2}' > CONTRIBUTORS",
|
|
||||||
"dev": "npm run clean && npm run build && npm start",
|
|
||||||
"format": "prettier '{,frontend/src/,scripts/,server/,test/**/!(bundle)}*.{js,css}' --single-quote --write",
|
|
||||||
"get-prod-locales": "node scripts/get-prod-locales",
|
|
||||||
"get-prod-locales:write": "npm run get-prod-locales -- --write",
|
|
||||||
"lint": "npm-run-all lint:*",
|
|
||||||
"lint:css": "stylelint 'frontend/src/*.css'",
|
|
||||||
"lint:js": "eslint .",
|
|
||||||
"lint-locales": "node scripts/lint-locales",
|
|
||||||
"lint-locales:dev": "npm run lint-locales",
|
|
||||||
"lint-locales:prod": "npm run lint-locales -- --production",
|
|
||||||
"release": "npm-run-all contributors changelog",
|
|
||||||
"start": "node server/server",
|
|
||||||
"test": "cross-env NODE_ENV=test npm-run-all test:*",
|
|
||||||
"test:unit": "mocha test/unit",
|
|
||||||
"test:server": "mocha test/server",
|
|
||||||
"test--browser": "browserify test/frontend/frontend.bundle.js -o test/frontend/bundle.js -d && node test/frontend/driver.js"
|
|
||||||
},
|
|
||||||
"lint-staged": {
|
|
||||||
"*.js": [
|
|
||||||
"prettier --single-quote --write",
|
|
||||||
"eslint",
|
|
||||||
"git add"
|
|
||||||
],
|
|
||||||
"*.css": [
|
|
||||||
"prettier --single-quote --write",
|
|
||||||
"stylelint",
|
|
||||||
"git add"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
@ -2,13 +2,13 @@ const autoprefixer = require('autoprefixer');
|
|||||||
const cssnano = require('cssnano');
|
const cssnano = require('cssnano');
|
||||||
const mqpacker = require('css-mqpacker');
|
const mqpacker = require('css-mqpacker');
|
||||||
|
|
||||||
const conf = require('./server/config');
|
const config = require('./server/config');
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
plugins: [autoprefixer, mqpacker, cssnano]
|
plugins: [autoprefixer, mqpacker, cssnano]
|
||||||
};
|
};
|
||||||
|
|
||||||
if (conf.env === 'development') {
|
if (config.env === 'development') {
|
||||||
options.map = { inline: true };
|
options.map = { inline: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 3.3 KiB |
@ -1,21 +0,0 @@
|
|||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
const pkg = require('../package.json');
|
|
||||||
const mkdirp = require('mkdirp');
|
|
||||||
|
|
||||||
let commit;
|
|
||||||
|
|
||||||
try {
|
|
||||||
commit = require('git-rev-sync').short();
|
|
||||||
} catch (err) {
|
|
||||||
// Whatever...
|
|
||||||
}
|
|
||||||
|
|
||||||
const filename = path.join(__dirname, '..', 'dist', 'public', 'version.json');
|
|
||||||
const filedata = {
|
|
||||||
commit,
|
|
||||||
source: pkg.homepage,
|
|
||||||
version: process.env.CIRCLE_TAG || `v${pkg.version}`
|
|
||||||
};
|
|
||||||
mkdirp.sync(path.dirname(filename));
|
|
||||||
fs.writeFileSync(filename, JSON.stringify(filedata, null, 2) + '\n');
|
|
13
server/dev.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
const assets = require('../common/assets');
|
||||||
|
const locales = require('../common/locales');
|
||||||
|
const routes = require('./routes');
|
||||||
|
const pages = require('./routes/pages');
|
||||||
|
|
||||||
|
module.exports = function(app, devServer) {
|
||||||
|
assets.setMiddleware(devServer.middleware);
|
||||||
|
locales.setMiddleware(devServer.middleware);
|
||||||
|
routes(app);
|
||||||
|
// webpack-dev-server routes haven't been added yet
|
||||||
|
// so wait for next tick to add 404 handler
|
||||||
|
process.nextTick(() => app.use(pages.notfound));
|
||||||
|
};
|
16
server/languages.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
const { availableLanguages } = require('../package.json');
|
||||||
|
const config = require('./config');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
function allLangs() {
|
||||||
|
const langs = fs.readdirSync(path.join(__dirname, '..', 'public', 'locales'));
|
||||||
|
langs.unshift('en-US'); // default first, TODO change for fluent-langneg
|
||||||
|
return langs;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.l10n_dev) {
|
||||||
|
module.exports = allLangs();
|
||||||
|
} else {
|
||||||
|
module.exports = availableLanguages;
|
||||||
|
}
|
98
server/layout.js
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
const html = require('choo/html');
|
||||||
|
const assets = require('../common/assets');
|
||||||
|
const locales = require('../common/locales');
|
||||||
|
|
||||||
|
module.exports = function(state, body = '') {
|
||||||
|
const firaTag = state.fira
|
||||||
|
? html`<link rel="stylesheet" type="text/css" href="https://code.cdn.mozilla.net/fonts/fira.css" />`
|
||||||
|
: '';
|
||||||
|
return html`
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|
||||||
|
<meta property="og:title" content="${state.title}"/>
|
||||||
|
<meta name="twitter:title" content="${state.title}"/>
|
||||||
|
<meta name="description" content="${state.description}"/>
|
||||||
|
<meta property="og:description" content="${state.description}"/>
|
||||||
|
<meta name="twitter:description" content="${state.description}"/>
|
||||||
|
<meta name="twitter:card" content="summary"/>
|
||||||
|
<meta property="og:image" content="${state.baseUrl}${assets.get(
|
||||||
|
'send-fb.jpg'
|
||||||
|
)}"/>
|
||||||
|
<meta name="twitter:image" content="${state.baseUrl}${assets.get(
|
||||||
|
'send-twitter.jpg'
|
||||||
|
)}"/>
|
||||||
|
<meta property="og:url" content="${state.baseUrl}"/>
|
||||||
|
|
||||||
|
<title>${state.title}</title>
|
||||||
|
|
||||||
|
<link rel="stylesheet" type="text/css" href="${assets.get('main.css')}" />
|
||||||
|
<link rel="icon" type="image/png" href="${assets.get(
|
||||||
|
'favicon-32x32.png'
|
||||||
|
)}" sizes="32x32" />
|
||||||
|
${firaTag}
|
||||||
|
<script defer src="/jsconfig.js"></script>
|
||||||
|
<script defer src="${assets.get('runtime.js')}"></script>
|
||||||
|
<script defer src="${assets.get('vendor.js')}"></script>
|
||||||
|
<script defer src="${locales.get(state.locale)}"></script>
|
||||||
|
<script defer src="${assets.get('app.js')}"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header class="header">
|
||||||
|
<div class="send-logo">
|
||||||
|
<a href="/">
|
||||||
|
<img src="${assets.get(
|
||||||
|
'send_logo.svg'
|
||||||
|
)}" alt="Send"/><h1 class="site-title">Send</h1>
|
||||||
|
</a>
|
||||||
|
<div class="site-subtitle">
|
||||||
|
<a href="https://testpilot.firefox.com">Firefox Test Pilot</a>
|
||||||
|
<div>${state.translate('siteSubtitle')}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a href="https://qsurvey.mozilla.com/s3/txp-firefox-send" rel="noreferrer noopener" class="feedback" target="_blank">${state.translate(
|
||||||
|
'siteFeedback'
|
||||||
|
)}</a>
|
||||||
|
</header>
|
||||||
|
<div class="all">
|
||||||
|
<noscript>
|
||||||
|
<h2>Firefox Send requires JavaScript</h2>
|
||||||
|
<p><a href="https://github.com/mozilla/send/blob/master/docs/faq.md#why-does-firefox-send-require-javascript">Why does Firefox Send require JavaScript?</a></p>
|
||||||
|
<p>Please enable JavaScript and try again.</p>
|
||||||
|
</noscript>
|
||||||
|
${body}
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
<div class="legal-links">
|
||||||
|
<a href="https://www.mozilla.org" role="presentation"><img class="mozilla-logo" src="${assets.get(
|
||||||
|
'mozilla-logo.svg'
|
||||||
|
)}" alt="mozilla"/></a>
|
||||||
|
<a href="https://www.mozilla.org/about/legal">${state.translate(
|
||||||
|
'footerLinkLegal'
|
||||||
|
)}</a>
|
||||||
|
<a href="https://testpilot.firefox.com/about">${state.translate(
|
||||||
|
'footerLinkAbout'
|
||||||
|
)}</a>
|
||||||
|
<a href="/legal">${state.translate('footerLinkPrivacy')}</a>
|
||||||
|
<a href="/legal">${state.translate('footerLinkTerms')}</a>
|
||||||
|
<a href="https://www.mozilla.org/privacy/websites/#cookies">${state.translate(
|
||||||
|
'footerLinkCookies'
|
||||||
|
)}</a>
|
||||||
|
</div>
|
||||||
|
<div class="social-links">
|
||||||
|
<a href="https://github.com/mozilla/send" role="presentation"><img class="github" src="${assets.get(
|
||||||
|
'github-icon.svg'
|
||||||
|
)}" alt="github"/></a>
|
||||||
|
<a href="https://twitter.com/FxTestPilot" role="presentation"><img class="twitter" src="${assets.get(
|
||||||
|
'twitter-icon.svg'
|
||||||
|
)}" alt="twitter"/></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
};
|
@ -1,4 +1,4 @@
|
|||||||
const conf = require('./config.js');
|
const conf = require('./config');
|
||||||
|
|
||||||
const isProduction = conf.env === 'production';
|
const isProduction = conf.env === 'production';
|
||||||
|
|
||||||
|
26
server/prod.js
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const path = require('path');
|
||||||
|
const Raven = require('raven');
|
||||||
|
const config = require('./config');
|
||||||
|
const routes = require('./routes');
|
||||||
|
const pages = require('./routes/pages');
|
||||||
|
|
||||||
|
if (config.sentry_dsn) {
|
||||||
|
Raven.config(config.sentry_dsn).install();
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
app.use(
|
||||||
|
express.static(path.resolve(__dirname, '../dist/'), {
|
||||||
|
setHeaders: function(res) {
|
||||||
|
res.set('Cache-Control', 'public, max-age=31536000, immutable');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
routes(app);
|
||||||
|
|
||||||
|
app.use(pages.notfound);
|
||||||
|
|
||||||
|
app.listen(1443);
|
30
server/routes/delete.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
const storage = require('../storage');
|
||||||
|
|
||||||
|
function validateID(route_id) {
|
||||||
|
return route_id.match(/^[0-9a-fA-F]{10}$/) !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = async function(req, res) {
|
||||||
|
const id = req.params.id;
|
||||||
|
|
||||||
|
if (!validateID(id)) {
|
||||||
|
res.sendStatus(404);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const delete_token = req.body.delete_token;
|
||||||
|
|
||||||
|
if (!delete_token) {
|
||||||
|
res.sendStatus(404);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const err = await storage.delete(id, delete_token);
|
||||||
|
if (!err) {
|
||||||
|
res.sendStatus(200);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
res.sendStatus(404);
|
||||||
|
}
|
||||||
|
};
|
38
server/routes/download.js
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
const storage = require('../storage');
|
||||||
|
const mozlog = require('../log');
|
||||||
|
const log = mozlog('send.download');
|
||||||
|
|
||||||
|
function validateID(route_id) {
|
||||||
|
return route_id.match(/^[0-9a-fA-F]{10}$/) !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = async function(req, res) {
|
||||||
|
const id = req.params.id;
|
||||||
|
if (!validateID(id)) {
|
||||||
|
return res.sendStatus(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const meta = await storage.metadata(id);
|
||||||
|
const contentLength = await storage.length(id);
|
||||||
|
res.writeHead(200, {
|
||||||
|
'Content-Disposition': `attachment; filename=${meta.filename}`,
|
||||||
|
'Content-Type': 'application/octet-stream',
|
||||||
|
'Content-Length': contentLength,
|
||||||
|
'X-File-Metadata': JSON.stringify(meta)
|
||||||
|
});
|
||||||
|
const file_stream = storage.get(id);
|
||||||
|
|
||||||
|
file_stream.on('end', async () => {
|
||||||
|
try {
|
||||||
|
await storage.forceDelete(id);
|
||||||
|
} catch (e) {
|
||||||
|
log.info('DeleteError:', id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
file_stream.pipe(res);
|
||||||
|
} catch (e) {
|
||||||
|
res.sendStatus(404);
|
||||||
|
}
|
||||||
|
};
|
19
server/routes/exists.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
const storage = require('../storage');
|
||||||
|
|
||||||
|
function validateID(route_id) {
|
||||||
|
return route_id.match(/^[0-9a-fA-F]{10}$/) !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = async (req, res) => {
|
||||||
|
const id = req.params.id;
|
||||||
|
if (!validateID(id)) {
|
||||||
|
return res.sendStatus(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await storage.exists(id);
|
||||||
|
res.sendStatus(200);
|
||||||
|
} catch (e) {
|
||||||
|
res.sendStatus(404);
|
||||||
|
}
|
||||||
|
};
|
80
server/routes/index.js
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
const busboy = require('connect-busboy');
|
||||||
|
const helmet = require('helmet');
|
||||||
|
const bodyParser = require('body-parser');
|
||||||
|
const requestLanguage = require('express-request-language');
|
||||||
|
const languages = require('../languages');
|
||||||
|
const storage = require('../storage');
|
||||||
|
const config = require('../config');
|
||||||
|
const pages = require('./pages');
|
||||||
|
// const lang = require('fluent-langneg')
|
||||||
|
|
||||||
|
module.exports = function(app) {
|
||||||
|
app.use(
|
||||||
|
requestLanguage({
|
||||||
|
languages
|
||||||
|
})
|
||||||
|
);
|
||||||
|
app.use(helmet());
|
||||||
|
app.use(
|
||||||
|
helmet.hsts({
|
||||||
|
maxAge: 31536000,
|
||||||
|
force: config.env === 'production'
|
||||||
|
})
|
||||||
|
);
|
||||||
|
app.use(
|
||||||
|
helmet.contentSecurityPolicy({
|
||||||
|
directives: {
|
||||||
|
defaultSrc: ["'self'"],
|
||||||
|
connectSrc: [
|
||||||
|
"'self'",
|
||||||
|
'https://sentry.prod.mozaws.net',
|
||||||
|
'https://www.google-analytics.com'
|
||||||
|
],
|
||||||
|
imgSrc: ["'self'", 'https://www.google-analytics.com'],
|
||||||
|
scriptSrc: ["'self'"],
|
||||||
|
styleSrc: ["'self'", 'https://code.cdn.mozilla.net'],
|
||||||
|
fontSrc: ["'self'", 'https://code.cdn.mozilla.net'],
|
||||||
|
formAction: ["'none'"],
|
||||||
|
frameAncestors: ["'none'"],
|
||||||
|
objectSrc: ["'none'"],
|
||||||
|
reportUri: '/__cspreport__'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
app.use(
|
||||||
|
busboy({
|
||||||
|
limits: {
|
||||||
|
fileSize: config.max_file_size
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
app.use(bodyParser.json());
|
||||||
|
app.get('/', pages.index);
|
||||||
|
app.get('/legal', pages.legal);
|
||||||
|
app.get('/jsconfig.js', require('./jsconfig'));
|
||||||
|
app.get('/share/:id', pages.blank);
|
||||||
|
app.get('/download/:id', pages.download);
|
||||||
|
app.get('/completed', pages.blank);
|
||||||
|
app.get('/unsupported/:reason', pages.unsupported);
|
||||||
|
app.post('/api/upload', require('./upload'));
|
||||||
|
app.get('/api/download/:id', require('./download'));
|
||||||
|
app.get('/api/exists/:id', require('./exists'));
|
||||||
|
app.post('/api/delete/:id', require('./delete'));
|
||||||
|
|
||||||
|
app.get('/__version__', function(req, res) {
|
||||||
|
res.sendFile(require.resolve('../../dist/version.json'));
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/__lbheartbeat__', function(req, res) {
|
||||||
|
res.sendStatus(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('__heartbeat__', async (req, res) => {
|
||||||
|
try {
|
||||||
|
await storage.ping();
|
||||||
|
res.sendStatus(200);
|
||||||
|
} catch (e) {
|
||||||
|
res.sendStatus(500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
46
server/routes/jsconfig.js
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
const config = require('../config');
|
||||||
|
|
||||||
|
let sentry = '';
|
||||||
|
if (config.sentry_id) {
|
||||||
|
//eslint-disable-next-line node/no-missing-require
|
||||||
|
const version = require('../../dist/version.json');
|
||||||
|
sentry = `
|
||||||
|
var RAVEN_CONFIG = {
|
||||||
|
release: '${version.version}',
|
||||||
|
tags: {
|
||||||
|
commit: '${version.commit}'
|
||||||
|
},
|
||||||
|
dataCallback: function (data) {
|
||||||
|
var hash = window.location.hash;
|
||||||
|
if (hash) {
|
||||||
|
return JSON.parse(JSON.stringify(data).replace(new RegExp(hash.slice(1), 'g'), ''));
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var SENTRY_ID = '${config.sentry_id}';
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let ga = '';
|
||||||
|
if (config.analytics_id) {
|
||||||
|
ga = `var GOOGLE_ANALYTICS_ID = ${config.analytics_id};`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* eslint-disable no-useless-escape */
|
||||||
|
const jsconfig = `
|
||||||
|
var isIE = /trident\\\/7\.|msie/i.test(navigator.userAgent);
|
||||||
|
var isUnsupportedPage = /\\\/unsupported/.test(location.pathname);
|
||||||
|
if (isIE && !isUnsupportedPage) {
|
||||||
|
window.location.replace('/unsupported/ie');
|
||||||
|
}
|
||||||
|
var MAXFILESIZE = ${config.max_file_size};
|
||||||
|
var EXPIRE_SECONDS = ${config.expire_seconds};
|
||||||
|
${ga}
|
||||||
|
${sentry}
|
||||||
|
`;
|
||||||
|
|
||||||
|
module.exports = function(req, res) {
|
||||||
|
res.set('Content-Type', 'application/javascript');
|
||||||
|
res.send(jsconfig);
|
||||||
|
};
|
68
server/routes/pages.js
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
const routes = require('../../app/routes');
|
||||||
|
const storage = require('../storage');
|
||||||
|
const state = require('../state');
|
||||||
|
|
||||||
|
function validateID(route_id) {
|
||||||
|
return route_id.match(/^[0-9a-fA-F]{10}$/) !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stripEvents(str) {
|
||||||
|
// For CSP we need to remove all the event handler placeholders.
|
||||||
|
// It's ok, app.js will add them when it attaches to the DOM.
|
||||||
|
return str.replace(/\son\w+=""/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
index: function(req, res) {
|
||||||
|
res.send(stripEvents(routes.toString('/', state(req))));
|
||||||
|
},
|
||||||
|
|
||||||
|
blank: function(req, res) {
|
||||||
|
res.send(stripEvents(routes.toString('/blank', state(req))));
|
||||||
|
},
|
||||||
|
|
||||||
|
download: async function(req, res, next) {
|
||||||
|
const id = req.params.id;
|
||||||
|
if (!validateID(id)) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const efilename = await storage.filename(id);
|
||||||
|
const name = decodeURIComponent(efilename);
|
||||||
|
const size = await storage.length(id);
|
||||||
|
const ttl = await storage.ttl(id);
|
||||||
|
res.send(
|
||||||
|
stripEvents(
|
||||||
|
routes.toString(
|
||||||
|
`/download/${req.params.id}`,
|
||||||
|
Object.assign(state(req), {
|
||||||
|
fileInfo: { name, size, ttl }
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
unsupported: function(req, res) {
|
||||||
|
res.send(
|
||||||
|
stripEvents(
|
||||||
|
routes.toString(
|
||||||
|
`/unsupported/${req.params.reason}`,
|
||||||
|
Object.assign(state(req), { fira: true })
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
legal: function(req, res) {
|
||||||
|
res.send(stripEvents(routes.toString('/legal', state(req))));
|
||||||
|
},
|
||||||
|
|
||||||
|
notfound: function(req, res) {
|
||||||
|
res.status(404).send(stripEvents(routes.toString('/404', state(req))));
|
||||||
|
}
|
||||||
|
};
|
65
server/routes/upload.js
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
const crypto = require('crypto');
|
||||||
|
const storage = require('../storage');
|
||||||
|
const config = require('../config');
|
||||||
|
const mozlog = require('../log');
|
||||||
|
|
||||||
|
const log = mozlog('send.upload');
|
||||||
|
|
||||||
|
const validateIV = route_id => {
|
||||||
|
return route_id.match(/^[0-9a-fA-F]{24}$/) !== null;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = function(req, res) {
|
||||||
|
const newId = crypto.randomBytes(5).toString('hex');
|
||||||
|
let meta;
|
||||||
|
|
||||||
|
try {
|
||||||
|
meta = JSON.parse(req.header('X-File-Metadata'));
|
||||||
|
} catch (e) {
|
||||||
|
res.sendStatus(400);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!meta.hasOwnProperty('id') ||
|
||||||
|
!meta.hasOwnProperty('filename') ||
|
||||||
|
!validateIV(meta.id)
|
||||||
|
) {
|
||||||
|
res.sendStatus(404);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
meta.delete = crypto.randomBytes(10).toString('hex');
|
||||||
|
req.pipe(req.busboy);
|
||||||
|
|
||||||
|
req.busboy.on(
|
||||||
|
'file',
|
||||||
|
async (fieldname, file, filename, encoding, mimeType) => {
|
||||||
|
try {
|
||||||
|
meta.mimeType = mimeType || 'application/octet-stream';
|
||||||
|
await storage.set(newId, file, filename, meta);
|
||||||
|
const protocol = config.env === 'production' ? 'https' : req.protocol;
|
||||||
|
const url = `${protocol}://${req.get('host')}/download/${newId}/`;
|
||||||
|
res.json({
|
||||||
|
url,
|
||||||
|
delete: meta.delete,
|
||||||
|
id: newId
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
log.error('upload', e);
|
||||||
|
if (e.message === 'limit') {
|
||||||
|
return res.sendStatus(413);
|
||||||
|
}
|
||||||
|
res.sendStatus(500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
req.on('close', async err => {
|
||||||
|
try {
|
||||||
|
await storage.forceDelete(newId);
|
||||||
|
} catch (e) {
|
||||||
|
log.info('DeleteError:', newId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
328
server/server.js
@ -1,328 +0,0 @@
|
|||||||
const express = require('express');
|
|
||||||
const exphbs = require('express-handlebars');
|
|
||||||
const busboy = require('connect-busboy');
|
|
||||||
const path = require('path');
|
|
||||||
const bodyParser = require('body-parser');
|
|
||||||
const helmet = require('helmet');
|
|
||||||
const conf = require('./config.js');
|
|
||||||
const storage = require('./storage.js');
|
|
||||||
const Raven = require('raven');
|
|
||||||
const crypto = require('crypto');
|
|
||||||
const fs = require('fs');
|
|
||||||
const version = require('../dist/public/version.json');
|
|
||||||
|
|
||||||
if (conf.sentry_dsn) {
|
|
||||||
Raven.config(conf.sentry_dsn).install();
|
|
||||||
}
|
|
||||||
|
|
||||||
const mozlog = require('./log.js');
|
|
||||||
|
|
||||||
const log = mozlog('send.server');
|
|
||||||
|
|
||||||
const STATIC_PATH = path.join(__dirname, '../dist/public');
|
|
||||||
|
|
||||||
const app = express();
|
|
||||||
|
|
||||||
function allLangs() {
|
|
||||||
return fs
|
|
||||||
.readdirSync(path.join(STATIC_PATH, 'locales'))
|
|
||||||
.map(function(f) {
|
|
||||||
return f.split('.')[0];
|
|
||||||
})
|
|
||||||
.join(',');
|
|
||||||
}
|
|
||||||
|
|
||||||
function prodLangs() {
|
|
||||||
return require('../package.json').availableLanguages.join(',');
|
|
||||||
}
|
|
||||||
|
|
||||||
const availableLanguages = conf.l10n_dev ? allLangs() : prodLangs();
|
|
||||||
|
|
||||||
// dev middleware is broken at the moment because of how webpack builds the
|
|
||||||
// handlebars templates. Leaving the commented code here as a mark of shame.
|
|
||||||
|
|
||||||
// if (conf.env === 'development') {
|
|
||||||
// const webpack = require('webpack');
|
|
||||||
// const webpackDevMiddleware = require('webpack-dev-middleware');
|
|
||||||
// const config = require('../webpack.config.js');
|
|
||||||
// config.devtool = 'inline-source-map';
|
|
||||||
// const compiler = webpack(config);
|
|
||||||
// const wdm = webpackDevMiddleware(compiler, {
|
|
||||||
// publicPath: config.output.publicPath
|
|
||||||
// });
|
|
||||||
// app.use(wdm);
|
|
||||||
// }
|
|
||||||
app.set('views', 'dist/views/');
|
|
||||||
app.engine(
|
|
||||||
'handlebars',
|
|
||||||
exphbs({
|
|
||||||
defaultLayout: 'main',
|
|
||||||
layoutsDir: 'dist/views/layouts',
|
|
||||||
helpers: {
|
|
||||||
availableLanguages,
|
|
||||||
baseUrl: conf.base_url,
|
|
||||||
title: 'Firefox Send',
|
|
||||||
description:
|
|
||||||
'Encrypt and send files with a link that automatically expires to ensure your important documents don’t stay online forever.'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
app.set('view engine', 'handlebars');
|
|
||||||
|
|
||||||
app.use(helmet());
|
|
||||||
app.use(
|
|
||||||
helmet.hsts({
|
|
||||||
maxAge: 31536000,
|
|
||||||
force: conf.env === 'production'
|
|
||||||
})
|
|
||||||
);
|
|
||||||
app.use(
|
|
||||||
helmet.contentSecurityPolicy({
|
|
||||||
directives: {
|
|
||||||
defaultSrc: ["'self'"],
|
|
||||||
connectSrc: [
|
|
||||||
"'self'",
|
|
||||||
'https://sentry.prod.mozaws.net',
|
|
||||||
'https://www.google-analytics.com'
|
|
||||||
],
|
|
||||||
imgSrc: ["'self'", 'https://www.google-analytics.com'],
|
|
||||||
scriptSrc: ["'self'"],
|
|
||||||
styleSrc: ["'self'", 'https://code.cdn.mozilla.net'],
|
|
||||||
fontSrc: ["'self'", 'https://code.cdn.mozilla.net'],
|
|
||||||
formAction: ["'none'"],
|
|
||||||
frameAncestors: ["'none'"],
|
|
||||||
objectSrc: ["'none'"],
|
|
||||||
reportUri: '/__cspreport__'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
app.use(
|
|
||||||
busboy({
|
|
||||||
limits: {
|
|
||||||
fileSize: conf.max_file_size
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
app.use(bodyParser.json());
|
|
||||||
app.use(
|
|
||||||
'/resources',
|
|
||||||
express.static(path.join(STATIC_PATH, 'resources'), {
|
|
||||||
setHeaders: function(res) {
|
|
||||||
res.set('Cache-Control', 'public, max-age=31536000, immutable');
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
app.use(express.static(STATIC_PATH));
|
|
||||||
|
|
||||||
app.get('/', (req, res) => {
|
|
||||||
res.render('index');
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/unsupported/:reason', (req, res) => {
|
|
||||||
const outdated = req.params.reason === 'outdated';
|
|
||||||
res.render('unsupported', {
|
|
||||||
outdated,
|
|
||||||
fira: true
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/legal', (req, res) => {
|
|
||||||
res.render('legal');
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/jsconfig.js', (req, res) => {
|
|
||||||
res.set('Content-Type', 'application/javascript');
|
|
||||||
res.render('jsconfig', {
|
|
||||||
googleAnalyticsId: conf.analytics_id,
|
|
||||||
sentryId: conf.sentry_id,
|
|
||||||
version: version.version,
|
|
||||||
commit: version.commit,
|
|
||||||
maxFileSize: conf.max_file_size,
|
|
||||||
expireSeconds: conf.expire_seconds,
|
|
||||||
layout: false
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/exists/:id', async (req, res) => {
|
|
||||||
const id = req.params.id;
|
|
||||||
if (!validateID(id)) {
|
|
||||||
res.sendStatus(404);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await storage.exists(id);
|
|
||||||
res.sendStatus(200);
|
|
||||||
} catch (e) {
|
|
||||||
res.sendStatus(404);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/download/:id', async (req, res) => {
|
|
||||||
const id = req.params.id;
|
|
||||||
if (!validateID(id)) {
|
|
||||||
res.status(404).render('notfound');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const efilename = await storage.filename(id);
|
|
||||||
const filename = decodeURIComponent(efilename);
|
|
||||||
const filenameJson = JSON.stringify({ filename });
|
|
||||||
const sizeInBytes = await storage.length(id);
|
|
||||||
const ttl = await storage.ttl(id);
|
|
||||||
res.render('download', {
|
|
||||||
filename,
|
|
||||||
filenameJson,
|
|
||||||
sizeInBytes,
|
|
||||||
ttl
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
res.status(404).render('notfound');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/assets/download/:id', async (req, res) => {
|
|
||||||
const id = req.params.id;
|
|
||||||
if (!validateID(id)) {
|
|
||||||
res.sendStatus(404);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const meta = await storage.metadata(id);
|
|
||||||
const contentLength = await storage.length(id);
|
|
||||||
res.writeHead(200, {
|
|
||||||
'Content-Disposition': `attachment; filename=${meta.filename}`,
|
|
||||||
'Content-Type': 'application/octet-stream',
|
|
||||||
'Content-Length': contentLength,
|
|
||||||
'X-File-Metadata': JSON.stringify(meta)
|
|
||||||
});
|
|
||||||
const file_stream = storage.get(id);
|
|
||||||
|
|
||||||
file_stream.on('end', async () => {
|
|
||||||
try {
|
|
||||||
await storage.forceDelete(id);
|
|
||||||
} catch (e) {
|
|
||||||
log.info('DeleteError:', id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
file_stream.pipe(res);
|
|
||||||
} catch (e) {
|
|
||||||
res.sendStatus(404);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.post('/delete/:id', async (req, res) => {
|
|
||||||
const id = req.params.id;
|
|
||||||
|
|
||||||
if (!validateID(id)) {
|
|
||||||
res.sendStatus(404);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const delete_token = req.body.delete_token;
|
|
||||||
|
|
||||||
if (!delete_token) {
|
|
||||||
res.sendStatus(404);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const err = await storage.delete(id, delete_token);
|
|
||||||
if (!err) {
|
|
||||||
res.sendStatus(200);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
res.sendStatus(404);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.post('/upload', (req, res, next) => {
|
|
||||||
const newId = crypto.randomBytes(5).toString('hex');
|
|
||||||
let meta;
|
|
||||||
|
|
||||||
try {
|
|
||||||
meta = JSON.parse(req.header('X-File-Metadata'));
|
|
||||||
} catch (e) {
|
|
||||||
res.sendStatus(400);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
!meta.hasOwnProperty('id') ||
|
|
||||||
!meta.hasOwnProperty('filename') ||
|
|
||||||
!validateIV(meta.id)
|
|
||||||
) {
|
|
||||||
res.sendStatus(404);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
meta.delete = crypto.randomBytes(10).toString('hex');
|
|
||||||
req.pipe(req.busboy);
|
|
||||||
|
|
||||||
req.busboy.on(
|
|
||||||
'file',
|
|
||||||
async (fieldname, file, filename, encoding, mimeType) => {
|
|
||||||
try {
|
|
||||||
meta.mimeType = mimeType || 'application/octet-stream';
|
|
||||||
await storage.set(newId, file, filename, meta);
|
|
||||||
const protocol = conf.env === 'production' ? 'https' : req.protocol;
|
|
||||||
const url = `${protocol}://${req.get('host')}/download/${newId}/`;
|
|
||||||
res.json({
|
|
||||||
url,
|
|
||||||
delete: meta.delete,
|
|
||||||
id: newId
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
if (e.message === 'limit') {
|
|
||||||
return res.sendStatus(413);
|
|
||||||
}
|
|
||||||
res.sendStatus(500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
req.on('close', async err => {
|
|
||||||
try {
|
|
||||||
await storage.forceDelete(newId);
|
|
||||||
} catch (e) {
|
|
||||||
log.info('DeleteError:', newId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/__lbheartbeat__', (req, res) => {
|
|
||||||
res.sendStatus(200);
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/__heartbeat__', async (req, res) => {
|
|
||||||
try {
|
|
||||||
await storage.ping();
|
|
||||||
res.sendStatus(200);
|
|
||||||
} catch (e) {
|
|
||||||
res.sendStatus(500);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/__version__', (req, res) => {
|
|
||||||
res.sendFile(path.join(STATIC_PATH, 'version.json'));
|
|
||||||
});
|
|
||||||
|
|
||||||
const server = app.listen(conf.listen_port, () => {
|
|
||||||
log.info('startServer:', `Send app listening on port ${conf.listen_port}!`);
|
|
||||||
});
|
|
||||||
|
|
||||||
const validateID = route_id => {
|
|
||||||
return route_id.match(/^[0-9a-fA-F]{10}$/) !== null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const validateIV = route_id => {
|
|
||||||
return route_id.match(/^[0-9a-fA-F]{24}$/) !== null;
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
server: server,
|
|
||||||
storage: storage
|
|
||||||
};
|
|
20
server/state.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
const config = require('./config');
|
||||||
|
const layout = require('./layout');
|
||||||
|
const locales = require('../common/locales');
|
||||||
|
|
||||||
|
module.exports = function(req) {
|
||||||
|
const locale = req.language || 'en-US';
|
||||||
|
return {
|
||||||
|
locale,
|
||||||
|
translate: locales.getTranslator(locale),
|
||||||
|
title: 'Firefox Send',
|
||||||
|
description:
|
||||||
|
'Encrypt and send files with a link that automatically expires to ensure your important documents don’t stay online forever.',
|
||||||
|
baseUrl: config.base_url,
|
||||||
|
ui: {},
|
||||||
|
storage: {
|
||||||
|
files: []
|
||||||
|
},
|
||||||
|
layout
|
||||||
|
};
|
||||||
|
};
|
@ -1,17 +1,18 @@
|
|||||||
const AWS = require('aws-sdk');
|
const AWS = require('aws-sdk');
|
||||||
const s3 = new AWS.S3();
|
const s3 = new AWS.S3();
|
||||||
|
|
||||||
const conf = require('./config.js');
|
const config = require('./config');
|
||||||
|
const { tmpdir } = require('os');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
const mozlog = require('./log.js');
|
const mozlog = require('./log');
|
||||||
|
|
||||||
const log = mozlog('send.storage');
|
const log = mozlog('send.storage');
|
||||||
|
|
||||||
const redis = require('redis');
|
const redis = require('redis');
|
||||||
const redis_client = redis.createClient({
|
const redis_client = redis.createClient({
|
||||||
host: conf.redis_host,
|
host: config.redis_host,
|
||||||
connect_timeout: 10000
|
connect_timeout: 10000
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -19,7 +20,9 @@ redis_client.on('error', err => {
|
|||||||
log.error('Redis:', err);
|
log.error('Redis:', err);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (conf.s3_bucket) {
|
let tempDir = null;
|
||||||
|
|
||||||
|
if (config.s3_bucket) {
|
||||||
module.exports = {
|
module.exports = {
|
||||||
filename: filename,
|
filename: filename,
|
||||||
exists: exists,
|
exists: exists,
|
||||||
@ -36,6 +39,8 @@ if (conf.s3_bucket) {
|
|||||||
metadata
|
metadata
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
|
tempDir = fs.mkdtempSync(`${tmpdir()}${path.sep}send-`);
|
||||||
|
log.info('tempDir', tempDir);
|
||||||
module.exports = {
|
module.exports = {
|
||||||
filename: filename,
|
filename: filename,
|
||||||
exists: exists,
|
exists: exists,
|
||||||
@ -113,7 +118,7 @@ function setField(id, key, value) {
|
|||||||
function localLength(id) {
|
function localLength(id) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
resolve(fs.statSync(path.join(__dirname, '../static', id)).size);
|
resolve(fs.statSync(path.join(tempDir, id)).size);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
reject();
|
reject();
|
||||||
}
|
}
|
||||||
@ -121,12 +126,12 @@ function localLength(id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function localGet(id) {
|
function localGet(id) {
|
||||||
return fs.createReadStream(path.join(__dirname, '../static', id));
|
return fs.createReadStream(path.join(tempDir, id));
|
||||||
}
|
}
|
||||||
|
|
||||||
function localSet(newId, file, filename, meta) {
|
function localSet(newId, file, filename, meta) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const filepath = path.join(__dirname, '../static', newId);
|
const filepath = path.join(tempDir, newId);
|
||||||
const fstream = fs.createWriteStream(filepath);
|
const fstream = fs.createWriteStream(filepath);
|
||||||
file.pipe(fstream);
|
file.pipe(fstream);
|
||||||
file.on('limit', () => {
|
file.on('limit', () => {
|
||||||
@ -135,7 +140,7 @@ function localSet(newId, file, filename, meta) {
|
|||||||
});
|
});
|
||||||
fstream.on('finish', () => {
|
fstream.on('finish', () => {
|
||||||
redis_client.hmset(newId, meta);
|
redis_client.hmset(newId, meta);
|
||||||
redis_client.expire(newId, conf.expire_seconds);
|
redis_client.expire(newId, config.expire_seconds);
|
||||||
log.info('localSet:', 'Upload Finished of ' + newId);
|
log.info('localSet:', 'Upload Finished of ' + newId);
|
||||||
resolve(meta.delete);
|
resolve(meta.delete);
|
||||||
});
|
});
|
||||||
@ -156,7 +161,7 @@ function localDelete(id, delete_token) {
|
|||||||
} else {
|
} else {
|
||||||
redis_client.del(id);
|
redis_client.del(id);
|
||||||
log.info('Deleted:', id);
|
log.info('Deleted:', id);
|
||||||
resolve(fs.unlinkSync(path.join(__dirname, '../static', id)));
|
resolve(fs.unlinkSync(path.join(tempDir, id)));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -165,7 +170,7 @@ function localDelete(id, delete_token) {
|
|||||||
function localForceDelete(id) {
|
function localForceDelete(id) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
redis_client.del(id);
|
redis_client.del(id);
|
||||||
resolve(fs.unlinkSync(path.join(__dirname, '../static', id)));
|
resolve(fs.unlinkSync(path.join(tempDir, id)));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -179,7 +184,7 @@ function localPing() {
|
|||||||
|
|
||||||
function awsLength(id) {
|
function awsLength(id) {
|
||||||
const params = {
|
const params = {
|
||||||
Bucket: conf.s3_bucket,
|
Bucket: config.s3_bucket,
|
||||||
Key: id
|
Key: id
|
||||||
};
|
};
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@ -195,7 +200,7 @@ function awsLength(id) {
|
|||||||
|
|
||||||
function awsGet(id) {
|
function awsGet(id) {
|
||||||
const params = {
|
const params = {
|
||||||
Bucket: conf.s3_bucket,
|
Bucket: config.s3_bucket,
|
||||||
Key: id
|
Key: id
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -208,7 +213,7 @@ function awsGet(id) {
|
|||||||
|
|
||||||
function awsSet(newId, file, filename, meta) {
|
function awsSet(newId, file, filename, meta) {
|
||||||
const params = {
|
const params = {
|
||||||
Bucket: conf.s3_bucket,
|
Bucket: config.s3_bucket,
|
||||||
Key: newId,
|
Key: newId,
|
||||||
Body: file
|
Body: file
|
||||||
};
|
};
|
||||||
@ -221,7 +226,7 @@ function awsSet(newId, file, filename, meta) {
|
|||||||
return upload.promise().then(
|
return upload.promise().then(
|
||||||
() => {
|
() => {
|
||||||
redis_client.hmset(newId, meta);
|
redis_client.hmset(newId, meta);
|
||||||
redis_client.expire(newId, conf.expire_seconds);
|
redis_client.expire(newId, config.expire_seconds);
|
||||||
},
|
},
|
||||||
err => {
|
err => {
|
||||||
if (hitLimit) {
|
if (hitLimit) {
|
||||||
@ -240,7 +245,7 @@ function awsDelete(id, delete_token) {
|
|||||||
reject();
|
reject();
|
||||||
} else {
|
} else {
|
||||||
const params = {
|
const params = {
|
||||||
Bucket: conf.s3_bucket,
|
Bucket: config.s3_bucket,
|
||||||
Key: id
|
Key: id
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -256,7 +261,7 @@ function awsDelete(id, delete_token) {
|
|||||||
function awsForceDelete(id) {
|
function awsForceDelete(id) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const params = {
|
const params = {
|
||||||
Bucket: conf.s3_bucket,
|
Bucket: config.s3_bucket,
|
||||||
Key: id
|
Key: id
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -269,6 +274,6 @@ function awsForceDelete(id) {
|
|||||||
|
|
||||||
function awsPing() {
|
function awsPing() {
|
||||||
return localPing().then(() =>
|
return localPing().then(() =>
|
||||||
s3.headBucket({ Bucket: conf.s3_bucket }).promise()
|
s3.headBucket({ Bucket: config.s3_bucket }).promise()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1 +0,0 @@
|
|||||||
This is where downloaded files are stored.
|
|
@ -12,11 +12,11 @@ window.Raven = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
window.FakeFile = FakeFile;
|
window.FakeFile = FakeFile;
|
||||||
window.FileSender = require('../../frontend/src/fileSender');
|
window.FileSender = require('../../app/fileSender');
|
||||||
window.FileReceiver = require('../../frontend/src/fileReceiver');
|
window.FileReceiver = require('../../app/fileReceiver');
|
||||||
window.sinon = require('sinon');
|
window.sinon = require('sinon');
|
||||||
window.server = window.sinon.fakeServer.create();
|
window.server = window.sinon.fakeServer.create();
|
||||||
window.assert = require('assert');
|
window.assert = require('assert');
|
||||||
const utils = require('../../frontend/src/utils');
|
const utils = require('../../app/utils');
|
||||||
window.hexToArray = utils.hexToArray;
|
window.hexToArray = utils.hexToArray;
|
||||||
window.arrayToHex = utils.arrayToHex;
|
window.arrayToHex = utils.arrayToHex;
|
||||||
|
@ -46,11 +46,11 @@ const awsStub = {
|
|||||||
const storage = proxyquire('../../server/storage', {
|
const storage = proxyquire('../../server/storage', {
|
||||||
redis: redisStub,
|
redis: redisStub,
|
||||||
fs: fsStub,
|
fs: fsStub,
|
||||||
'./log.js': function() {
|
'./log': function() {
|
||||||
return logStub;
|
return logStub;
|
||||||
},
|
},
|
||||||
'aws-sdk': awsStub,
|
'aws-sdk': awsStub,
|
||||||
'./config.js': {
|
'./config': {
|
||||||
s3_bucket: 'test'
|
s3_bucket: 'test'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -2,8 +2,6 @@ const assert = require('assert');
|
|||||||
const sinon = require('sinon');
|
const sinon = require('sinon');
|
||||||
const proxyquire = require('proxyquire');
|
const proxyquire = require('proxyquire');
|
||||||
|
|
||||||
// const conf = require('../server/config.js');
|
|
||||||
|
|
||||||
const redisStub = {};
|
const redisStub = {};
|
||||||
const exists = sinon.stub();
|
const exists = sinon.stub();
|
||||||
const hget = sinon.stub();
|
const hget = sinon.stub();
|
||||||
@ -35,7 +33,7 @@ logStub.error = sinon.stub();
|
|||||||
const storage = proxyquire('../../server/storage', {
|
const storage = proxyquire('../../server/storage', {
|
||||||
redis: redisStub,
|
redis: redisStub,
|
||||||
fs: fsStub,
|
fs: fsStub,
|
||||||
'./log.js': function() {
|
'./log': function() {
|
||||||
return logStub;
|
return logStub;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -1,28 +0,0 @@
|
|||||||
{{!-- This file should be es5 only --}}
|
|
||||||
var isIE = /trident\/7\.|msie/i.test(navigator.userAgent);
|
|
||||||
var isUnsupportedPage = /\/unsupported/.test(location.pathname);
|
|
||||||
if (isIE && !isUnsupportedPage) {
|
|
||||||
window.location.replace('/unsupported/ie');
|
|
||||||
}
|
|
||||||
|
|
||||||
{{#if sentryId}}
|
|
||||||
var RAVEN_CONFIG = {
|
|
||||||
release: '{{{version}}}',
|
|
||||||
tags: {
|
|
||||||
commit: '{{{commit}}}'
|
|
||||||
},
|
|
||||||
dataCallback: function (data) {
|
|
||||||
var hash = window.location.hash;
|
|
||||||
if (hash) {
|
|
||||||
return JSON.parse(JSON.stringify(data).replace(new RegExp(hash.slice(1), 'g'), ''));
|
|
||||||
}
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var SENTRY_ID = '{{{sentryId}}}';
|
|
||||||
{{/if}}
|
|
||||||
{{#if googleAnalyticsId}}
|
|
||||||
var GOOGLE_ANALYTICS_ID = '{{{googleAnalyticsId}}}';
|
|
||||||
{{/if}}
|
|
||||||
var MAXFILESIZE = {{{maxFileSize}}};
|
|
||||||
var EXPIRE_SECONDS = {{{expireSeconds}}};
|
|
@ -1,17 +1,16 @@
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const webpack = require('webpack');
|
const webpack = require('webpack');
|
||||||
const HtmlPlugin = require('html-webpack-plugin');
|
|
||||||
const CopyPlugin = require('copy-webpack-plugin');
|
const CopyPlugin = require('copy-webpack-plugin');
|
||||||
|
const ManifestPlugin = require('webpack-manifest-plugin');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
entry: {
|
entry: {
|
||||||
vendor: ['babel-polyfill', 'raven-js'],
|
vendor: ['babel-polyfill', 'raven-js', 'fluent', 'choo'],
|
||||||
upload: ['./frontend/src/upload.js'],
|
app: ['./app/main.js']
|
||||||
download: ['./frontend/src/download.js']
|
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
filename: 'resources/[name].[chunkhash].js',
|
filename: '[name].[chunkhash:8].js',
|
||||||
path: path.resolve(__dirname, 'dist/public'),
|
path: path.resolve(__dirname, 'dist'),
|
||||||
publicPath: '/'
|
publicPath: '/'
|
||||||
},
|
},
|
||||||
module: {
|
module: {
|
||||||
@ -20,19 +19,48 @@ module.exports = {
|
|||||||
test: /\.js$/,
|
test: /\.js$/,
|
||||||
loader: 'babel-loader',
|
loader: 'babel-loader',
|
||||||
include: [
|
include: [
|
||||||
path.resolve(__dirname, 'frontend'),
|
path.resolve(__dirname, 'app'),
|
||||||
|
path.resolve(__dirname, 'common'),
|
||||||
path.resolve(__dirname, 'node_modules/testpilot-ga/src')
|
path.resolve(__dirname, 'node_modules/testpilot-ga/src')
|
||||||
],
|
],
|
||||||
options: {
|
options: {
|
||||||
babelrc: false,
|
babelrc: false,
|
||||||
presets: [['es2015', { modules: false }], 'stage-2']
|
presets: [['env', { modules: false }], 'stage-2'],
|
||||||
|
plugins: ['yo-yoify']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
test: /\.js$/,
|
||||||
|
include: [path.dirname(require.resolve('fluent'))],
|
||||||
|
use: [
|
||||||
|
{
|
||||||
|
loader: 'expose-loader',
|
||||||
|
options: 'fluent'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
loader: 'babel-loader',
|
||||||
|
options: {
|
||||||
|
presets: [['env', { modules: false }]]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: require.resolve('./assets/cryptofill'),
|
||||||
|
use: [
|
||||||
|
{
|
||||||
|
loader: 'file-loader',
|
||||||
|
options: {
|
||||||
|
name: '[name].[hash:8].[ext]'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
test: /\.(svg|png|jpg)$/,
|
test: /\.(svg|png|jpg)$/,
|
||||||
loader: 'file-loader',
|
loader: 'file-loader',
|
||||||
options: {
|
options: {
|
||||||
name: 'resources/[name].[hash].[ext]'
|
name: '[name].[hash:8].[ext]'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -41,7 +69,7 @@ module.exports = {
|
|||||||
{
|
{
|
||||||
loader: 'file-loader',
|
loader: 'file-loader',
|
||||||
options: {
|
options: {
|
||||||
name: 'resources/[name].[hash].[ext]'
|
name: '[name].[hash:8].[ext]'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'extract-loader',
|
'extract-loader',
|
||||||
@ -50,75 +78,64 @@ module.exports = {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.hbs$/,
|
test: require.resolve('./package.json'),
|
||||||
use: [
|
use: [
|
||||||
{
|
{
|
||||||
loader: 'html-loader',
|
loader: 'file-loader',
|
||||||
options: {
|
options: {
|
||||||
interpolate: 'require',
|
name: 'version.json'
|
||||||
minimize: false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
'extract-loader',
|
||||||
|
'./build/package_json_loader'
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.ftl$/,
|
||||||
|
use: [
|
||||||
|
{
|
||||||
|
loader: 'file-loader',
|
||||||
|
options: {
|
||||||
|
name: '[path][name].[hash:8].js'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'extract-loader',
|
||||||
|
'./build/fluent_loader'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: require.resolve('./build/generate_asset_map.js'),
|
||||||
|
use: ['babel-loader', 'val-loader']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: require.resolve('./build/generate_l10n_map.js'),
|
||||||
|
use: ['babel-loader', 'val-loader']
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
new CopyPlugin([
|
new CopyPlugin([
|
||||||
{
|
|
||||||
context: 'public',
|
|
||||||
from: 'locales/**/*.ftl'
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
context: 'public',
|
context: 'public',
|
||||||
from: '*.*'
|
from: '*.*'
|
||||||
},
|
|
||||||
{
|
|
||||||
from: 'views/**',
|
|
||||||
to: '../'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
context: 'node_modules/l20n/dist/web',
|
|
||||||
from: 'l20n.min.js'
|
|
||||||
}
|
}
|
||||||
]),
|
]),
|
||||||
new HtmlPlugin({
|
new webpack.IgnorePlugin(/dist/),
|
||||||
filename: '../views/index.handlebars',
|
new webpack.IgnorePlugin(/require-from-string/),
|
||||||
template: 'webpack/upload.hbs',
|
|
||||||
chunks: ['upload']
|
|
||||||
}),
|
|
||||||
new HtmlPlugin({
|
|
||||||
filename: '../views/download.handlebars',
|
|
||||||
template: 'webpack/download.hbs',
|
|
||||||
chunks: ['download']
|
|
||||||
}),
|
|
||||||
new HtmlPlugin({
|
|
||||||
filename: '../views/legal.handlebars',
|
|
||||||
template: 'webpack/legal.hbs',
|
|
||||||
inject: false
|
|
||||||
}),
|
|
||||||
new HtmlPlugin({
|
|
||||||
filename: '../views/notfound.handlebars',
|
|
||||||
template: 'webpack/notfound.hbs',
|
|
||||||
inject: false
|
|
||||||
}),
|
|
||||||
new HtmlPlugin({
|
|
||||||
filename: '../views/layouts/main.handlebars',
|
|
||||||
template: 'webpack/layout.hbs',
|
|
||||||
inject: 'head',
|
|
||||||
excludeChunks: ['upload', 'download']
|
|
||||||
}),
|
|
||||||
new HtmlPlugin({
|
|
||||||
filename: '../views/unsupported.handlebars',
|
|
||||||
template: 'webpack/unsupported.hbs',
|
|
||||||
inject: false
|
|
||||||
}),
|
|
||||||
new webpack.HashedModuleIdsPlugin(),
|
new webpack.HashedModuleIdsPlugin(),
|
||||||
new webpack.optimize.CommonsChunkPlugin({
|
new webpack.optimize.CommonsChunkPlugin({
|
||||||
name: 'vendor'
|
name: 'vendor'
|
||||||
}),
|
}),
|
||||||
new webpack.optimize.CommonsChunkPlugin({
|
new webpack.optimize.CommonsChunkPlugin({
|
||||||
name: 'runtime'
|
name: 'runtime'
|
||||||
})
|
}),
|
||||||
]
|
new ManifestPlugin()
|
||||||
|
],
|
||||||
|
devServer: {
|
||||||
|
compress: true,
|
||||||
|
setup:
|
||||||
|
process.env.NODE_ENV === 'development'
|
||||||
|
? require('./server/dev')
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
@ -1,43 +0,0 @@
|
|||||||
<div id="download">
|
|
||||||
<div id="download-page-one">
|
|
||||||
<div class="title">
|
|
||||||
<span id="dl-file"
|
|
||||||
data-filename="{{filename}}"
|
|
||||||
data-size="{{sizeInBytes}}"
|
|
||||||
data-ttl="{{ttl}}"
|
|
||||||
data-l10n-id="downloadFileName"
|
|
||||||
data-l10n-args='{{filenameJson}}'></span>
|
|
||||||
<span id="dl-filesize"></span>
|
|
||||||
</div>
|
|
||||||
<div class="description" data-l10n-id="downloadMessage"></div>
|
|
||||||
<img src="../public/resources/illustration_download.svg" id="download-img" data-l10n-id="downloadAltText"/>
|
|
||||||
<div>
|
|
||||||
<button id="download-btn" class="btn" data-l10n-id="downloadButtonLabel"></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="download-progress" hidden="true">
|
|
||||||
<div id="dl-title" class="title"></div>
|
|
||||||
<div class="description" data-l10n-id="downloadingPageMessage"></div>
|
|
||||||
<div class="progress-bar">
|
|
||||||
<svg id="progress" width="166" height="166" viewPort="0 0 166 166" version="1.1">
|
|
||||||
<circle r="73" cx="83" cy="83" fill="transparent"/>
|
|
||||||
<circle id="bar" r="73" cx="83" cy="83" fill="transparent" transform="rotate(-90 83 83)" stroke-dasharray="458.67" stroke-dashoffset="458.67"/>
|
|
||||||
</svg>
|
|
||||||
<div class="percentage">
|
|
||||||
<span class="percent-number"></span>
|
|
||||||
<span class="percent-sign">%</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="upload">
|
|
||||||
<div class="progress-text">{{filename}}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="upload-error" hidden="true">
|
|
||||||
<div class="title" data-l10n-id="errorPageHeader"></div>
|
|
||||||
<img id="upload-error-img" data-l10n-id="errorAltText" src="../public/resources/illustration_error.svg"/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<a class="send-new" data-state="completed" data-l10n-id="sendYourFilesLink" href="/"></a>
|
|
||||||
</div>
|
|
@ -1,69 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<meta name="defaultLanguage" content="en-US">
|
|
||||||
<meta name="availableLanguages" content="{{availableLanguages}}">
|
|
||||||
|
|
||||||
<meta property="og:title" content="{{title}}"/>
|
|
||||||
<meta name="twitter:title" content="{{title}}"/>
|
|
||||||
<meta name="description" content="{{description}}"/>
|
|
||||||
<meta property="og:description" content="{{description}}"/>
|
|
||||||
<meta name="twitter:description" content="{{description}}"/>
|
|
||||||
<meta name="twitter:card" content="summary"/>
|
|
||||||
<meta property="og:image" content="{{baseUrl}}${require('../public/resources/send-fb.jpg')}"/>
|
|
||||||
<meta name="twitter:image" content="{{baseUrl}}${require('../public/resources/send-twitter.jpg')}"/>
|
|
||||||
<meta property="og:url" content="{{baseUrl}}"/>
|
|
||||||
|
|
||||||
<title>{{title}}</title>
|
|
||||||
|
|
||||||
<link rel="stylesheet" type="text/css" href="${require('../frontend/src/main.css')}" />
|
|
||||||
{{#if fira}}
|
|
||||||
<link rel="stylesheet" type="text/css" href="https://code.cdn.mozilla.net/fonts/fira.css" />
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
<link rel="icon" type="image/png" href="${require('../public/resources/favicon-32x32.png')}" sizes="32x32" />
|
|
||||||
<link rel="localization" href="/locales/{locale}/send.ftl">
|
|
||||||
|
|
||||||
<script src="/jsconfig.js"></script>
|
|
||||||
<script defer src="/l20n.min.js"></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header class="header">
|
|
||||||
<div class="send-logo">
|
|
||||||
<a href="/">
|
|
||||||
<img src="../public/resources/send_logo.svg" alt="Send"/><h1 class="site-title">Send</h1>
|
|
||||||
</a>
|
|
||||||
<div class="site-subtitle">
|
|
||||||
<a href="https://testpilot.firefox.com">Firefox Test Pilot</a>
|
|
||||||
<div data-l10n-id="siteSubtitle">web experiment</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<a href="https://qsurvey.mozilla.com/s3/txp-firefox-send" rel="noreferrer noopener" class="feedback" target="_blank" data-l10n-id="siteFeedback">Feedback</a>
|
|
||||||
</header>
|
|
||||||
<div class="all">
|
|
||||||
<noscript>
|
|
||||||
<h2>Firefox Send requires JavaScript</h2>
|
|
||||||
<p><a href="https://github.com/mozilla/send/blob/master/docs/faq.md#why-does-firefox-send-require-javascript">Why does Firefox Send require JavaScript?</a></p>
|
|
||||||
<p>Please enable JavaScript and try again.</p>
|
|
||||||
</noscript>
|
|
||||||
{{{body}}}
|
|
||||||
</div>
|
|
||||||
<div class="footer">
|
|
||||||
<div class="legal-links">
|
|
||||||
<a href="https://www.mozilla.org" role="presentation"><img class="mozilla-logo" src="../public/resources/mozilla-logo.svg" alt="mozilla"/></a>
|
|
||||||
<a href="https://www.mozilla.org/about/legal" data-l10n-id="footerLinkLegal">Legal</a>
|
|
||||||
<a href="https://testpilot.firefox.com/about" data-l10n-id="footerLinkAbout">About Test Pilot</a>
|
|
||||||
<a href="/legal" data-l10n-id="footerLinkPrivacy">Privacy</a>
|
|
||||||
<a href="/legal" data-l10n-id="footerLinkTerms">Terms</a>
|
|
||||||
<a href="https://www.mozilla.org/privacy/websites/#cookies" data-l10n-id="footerLinkCookies">Cookies</a>
|
|
||||||
</div>
|
|
||||||
<div class="social-links">
|
|
||||||
<a href="https://github.com/mozilla/send" role="presentation"><img class="github" src="../public/resources/github-icon.svg" alt="github"/></a>
|
|
||||||
<a href="https://twitter.com/FxTestPilot" role="presentation"><img class="twitter" src="../public/resources/twitter-icon.svg" alt="twitter"/></a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,12 +0,0 @@
|
|||||||
<div id="legal">
|
|
||||||
<div class="title" data-l10n-id="legalHeader"></div>
|
|
||||||
<div class="description" data-l10n-id="legalNoticeTestPilot">
|
|
||||||
<a href="https://testpilot.firefox.com/terms"></a>
|
|
||||||
<a href="https://testpilot.firefox.com/privacy"></a>
|
|
||||||
<a href="https://testpilot.firefox.com/experiments/send"></a>
|
|
||||||
</div>
|
|
||||||
<div class="description" data-l10n-id="legalNoticeMozilla">
|
|
||||||
<a href="https://www.mozilla.org/privacy/websites/"></a>
|
|
||||||
<a href="https://www.mozilla.org/about/legal/terms/mozilla/"></a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
@ -1,8 +0,0 @@
|
|||||||
<div id="download">
|
|
||||||
<div class="title" data-l10n-id="expiredPageHeader"></div>
|
|
||||||
<div class="share-window">
|
|
||||||
<img src="../public/resources/illustration_expired.svg" id="expired-img" data-l10n-id="linkExpiredAlt"/>
|
|
||||||
</div>
|
|
||||||
<div class="expired-description" data-l10n-id="uploadPageExplainer"></div>
|
|
||||||
<a class="send-new" href="/" data-state="notfound" data-l10n-id="sendYourFilesLink"></a>
|
|
||||||
</div>
|
|
@ -1,20 +0,0 @@
|
|||||||
<div id="unsupported-browser">
|
|
||||||
<div class="title" data-l10n-id="notSupportedHeader">Your browser is not supported.</div>
|
|
||||||
{{#if outdated}}
|
|
||||||
<div class="description" data-l10n-id="notSupportedOutdatedDetail">Unfortunately this version of Firefox does not support the web technology that powers Firefox Send. You’ll need to update your browser.</div>
|
|
||||||
<a id="update-firefox" href="https://support.mozilla.org/kb/update-firefox-latest-version">
|
|
||||||
<img src="../public/resources/firefox_logo-only.svg" class="firefox-logo" alt="Firefox"/>
|
|
||||||
<div class="unsupported-button-text" data-l10n-id="updateFirefox">Update Firefox</div>
|
|
||||||
</a>
|
|
||||||
{{else}}
|
|
||||||
<div class="description" data-l10n-id="notSupportedDetail">Unfortunately this browser does not support the web technology that powers Firefox Send. You’ll need to try another browser. We recommend Firefox!</div>
|
|
||||||
<div class="description"><a href="https://github.com/mozilla/send/blob/master/docs/faq.md#why-is-my-browser-not-supported" data-l10n-id="notSupportedLink">Why is my browser not supported?</a></div>
|
|
||||||
<a id="dl-firefox" href="https://www.mozilla.org/firefox/new/?scene=2">
|
|
||||||
<img src="../public/resources/firefox_logo-only.svg" class="firefox-logo" alt="Firefox"/>
|
|
||||||
<div class="unsupported-button-text">Firefox<br>
|
|
||||||
<span data-l10n-id="downloadFirefoxButtonSub">Free Download</span>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
{{/if}}
|
|
||||||
<div class="unsupported-description" data-l10n-id="uploadPageExplainer">Send files through a safe, private, and encrypted link that automatically expires to ensure your stuff does not remain online forever.</div>
|
|
||||||
</div>
|
|
@ -1,75 +0,0 @@
|
|||||||
<div id="page-one" hidden>
|
|
||||||
<div class="title" data-l10n-id="uploadPageHeader"></div>
|
|
||||||
<div class="description">
|
|
||||||
<div data-l10n-id="uploadPageExplainer"></div>
|
|
||||||
<a href="https://testpilot.firefox.com/experiments/send" class="link" data-l10n-id="uploadPageLearnMore"></a>
|
|
||||||
</div>
|
|
||||||
<div class="upload-window" >
|
|
||||||
<div id="upload-img"><img data-l10n-id="uploadSvgAlt" src="../public/resources/upload.svg"/></div>
|
|
||||||
<div id="upload-text" data-l10n-id="uploadPageDropMessage"></div>
|
|
||||||
<span id="file-size-msg"><em data-l10n-id="uploadPageSizeMessage"></em></span>
|
|
||||||
<form method="post" action="upload" enctype="multipart/form-data">
|
|
||||||
<label for="file-upload" id="browse"
|
|
||||||
data-l10n-id="uploadPageBrowseButton1" class="btn"></label>
|
|
||||||
<input id="file-upload" type="file" name="fileUploaded" />
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="file-list">
|
|
||||||
<table id="uploaded-files">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<!-- htmllint attr-bans="false" -->
|
|
||||||
<th id="uploaded-file" data-l10n-id="uploadedFile"></th>
|
|
||||||
<th id="copy-file-list" data-l10n-id="copyFileList"></th>
|
|
||||||
<th id="expiry-file-list" data-l10n-id="expiryFileList"></th>
|
|
||||||
<th id="delete-file-list" data-l10n-id="deleteFileList"></th>
|
|
||||||
<!-- htmllint tag-bans="$previous" -->
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="upload-progress" hidden="true">
|
|
||||||
<div class="title" id="upload-filename"></div>
|
|
||||||
<div class="description"></div>
|
|
||||||
<div class="progress-bar">
|
|
||||||
<svg id="progress" width="166" height="166" viewPort="0 0 166 166" version="1.1">
|
|
||||||
<circle r="73" cx="83" cy="83" fill="transparent"/>
|
|
||||||
<circle id="bar" r="73" cx="83" cy="83" fill="transparent" transform="rotate(-90 83 83)" stroke-dasharray="458.67" stroke-dashoffset="458.67"/>
|
|
||||||
</svg>
|
|
||||||
<div class="percentage">
|
|
||||||
<span class="percent-number">0</span>
|
|
||||||
<span class="percent-sign">%</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="upload">
|
|
||||||
<div class="progress-text"></div>
|
|
||||||
<div id="cancel-upload"
|
|
||||||
data-l10n-id="uploadingPageCancel"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="share-link" hidden="true">
|
|
||||||
<div class="title" data-l10n-id="uploadSuccessTimingHeader"></div>
|
|
||||||
<div id="share-window">
|
|
||||||
<div id="copy-text"></div>
|
|
||||||
<div id="copy">
|
|
||||||
<input id="link" type="url" value="" readonly/>
|
|
||||||
<button id="copy-btn" class="btn" data-l10n-id="copyUrlFormButton"></button>
|
|
||||||
</div>
|
|
||||||
<button id="delete-file" class="btn" data-l10n-id="deleteFileButton"></button>
|
|
||||||
<a class="send-new" data-state="completed" data-l10n-id="sendAnotherFileLink" href="/"></a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="upload-error" hidden="true">
|
|
||||||
<div class="title" data-l10n-id="errorPageHeader"></div>
|
|
||||||
<div class="expired-description" data-l10n-id="errorPageMessage"></div>
|
|
||||||
<img id="upload-error-img" data-l10n-id="errorAltText" src="../public/resources/illustration_error.svg"/>
|
|
||||||
<a class="send-new" href="/" data-state="errored" data-l10n-id="sendAnotherFileLink"></a>
|
|
||||||
</div>
|
|