refactor to single bucket

This commit is contained in:
Emily 2018-08-09 14:49:52 -07:00
parent 452ccd068b
commit b89bef6e89
7 changed files with 68 additions and 69 deletions

View File

@ -30,7 +30,7 @@ module.exports = function(file, state) {
<span>${bytes(file.size)}</span> · <span>${bytes(file.size)}</span> ·
<span>${state.translate('downloadCount', { <span>${state.translate('downloadCount', {
num: `${number(totalDownloads)} / ${number(downloadLimit)}` num: `${number(totalDownloads)} / ${number(downloadLimit)}`
})}</span> })}</span> ·
<span>${remainingTime}</span> <span>${remainingTime}</span>
</p> </p>
</div> </div>

View File

@ -4,19 +4,24 @@ const path = require('path');
const { randomBytes } = require('crypto'); const { randomBytes } = require('crypto');
const conf = convict({ const conf = convict({
s3_buckets: { s3_bucket: {
format: Array, format: String,
default: [], default: '',
env: 'S3_BUCKETS' env: 'S3_BUCKET'
}, },
num_of_buckets: { num_of_prefixes: {
format: Number, format: Number,
default: 3, default: 5,
env: 'NUM_OF_BUCKETS' env: 'NUM_OF_PREFIXES'
},
expire_prefixes: {
format: Array,
default: ['5minutes', '1hour', '1day', '1week', '2weeks'],
env: 'EXPIRE_PREFIXES'
}, },
expire_times_seconds: { expire_times_seconds: {
format: Array, format: Array,
default: [86400, 604800, 1209600], default: [300, 3600, 86400, 604800, 1209600],
env: 'EXPIRE_TIMES_SECONDS' env: 'EXPIRE_TIMES_SECONDS'
}, },
default_expire_seconds: { default_expire_seconds: {

View File

@ -6,7 +6,7 @@ const mkdirp = require('mkdirp');
const stat = promisify(fs.stat); const stat = promisify(fs.stat);
class FSStorage { class FSStorage {
constructor(config, index, log) { constructor(config, log) {
this.log = log; this.log = log;
this.dir = config.file_dir; this.dir = config.file_dir;
mkdirp.sync(this.dir); mkdirp.sync(this.dir);

View File

@ -5,15 +5,10 @@ const createRedisClient = require('./redis');
class DB { class DB {
constructor(config) { constructor(config) {
const Storage = const Storage = config.s3_bucket ? require('./s3') : require('./fs');
config.s3_buckets.length > 0 ? require('./s3') : require('./fs');
this.log = mozlog('send.storage'); this.log = mozlog('send.storage');
this.storage = []; this.storage = new Storage(config, this.log);
for (let i = 0; i < config.num_of_buckets; i++) {
this.storage.push(new Storage(config, i, this.log));
}
this.redis = createRedisClient(config); this.redis = createRedisClient(config);
this.redis.on('error', err => { this.redis.on('error', err => {
@ -26,32 +21,33 @@ class DB {
return Math.ceil(result) * 1000; return Math.ceil(result) * 1000;
} }
async getBucket(id) { async getPrefixedId(id) {
return this.redis.hgetAsync(id, 'bucket'); const prefix = await this.redis.hgetAsync(id, 'prefix');
return `${prefix}-${id}`;
} }
async length(id) { async length(id) {
const bucket = await this.redis.hgetAsync(id, 'bucket'); const filePath = await this.getPrefixedId(id);
return this.storage[bucket].length(id); return this.storage.length(filePath);
} }
async get(id) { async get(id) {
const bucket = await this.redis.hgetAsync(id, 'bucket'); const filePath = await this.getPrefixedId(id);
return this.storage[bucket].getStream(id); return this.storage.getStream(filePath);
} }
async set(id, file, meta, expireSeconds = config.default_expire_seconds) { async set(id, file, meta, expireSeconds = config.default_expire_seconds) {
const bucketTimes = config.expire_times_seconds; const expireTimes = config.expire_times_seconds;
let bucket = 0; let i;
while (bucket < config.num_of_buckets - 1) { for (i = 0; i < expireTimes.length - 1; i++) {
if (expireSeconds <= bucketTimes[bucket]) { if (expireSeconds <= expireTimes[i]) {
break; break;
} }
bucket++;
} }
const prefix = config.expire_prefixes[i];
await this.storage[bucket].set(id, file); const filePath = `${prefix}-${id}`;
this.redis.hset(id, 'bucket', bucket); await this.storage.set(filePath, file);
this.redis.hset(id, 'prefix', prefix);
this.redis.hmset(id, meta); this.redis.hmset(id, meta);
this.redis.expire(id, expireSeconds); this.redis.expire(id, expireSeconds);
} }
@ -61,16 +57,14 @@ class DB {
} }
async del(id) { 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.redis.del(id);
this.storage[bucket].del(id);
} }
async ping() { async ping() {
await this.redis.pingAsync(); await this.redis.pingAsync();
for (const bucket of this.storage) { await this.storage.ping();
bucket.ping();
}
} }
async metadata(id) { async metadata(id) {

View File

@ -2,8 +2,8 @@ const AWS = require('aws-sdk');
const s3 = new AWS.S3(); const s3 = new AWS.S3();
class S3Storage { class S3Storage {
constructor(config, index, log) { constructor(config, log) {
this.bucket = config.s3_buckets[index]; this.bucket = config.s3_bucket;
this.log = log; this.log = log;
} }

View File

@ -32,8 +32,8 @@ const S3Storage = proxyquire('../../server/storage/s3', {
}); });
describe('S3Storage', function() { describe('S3Storage', function() {
it('uses config.s3_buckets', function() { it('uses config.s3_bucket', function() {
const s = new S3Storage({ s3_buckets: ['foo', 'bar', 'baz'] }, 0); const s = new S3Storage({ s3_bucket: 'foo' });
assert.equal(s.bucket, 'foo'); assert.equal(s.bucket, 'foo');
}); });
@ -42,7 +42,7 @@ describe('S3Storage', function() {
s3Stub.headObject = sinon s3Stub.headObject = sinon
.stub() .stub()
.returns(resolvedPromise({ ContentLength: 123 })); .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'); const len = await s.length('x');
assert.equal(len, 123); assert.equal(len, 123);
sinon.assert.calledWithMatch(s3Stub.headObject, { sinon.assert.calledWithMatch(s3Stub.headObject, {
@ -54,7 +54,7 @@ describe('S3Storage', function() {
it('throws when id not found', async function() { it('throws when id not found', async function() {
const err = new Error(); const err = new Error();
s3Stub.headObject = sinon.stub().returns(rejectedPromise(err)); 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 { try {
await s.length('x'); await s.length('x');
assert.fail(); assert.fail();
@ -70,7 +70,7 @@ describe('S3Storage', function() {
s3Stub.getObject = sinon s3Stub.getObject = sinon
.stub() .stub()
.returns({ createReadStream: () => stream }); .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'); const result = s.getStream('x');
assert.equal(result, stream); assert.equal(result, stream);
sinon.assert.calledWithMatch(s3Stub.getObject, { sinon.assert.calledWithMatch(s3Stub.getObject, {
@ -84,7 +84,7 @@ describe('S3Storage', function() {
it('calls s3.upload', async function() { it('calls s3.upload', async function() {
const file = { on: sinon.stub() }; const file = { on: sinon.stub() };
s3Stub.upload = sinon.stub().returns(resolvedPromise()); 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); await s.set('x', file);
sinon.assert.calledWithMatch(s3Stub.upload, { sinon.assert.calledWithMatch(s3Stub.upload, {
Bucket: 'foo', Bucket: 'foo',
@ -103,7 +103,7 @@ describe('S3Storage', function() {
promise: () => Promise.reject(err), promise: () => Promise.reject(err),
abort abort
}); });
const s = new S3Storage({ s3_buckets: ['foo', 'bar', 'baz'] }, 0); const s = new S3Storage({ s3_bucket: 'foo' });
try { try {
await s.set('x', file); await s.set('x', file);
assert.fail(); assert.fail();
@ -119,7 +119,7 @@ describe('S3Storage', function() {
}; };
const err = new Error(); const err = new Error();
s3Stub.upload = sinon.stub().returns(rejectedPromise(err)); 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 { try {
await s.set('x', file); await s.set('x', file);
assert.fail(); assert.fail();
@ -132,7 +132,7 @@ describe('S3Storage', function() {
describe('del', function() { describe('del', function() {
it('calls s3.deleteObject', async function() { it('calls s3.deleteObject', async function() {
s3Stub.deleteObject = sinon.stub().returns(resolvedPromise(true)); 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'); const result = await s.del('x');
assert.equal(result, true); assert.equal(result, true);
sinon.assert.calledWithMatch(s3Stub.deleteObject, { sinon.assert.calledWithMatch(s3Stub.deleteObject, {
@ -145,7 +145,7 @@ describe('S3Storage', function() {
describe('ping', function() { describe('ping', function() {
it('calls s3.headBucket', async function() { it('calls s3.headBucket', async function() {
s3Stub.headBucket = sinon.stub().returns(resolvedPromise(true)); 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(); const result = await s.ping();
assert.equal(result, true); assert.equal(result, true);
sinon.assert.calledWithMatch(s3Stub.headBucket, { Bucket: 'foo' }); sinon.assert.calledWithMatch(s3Stub.headBucket, { Bucket: 'foo' });

View File

@ -21,10 +21,11 @@ class MockStorage {
} }
const config = { const config = {
default_expire_seconds: 10, s3_bucket: 'foo',
num_of_buckets: 3, default_expire_seconds: 20,
expire_times_seconds: [86400, 604800, 1209600], num_of_prefixes: 3,
s3_buckets: ['foo', 'bar', 'baz'], expire_prefixes: ['ten', 'twenty', 'thirty'],
expire_times_seconds: [10, 20, 30],
env: 'development', env: 'development',
redis_host: 'localhost' redis_host: 'localhost'
}; };
@ -48,7 +49,6 @@ describe('Storage', function() {
describe('length', function() { describe('length', function() {
it('returns the file size', async function() { it('returns the file size', async function() {
await storage.set('x', null);
const len = await storage.length('x'); const len = await storage.length('x');
assert.equal(len, 12); assert.equal(len, 12);
}); });
@ -56,7 +56,6 @@ describe('Storage', function() {
describe('get', function() { describe('get', function() {
it('returns a stream', async function() { it('returns a stream', async function() {
await storage.set('x', null);
const s = await storage.get('x'); const s = await storage.get('x');
assert.equal(s, stream); assert.equal(s, stream);
}); });
@ -71,30 +70,31 @@ describe('Storage', function() {
assert.equal(Math.ceil(s), seconds); assert.equal(Math.ceil(s), seconds);
}); });
it('puts into right bucket based on expire time', async function() { it('adds right prefix based on expire time', async function() {
for (let i = 0; i < config.num_of_buckets; i++) { await storage.set('x', null, { foo: 'bar' }, 10);
await storage.set( const path_x = await storage.getPrefixedId('x');
'x', assert.equal(path_x, 'ten-x');
null,
{ foo: 'bar' },
config.expire_times_seconds[i]
);
const bucket = await storage.getBucket('x');
assert.equal(bucket, i);
await storage.del('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() { it('sets metadata', async function() {
const m = { foo: 'bar' }; const m = { foo: 'bar' };
await storage.set('x', null, m); await storage.set('x', null, m);
const meta = await storage.redis.hgetallAsync('x'); const meta = await storage.redis.hgetallAsync('x');
delete meta.bucket; delete meta.prefix;
await storage.del('x'); await storage.del('x');
assert.deepEqual(meta, m); assert.deepEqual(meta, m);
}); });
//it('throws when storage fails');
}); });
describe('setField', function() { describe('setField', function() {