Added multiple download option

This commit is contained in:
Danny Coates 2017-11-30 13:41:09 -08:00
parent beb3a6e67b
commit 7b4060f9e1
No known key found for this signature in database
GPG key ID: 4C442633C62E00CB
22 changed files with 1159 additions and 453 deletions

View file

@ -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({
@ -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);
}

View file

@ -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 = () => {
@ -132,7 +133,7 @@ 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();
@ -140,16 +141,16 @@ export default class FileReceiver extends Nanobus {
}
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(
{
@ -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();
@ -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]);
@ -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(

View file

@ -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 }));
});
}
@ -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
});
}
@ -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(
@ -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),
@ -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;

View file

@ -1,3 +1,4 @@
import 'fluent-intl-polyfill';
import app from './routes';
import locale from '../common/locales';
import fileManager from './fileManager';

View file

@ -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,
@ -272,6 +282,7 @@ export {
cancelledUpload,
stoppedUpload,
completedUpload,
changedDownloadLimit,
deletedUpload,
startedDownload,
cancelledDownload,

View file

@ -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'

View file

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

View file

@ -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) {
@ -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) {
@ -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', {