Skip to content

Commit

Permalink
Added multiple download option
Browse files Browse the repository at this point in the history
  • Loading branch information
dannycoates committed Dec 4, 2017
1 parent beb3a6e commit 7b4060f
Show file tree
Hide file tree
Showing 22 changed files with 1,159 additions and 453 deletions.
9 changes: 8 additions & 1 deletion app/fileManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,13 @@ export default function(state, emitter) {
lastRender = Date.now();
});

emitter.on('changeLimit', async ({ file, value }) => {
await FileSender.changeLimit(file.id, file.ownerToken, value);
file.dlimit = value;
state.storage.writeFiles();
metrics.changedDownloadLimit(file);
});

emitter.on('delete', async ({ file, location }) => {
try {
metrics.deletedUpload({
Expand All @@ -108,7 +115,7 @@ export default function(state, emitter) {
location
});
state.storage.remove(file.id);
await FileSender.delete(file.id, file.deleteToken);
await FileSender.delete(file.id, file.ownerToken);
} catch (e) {
state.raven.captureException(e);
}
Expand Down
62 changes: 38 additions & 24 deletions app/fileReceiver.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,8 @@ export default class FileReceiver extends Nanobus {
// TODO
}

fetchMetadata(sig) {
async fetchMetadata(nonce) {
const authHeader = await this.getAuthHeader(nonce);
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = () => {
Expand All @@ -132,24 +133,24 @@ export default class FileReceiver extends Nanobus {
xhr.onerror = () => reject(new Error(0));
xhr.ontimeout = () => reject(new Error(0));
xhr.open('get', `/api/metadata/${this.file.id}`);
xhr.setRequestHeader('Authorization', `send-v1 ${arrayToB64(sig)}`);
xhr.setRequestHeader('Authorization', authHeader);
xhr.responseType = 'json';
xhr.timeout = 2000;
xhr.send();
});
}

async getMetadata(nonce) {
let data = null;
try {
const authKey = await this.authKeyPromise;
const sig = await window.crypto.subtle.sign(
{
name: 'HMAC'
},
authKey,
b64ToArray(nonce)
);
const data = await this.fetchMetadata(new Uint8Array(sig));
try {
data = await this.fetchMetadata(nonce);
} catch (e) {
if (e.message === '401') {
// allow one retry for changed nonce
data = await this.fetchMetadata(e.nonce);
}
}
const metaKey = await this.metaKeyPromise;
const json = await window.crypto.subtle.decrypt(
{
Expand All @@ -174,7 +175,8 @@ export default class FileReceiver extends Nanobus {
}
}

downloadFile(sig) {
async downloadFile(nonce) {
const authHeader = await this.getAuthHeader(nonce);
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();

Expand All @@ -190,9 +192,10 @@ export default class FileReceiver extends Nanobus {
reject(new Error('notfound'));
return;
}

if (xhr.status !== 200) {
return reject(new Error(xhr.status));
const err = new Error(xhr.status);
err.nonce = xhr.getResponseHeader('WWW-Authenticate').split(' ')[1];
return reject(err);
}

const blob = new Blob([xhr.response]);
Expand All @@ -205,26 +208,37 @@ export default class FileReceiver extends Nanobus {
};

xhr.open('get', this.url);
xhr.setRequestHeader('Authorization', `send-v1 ${arrayToB64(sig)}`);
xhr.setRequestHeader('Authorization', authHeader);
xhr.responseType = 'blob';
xhr.send();
});
}

async getAuthHeader(nonce) {
const authKey = await this.authKeyPromise;
const sig = await window.crypto.subtle.sign(
{
name: 'HMAC'
},
authKey,
b64ToArray(nonce)
);
return `send-v1 ${arrayToB64(new Uint8Array(sig))}`;
}

async download(nonce) {
this.state = 'downloading';
this.emit('progress', this.progress);
try {
const encryptKey = await this.encryptKeyPromise;
const authKey = await this.authKeyPromise;
const sig = await window.crypto.subtle.sign(
{
name: 'HMAC'
},
authKey,
b64ToArray(nonce)
);
const ciphertext = await this.downloadFile(new Uint8Array(sig));
let ciphertext = null;
try {
ciphertext = await this.downloadFile(nonce);
} catch (e) {
if (e.message === '401') {
ciphertext = await this.downloadFile(e.nonce);
}
}
this.msg = 'decryptingFile';
this.emit('decrypting');
const plaintext = await window.crypto.subtle.decrypt(
Expand Down
47 changes: 34 additions & 13 deletions app/fileSender.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,26 @@ export default class FileSender extends Nanobus {
}
};

xhr.send(JSON.stringify({ delete_token: token }));
xhr.send(JSON.stringify({ owner_token: token }));
});
}

static changeLimit(id, owner_token, dlimit) {
return new Promise((resolve, reject) => {
if (!id || !owner_token) {
return reject();
}
const xhr = new XMLHttpRequest();
xhr.open('POST', `/api/params/${id}`);
xhr.setRequestHeader('Content-Type', 'application/json');

xhr.onreadystatechange = () => {
if (xhr.readyState === XMLHttpRequest.DONE) {
resolve();
}
};

xhr.send(JSON.stringify({ owner_token, dlimit }));
});
}

Expand Down Expand Up @@ -100,7 +119,7 @@ export default class FileSender extends Nanobus {
url: responseObj.url,
id: responseObj.id,
secretKey: arrayToB64(this.rawSecret),
deleteToken: responseObj.delete,
ownerToken: responseObj.owner,
nonce
});
}
Expand Down Expand Up @@ -205,6 +224,17 @@ export default class FileSender extends Nanobus {
return this.uploadFile(encrypted, metadata, new Uint8Array(rawAuth));
}

async getAuthHeader(authKey, nonce) {
const sig = await window.crypto.subtle.sign(
{
name: 'HMAC'
},
authKey,
b64ToArray(nonce)
);
return `send-v1 ${arrayToB64(new Uint8Array(sig))}`;
}

static async setPassword(password, file) {
const encoder = new TextEncoder();
const secretKey = await window.crypto.subtle.importKey(
Expand All @@ -229,13 +259,7 @@ export default class FileSender extends Nanobus {
true,
['sign']
);
const sig = await window.crypto.subtle.sign(
{
name: 'HMAC'
},
authKey,
b64ToArray(file.nonce)
);
const authHeader = await this.getAuthHeader(authKey, file.nonce);
const pwdKey = await window.crypto.subtle.importKey(
'raw',
encoder.encode(password),
Expand Down Expand Up @@ -278,10 +302,7 @@ export default class FileSender extends Nanobus {
xhr.onerror = () => reject(new Error(0));
xhr.ontimeout = () => reject(new Error(0));
xhr.open('post', `/api/password/${file.id}`);
xhr.setRequestHeader(
'Authorization',
`send-v1 ${arrayToB64(new Uint8Array(sig))}`
);
xhr.setRequestHeader('Authorization', authHeader);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.responseType = 'json';
xhr.timeout = 2000;
Expand Down
1 change: 1 addition & 0 deletions app/main.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'fluent-intl-polyfill';
import app from './routes';
import locale from '../common/locales';
import fileManager from './fileManager';
Expand Down
11 changes: 11 additions & 0 deletions app/metrics.js
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,16 @@ function stoppedUpload(params) {
});
}

function changedDownloadLimit(params) {
return sendEvent('sender', 'download-limit-changed', {
cm1: params.size,
cm5: storage.totalUploads,
cm6: storage.files.length,
cm7: storage.totalDownloads,
cm8: params.dlimit
});
}

function completedDownload(params) {
return sendEvent('recipient', 'download-stopped', {
cm1: params.size,
Expand Down Expand Up @@ -272,6 +282,7 @@ export {
cancelledUpload,
stoppedUpload,
completedUpload,
changedDownloadLimit,
deletedUpload,
startedDownload,
cancelledDownload,
Expand Down
4 changes: 3 additions & 1 deletion app/templates/file.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ module.exports = function(file, state, emit) {
const remaining = timeLeft(ttl) || state.translate('linkExpiredAlt');
const row = html`
<tr id="${file.id}">
<td class="overflow-col" title="${file.name}">${file.name}</td>
<td class="overflow-col" title="${
file.name
}"><a class="link" href="/share/${file.id}">${file.name}</a></td>
<td class="center-col">
<img onclick=${copyClick} src="${assets.get(
'copy-16.svg'
Expand Down
56 changes: 56 additions & 0 deletions app/templates/selectbox.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
const html = require('choo/html');

module.exports = function(selected, options, translate, changed) {
const id = `select-${Math.random()}`;
let x = selected;

function close() {
const ul = document.getElementById(id);
const body = document.querySelector('body');
ul.classList.remove('active');
body.removeEventListener('click', close);
}

function toggle(event) {
event.stopPropagation();
const ul = document.getElementById(id);
if (ul.classList.contains('active')) {
close();
} else {
ul.classList.add('active');
const body = document.querySelector('body');
body.addEventListener('click', close);
}
}

function choose(event) {
event.stopPropagation();
const target = event.target;
const value = +target.dataset.value;
target.parentNode.previousSibling.firstElementChild.textContent = translate(
value
);
if (x !== value) {
x = value;
changed(value);
}
close();
}
return html`
<div class="selectbox">
<div onclick=${toggle}>
<span class="link">${translate(selected)}</span>
<svg width="32" height="32">
<polygon points="8 18 17 28 26 18" fill="#0094fb"/>
</svg>
</div>
<ul id="${id}" class="selectOptions">
${options.map(
i =>
html`<li class="selectOption" onclick=${choose} data-value="${i}">${
i
}</li>`
)}
</ul>
</div>`;
};
21 changes: 20 additions & 1 deletion app/templates/share.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const html = require('choo/html');
const assets = require('../../common/assets');
const notFound = require('./notFound');
const uploadPassword = require('./uploadPassword');
const selectbox = require('./selectbox');
const { allowedCopy, delay, fadeOut } = require('../utils');

function passwordComplete(state, password) {
Expand All @@ -14,6 +15,24 @@ function passwordComplete(state, password) {
return el;
}

function expireInfo(file, translate, emit) {
const el = html([
`<div>${translate('expireInfo', {
downloadCount: '<select></select>',
timespan: translate('timespanHours', { number: 24 })
})}</div>`
]);
const select = el.querySelector('select');
const options = [1, 2, 3, 4, 5, 20];
const t = number => translate('downloadCount', { number });
const changed = value => emit('changeLimit', { file, value });
select.parentNode.replaceChild(
selectbox(file.dlimit || 1, options, t, changed),
select
);
return el;
}

module.exports = function(state, emit) {
const file = state.storage.getFileById(state.params.id);
if (!file) {
Expand All @@ -27,7 +46,7 @@ module.exports = function(state, emit) {
: uploadPassword(state, emit);
const div = html`
<div id="share-link" class="fadeIn">
<div class="title">${state.translate('uploadSuccessTimingHeader')}</div>
<div class="title">${expireInfo(file, state.translate, emit)}</div>
<div id="share-window">
<div id="copy-text">
${state.translate('copyUrlFormLabelWithName', {
Expand Down
Loading

0 comments on commit 7b4060f

Please sign in to comment.