From bf16e5c8a9078400eb172aaa19cd359b9492e86a Mon Sep 17 00:00:00 2001 From: Emily Date: Wed, 8 Aug 2018 11:07:09 -0700 Subject: [PATCH 1/3] integrate with new ui --- app/api.js | 30 ++- app/base.css | 7 +- app/fileManager.js | 7 +- app/fileSender.js | 24 +- app/ownedFile.js | 4 +- app/pages/share/index.js | 18 +- app/pages/welcome/index.js | 1 + app/templates/expireInfo/index.js | 14 +- app/templates/file/index.js | 8 + app/templates/fileIcon/fileIcon.css | 1 + app/templates/signupPromo/signupPromo.css | 2 +- app/templates/timeLimitText/index.js | 14 ++ .../uploadedFileList/uploadedFileList.css | 1 + package-lock.json | 232 +----------------- package.json | 2 +- public/locales/en-US/send.ftl | 2 + server/config.js | 33 ++- server/routes/download.js | 3 +- server/routes/jsconfig.js | 2 +- server/routes/upload.js | 4 +- server/routes/ws.js | 10 +- server/storage/fs.js | 2 +- server/storage/index.js | 51 +++- server/storage/s3.js | 4 +- test/backend/s3-tests.js | 20 +- test/backend/storage-tests.js | 51 ++-- test/frontend/tests/api-tests.js | 18 +- 27 files changed, 250 insertions(+), 315 deletions(-) create mode 100644 app/templates/timeLimitText/index.js diff --git a/app/api.js b/app/api.js index 79a7785e..a816e63c 100644 --- a/app/api.js +++ b/app/api.js @@ -136,7 +136,14 @@ function listenForResponse(ws, canceller) { }); } -async function upload(stream, metadata, verifierB64, onprogress, canceller) { +async function upload( + stream, + metadata, + verifierB64, + timeLimit, + onprogress, + canceller +) { const host = window.location.hostname; const port = window.location.port; const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; @@ -151,7 +158,8 @@ async function upload(stream, metadata, verifierB64, onprogress, canceller) { const metadataHeader = arrayToB64(new Uint8Array(metadata)); const fileMeta = { fileMetadata: metadataHeader, - authorization: `send-v1 ${verifierB64}` + authorization: `send-v1 ${verifierB64}`, + timeLimit }; const responsePromise = listenForResponse(ws, canceller); @@ -188,7 +196,13 @@ async function upload(stream, metadata, verifierB64, onprogress, canceller) { } } -export function uploadWs(encrypted, metadata, verifierB64, onprogress) { +export function uploadWs( + encrypted, + metadata, + verifierB64, + onprogress, + timeLimit +) { const canceller = { cancelled: false }; return { @@ -196,7 +210,15 @@ export function uploadWs(encrypted, metadata, verifierB64, onprogress) { canceller.error = new Error(0); canceller.cancelled = true; }, - result: upload(encrypted, metadata, verifierB64, onprogress, canceller) + + result: upload( + encrypted, + metadata, + verifierB64, + timeLimit, + onprogress, + canceller + ) }; } diff --git a/app/base.css b/app/base.css index f30d5c63..c922a34e 100644 --- a/app/base.css +++ b/app/base.css @@ -276,7 +276,6 @@ a { } .stripedBox { - margin-top: 72; min-height: 400px; flex: 1; } @@ -289,3 +288,9 @@ a { margin: 15px; } } + +@media (max-device-width: 700px), (max-width: 700px) { + .stripedBox { + margin-top: 72px; + } +} diff --git a/app/fileManager.js b/app/fileManager.js index f294f199..204034ad 100644 --- a/app/fileManager.js +++ b/app/fileManager.js @@ -1,4 +1,5 @@ /* global MAXFILESIZE */ +/* global DEFAULT_EXPIRE_SECONDS */ import FileSender from './fileSender'; import FileReceiver from './fileReceiver'; import { copyToClipboard, delay, openLinksInNewTab, percent } from './utils'; @@ -110,7 +111,9 @@ export default function(state, emitter) { emitter.on('upload', async ({ type, dlCount, password }) => { if (!state.archive) return; const size = state.archive.size; - const sender = new FileSender(state.archive); + if (!state.timeLimit) state.timeLimit = DEFAULT_EXPIRE_SECONDS; + const sender = new FileSender(state.archive, state.timeLimit); + sender.on('progress', updateProgress); sender.on('encrypting', render); sender.on('complete', render); @@ -157,7 +160,7 @@ export default function(state, emitter) { } } finally { openLinksInNewTab(links, false); - state.files = []; + state.archive = null; state.password = ''; state.uploading = false; state.transfer = null; diff --git a/app/fileSender.js b/app/fileSender.js index d8ef3f8b..42d67e40 100644 --- a/app/fileSender.js +++ b/app/fileSender.js @@ -1,4 +1,4 @@ -/* global EXPIRE_SECONDS */ +/* global DEFAULT_EXPIRE_SECONDS */ import Nanobus from 'nanobus'; import OwnedFile from './ownedFile'; import Keychain from './keychain'; @@ -7,8 +7,9 @@ import { uploadWs } from './api'; import { encryptedSize } from './ece'; export default class FileSender extends Nanobus { - constructor(file) { + constructor(file, timeLimit) { super('FileSender'); + this.timeLimit = timeLimit || DEFAULT_EXPIRE_SECONDS; this.file = file; this.keychain = new Keychain(); this.reset(); @@ -70,10 +71,16 @@ export default class FileSender extends Nanobus { const metadata = await this.keychain.encryptMetadata(this.file); const authKeyB64 = await this.keychain.authKeyB64(); - this.uploadRequest = uploadWs(encStream, metadata, authKeyB64, p => { - this.progress = [p, totalSize]; - this.emit('progress'); - }); + this.uploadRequest = uploadWs( + encStream, + metadata, + authKeyB64, + p => { + this.progress = [p, totalSize]; + this.emit('progress'); + }, + this.timeLimit + ); if (this.cancelled) { throw new Error(0); @@ -97,10 +104,11 @@ export default class FileSender extends Nanobus { time: time, speed: this.file.size / (time / 1000), createdAt: Date.now(), - expiresAt: Date.now() + EXPIRE_SECONDS * 1000, + expiresAt: Date.now() + this.timeLimit * 1000, secretKey: secretKey, nonce: this.keychain.nonce, - ownerToken: result.ownerToken + ownerToken: result.ownerToken, + timeLimit: this.timeLimit }); return ownedFile; diff --git a/app/ownedFile.js b/app/ownedFile.js index 3cee3594..3b4dfbae 100644 --- a/app/ownedFile.js +++ b/app/ownedFile.js @@ -19,6 +19,7 @@ export default class OwnedFile { this.dtotal = obj.dtotal || 0; this.keychain = new Keychain(obj.secretKey, obj.nonce); this._hasPassword = !!obj.hasPassword; + this.timeLimit = obj.timeLimit; } async setPassword(password) { @@ -80,7 +81,8 @@ export default class OwnedFile { ownerToken: this.ownerToken, dlimit: this.dlimit, dtotal: this.dtotal, - hasPassword: this.hasPassword + hasPassword: this.hasPassword, + timeLimit: this.timeLimit }; } } diff --git a/app/pages/share/index.js b/app/pages/share/index.js index 8d72729b..153c3a98 100644 --- a/app/pages/share/index.js +++ b/app/pages/share/index.js @@ -1,10 +1,10 @@ -/* global EXPIRE_SECONDS */ const html = require('choo/html'); const raw = require('choo/html/raw'); const assets = require('../../../common/assets'); const notFound = require('../notFound'); const deletePopup = require('../../templates/popup'); const uploadedFileList = require('../../templates/uploadedFileList'); +const timeLimitText = require('../../templates/timeLimitText'); const { allowedCopy, delay, fadeOut } = require('../../utils'); module.exports = function(state, emit) { @@ -18,7 +18,6 @@ module.exports = function(state, emit) { : 'passwordReminder--hidden'; return html` -
@@ -98,13 +97,14 @@ module.exports = function(state, emit) { }; function expireInfo(file, translate) { - const hours = Math.floor(EXPIRE_SECONDS / 60 / 60); - const el = html`
${raw( - translate('expireInfo', { - downloadCount: translate('downloadCount', { num: file.dlimit }), - timespan: translate('timespanHours', { num: hours }) - }) - )}
`; + const el = html`
+ ${raw( + translate('expireInfo', { + downloadCount: translate('downloadCount', { num: file.dlimit }), + timespan: timeLimitText(translate, file.timeLimit) + }) + )} +
`; return el; } diff --git a/app/pages/welcome/index.js b/app/pages/welcome/index.js index c20a9965..ad818aa5 100644 --- a/app/pages/welcome/index.js +++ b/app/pages/welcome/index.js @@ -34,6 +34,7 @@ module.exports = function(state, emit) { ${title(state)}
diff --git a/server/config.js b/server/config.js index 6d037721..73985155 100644 --- a/server/config.js +++ b/server/config.js @@ -4,19 +4,24 @@ const path = require('path'); const { randomBytes } = require('crypto'); const conf = convict({ - s3_buckets: { - format: Array, - default: [], - env: 'S3_BUCKETS' + s3_bucket: { + format: String, + default: '', + env: 'S3_BUCKET' }, - num_of_buckets: { + num_of_prefixes: { format: Number, - default: 3, - env: 'NUM_OF_BUCKETS' + default: 5, + env: 'NUM_OF_PREFIXES' + }, + expire_prefixes: { + format: Array, + default: ['5minutes', '1hour', '1day', '1week', '2weeks'], + env: 'EXPIRE_PREFIXES' }, expire_times_seconds: { format: Array, - default: [86400, 604800, 1209600], + default: [300, 3600, 86400, 604800, 1209600], env: 'EXPIRE_TIMES_SECONDS' }, default_expire_seconds: { diff --git a/server/storage/fs.js b/server/storage/fs.js index 69e92bd9..aa6da744 100644 --- a/server/storage/fs.js +++ b/server/storage/fs.js @@ -6,7 +6,7 @@ const mkdirp = require('mkdirp'); const stat = promisify(fs.stat); class FSStorage { - constructor(config, index, log) { + constructor(config, log) { this.log = log; this.dir = config.file_dir; mkdirp.sync(this.dir); diff --git a/server/storage/index.js b/server/storage/index.js index 479aad4c..d3c6c866 100644 --- a/server/storage/index.js +++ b/server/storage/index.js @@ -5,15 +5,10 @@ const createRedisClient = require('./redis'); class DB { constructor(config) { - const Storage = - config.s3_buckets.length > 0 ? require('./s3') : require('./fs'); + const Storage = config.s3_bucket ? require('./s3') : require('./fs'); this.log = mozlog('send.storage'); - this.storage = []; - - for (let i = 0; i < config.num_of_buckets; i++) { - this.storage.push(new Storage(config, i, this.log)); - } + this.storage = new Storage(config, this.log); this.redis = createRedisClient(config); this.redis.on('error', err => { @@ -26,32 +21,33 @@ class DB { return Math.ceil(result) * 1000; } - async getBucket(id) { - return this.redis.hgetAsync(id, 'bucket'); + async getPrefixedId(id) { + const prefix = await this.redis.hgetAsync(id, 'prefix'); + return `${prefix}-${id}`; } async length(id) { - const bucket = await this.redis.hgetAsync(id, 'bucket'); - return this.storage[bucket].length(id); + const filePath = await this.getPrefixedId(id); + return this.storage.length(filePath); } async get(id) { - const bucket = await this.redis.hgetAsync(id, 'bucket'); - return this.storage[bucket].getStream(id); + const filePath = await this.getPrefixedId(id); + return this.storage.getStream(filePath); } async set(id, file, meta, expireSeconds = config.default_expire_seconds) { - const bucketTimes = config.expire_times_seconds; - let bucket = 0; - while (bucket < config.num_of_buckets - 1) { - if (expireSeconds <= bucketTimes[bucket]) { + const expireTimes = config.expire_times_seconds; + let i; + for (i = 0; i < expireTimes.length - 1; i++) { + if (expireSeconds <= expireTimes[i]) { break; } - bucket++; } - - await this.storage[bucket].set(id, file); - this.redis.hset(id, 'bucket', bucket); + const prefix = config.expire_prefixes[i]; + const filePath = `${prefix}-${id}`; + await this.storage.set(filePath, file); + this.redis.hset(id, 'prefix', prefix); this.redis.hmset(id, meta); this.redis.expire(id, expireSeconds); } @@ -61,16 +57,14 @@ class DB { } async del(id) { - const bucket = await this.redis.hgetAsync(id, 'bucket'); + const filePath = await this.getPrefixedId(id); + this.storage.del(filePath); this.redis.del(id); - this.storage[bucket].del(id); } async ping() { await this.redis.pingAsync(); - for (const bucket of this.storage) { - bucket.ping(); - } + await this.storage.ping(); } async metadata(id) { diff --git a/server/storage/s3.js b/server/storage/s3.js index 08d6133f..bb2b0100 100644 --- a/server/storage/s3.js +++ b/server/storage/s3.js @@ -2,8 +2,8 @@ const AWS = require('aws-sdk'); const s3 = new AWS.S3(); class S3Storage { - constructor(config, index, log) { - this.bucket = config.s3_buckets[index]; + constructor(config, log) { + this.bucket = config.s3_bucket; this.log = log; } diff --git a/test/backend/s3-tests.js b/test/backend/s3-tests.js index d4afd8b3..997b7c34 100644 --- a/test/backend/s3-tests.js +++ b/test/backend/s3-tests.js @@ -32,8 +32,8 @@ const S3Storage = proxyquire('../../server/storage/s3', { }); describe('S3Storage', function() { - it('uses config.s3_buckets', function() { - const s = new S3Storage({ s3_buckets: ['foo', 'bar', 'baz'] }, 0); + it('uses config.s3_bucket', function() { + const s = new S3Storage({ s3_bucket: 'foo' }); assert.equal(s.bucket, 'foo'); }); @@ -42,7 +42,7 @@ describe('S3Storage', function() { s3Stub.headObject = sinon .stub() .returns(resolvedPromise({ ContentLength: 123 })); - const s = new S3Storage({ s3_buckets: ['foo', 'bar', 'baz'] }, 0); + const s = new S3Storage({ s3_bucket: 'foo' }); const len = await s.length('x'); assert.equal(len, 123); sinon.assert.calledWithMatch(s3Stub.headObject, { @@ -54,7 +54,7 @@ describe('S3Storage', function() { it('throws when id not found', async function() { const err = new Error(); s3Stub.headObject = sinon.stub().returns(rejectedPromise(err)); - const s = new S3Storage({ s3_buckets: ['foo', 'bar', 'baz'] }, 0); + const s = new S3Storage({ s3_bucket: 'foo' }); try { await s.length('x'); assert.fail(); @@ -70,7 +70,7 @@ describe('S3Storage', function() { s3Stub.getObject = sinon .stub() .returns({ createReadStream: () => stream }); - const s = new S3Storage({ s3_buckets: ['foo', 'bar', 'baz'] }, 0); + const s = new S3Storage({ s3_bucket: 'foo' }); const result = s.getStream('x'); assert.equal(result, stream); sinon.assert.calledWithMatch(s3Stub.getObject, { @@ -84,7 +84,7 @@ describe('S3Storage', function() { it('calls s3.upload', async function() { const file = { on: sinon.stub() }; s3Stub.upload = sinon.stub().returns(resolvedPromise()); - const s = new S3Storage({ s3_buckets: ['foo', 'bar', 'baz'] }, 0); + const s = new S3Storage({ s3_bucket: 'foo' }); await s.set('x', file); sinon.assert.calledWithMatch(s3Stub.upload, { Bucket: 'foo', @@ -103,7 +103,7 @@ describe('S3Storage', function() { promise: () => Promise.reject(err), abort }); - const s = new S3Storage({ s3_buckets: ['foo', 'bar', 'baz'] }, 0); + const s = new S3Storage({ s3_bucket: 'foo' }); try { await s.set('x', file); assert.fail(); @@ -119,7 +119,7 @@ describe('S3Storage', function() { }; const err = new Error(); s3Stub.upload = sinon.stub().returns(rejectedPromise(err)); - const s = new S3Storage({ s3_buckets: ['foo', 'bar', 'baz'] }, 0); + const s = new S3Storage({ s3_bucket: 'foo' }); try { await s.set('x', file); assert.fail(); @@ -132,7 +132,7 @@ describe('S3Storage', function() { describe('del', function() { it('calls s3.deleteObject', async function() { s3Stub.deleteObject = sinon.stub().returns(resolvedPromise(true)); - const s = new S3Storage({ s3_buckets: ['foo', 'bar', 'baz'] }, 0); + const s = new S3Storage({ s3_bucket: 'foo' }); const result = await s.del('x'); assert.equal(result, true); sinon.assert.calledWithMatch(s3Stub.deleteObject, { @@ -145,7 +145,7 @@ describe('S3Storage', function() { describe('ping', function() { it('calls s3.headBucket', async function() { s3Stub.headBucket = sinon.stub().returns(resolvedPromise(true)); - const s = new S3Storage({ s3_buckets: ['foo', 'bar', 'baz'] }, 0); + const s = new S3Storage({ s3_bucket: 'foo' }); const result = await s.ping(); assert.equal(result, true); sinon.assert.calledWithMatch(s3Stub.headBucket, { Bucket: 'foo' }); diff --git a/test/backend/storage-tests.js b/test/backend/storage-tests.js index e92d7a34..d7bfbb76 100644 --- a/test/backend/storage-tests.js +++ b/test/backend/storage-tests.js @@ -21,10 +21,11 @@ class MockStorage { } const config = { - default_expire_seconds: 10, - num_of_buckets: 3, - expire_times_seconds: [86400, 604800, 1209600], - s3_buckets: ['foo', 'bar', 'baz'], + s3_bucket: 'foo', + default_expire_seconds: 20, + num_of_prefixes: 3, + expire_prefixes: ['ten', 'twenty', 'thirty'], + expire_times_seconds: [10, 20, 30], env: 'development', redis_host: 'localhost' }; @@ -48,7 +49,6 @@ describe('Storage', function() { describe('length', function() { it('returns the file size', async function() { - await storage.set('x', null); const len = await storage.length('x'); assert.equal(len, 12); }); @@ -56,7 +56,6 @@ describe('Storage', function() { describe('get', function() { it('returns a stream', async function() { - await storage.set('x', null); const s = await storage.get('x'); assert.equal(s, stream); }); @@ -71,30 +70,31 @@ describe('Storage', function() { assert.equal(Math.ceil(s), seconds); }); - it('puts into right bucket based on expire time', async function() { - for (let i = 0; i < config.num_of_buckets; i++) { - await storage.set( - 'x', - null, - { foo: 'bar' }, - config.expire_times_seconds[i] - ); - const bucket = await storage.getBucket('x'); - assert.equal(bucket, i); - await storage.del('x'); - } + it('adds right prefix based on expire time', async function() { + await storage.set('x', null, { foo: 'bar' }, 10); + const path_x = await storage.getPrefixedId('x'); + assert.equal(path_x, 'ten-x'); + await storage.del('x'); + + await storage.set('y', null, { foo: 'bar' }, 11); + const path_y = await storage.getPrefixedId('y'); + assert.equal(path_y, 'twenty-y'); + await storage.del('y'); + + await storage.set('z', null, { foo: 'bar' }, 33); + const path_z = await storage.getPrefixedId('z'); + assert.equal(path_z, 'thirty-z'); + await storage.del('z'); }); it('sets metadata', async function() { const m = { foo: 'bar' }; await storage.set('x', null, m); const meta = await storage.redis.hgetallAsync('x'); - delete meta.bucket; + delete meta.prefix; await storage.del('x'); assert.deepEqual(meta, m); }); - - //it('throws when storage fails'); }); describe('setField', function() {