Switch from hashed to random attachment file names

Using hashes, we get the benefit of deduplication but if a user receives two
messages with the same attachment, deleting one would delete it for both since
they are only stored once. To avoid the complexity of tracking number of
references, we simply generate random file names similar to iMessage on MacOS
(?) and Signal Android.
This commit is contained in:
Daniel Gasienica 2018-03-13 17:42:12 -04:00
parent d9de6dacba
commit 1283c77518
2 changed files with 17 additions and 31 deletions

View file

@ -1,7 +1,6 @@
const crypto = require('crypto');
const FSE = require('fs-extra');
const isArrayBuffer = require('lodash/isArrayBuffer');
const isBuffer = require('lodash/isBuffer');
const isString = require('lodash/isString');
const Path = require('path');
@ -17,29 +16,20 @@ exports.writeAttachmentData = (root) => {
}
const buffer = new Buffer(arrayBuffer);
const path = Path.join(root, exports._getAttachmentPath(buffer));
const path = Path.join(root, exports._getAttachmentPath());
await FSE.ensureFile(path);
await FSE.writeFile(path, buffer);
return path;
};
};
exports._getAttachmentName = (buffer) => {
if (!isBuffer(buffer)) {
throw new TypeError('`buffer` must be a buffer');
}
const hash = crypto.createHash('sha256');
hash.update(buffer);
return hash.digest('hex');
exports._getAttachmentName = () => {
const buffer = crypto.randomBytes(32);
return buffer.toString('hex');
};
exports._getAttachmentPath = (buffer) => {
if (!isBuffer(buffer)) {
throw new TypeError('`buffer` must be a buffer');
}
const name = exports._getAttachmentName(buffer);
exports._getAttachmentPath = () => {
const name = exports._getAttachmentName();
const prefix = name.slice(0, 3);
return Path.join(prefix, name);
};

View file

@ -2,7 +2,7 @@ const FSE = require('fs-extra');
const isEqual = require('lodash/isEqual');
const Path = require('path');
const stringToArrayBuffer = require('string-to-arraybuffer');
const tempy = require('tempy');
const tmp = require('tmp');
const { assert } = require('chai');
const {
@ -12,11 +12,15 @@ const {
} = require('../../../../app/types/attachment/write_attachment_data');
const PREFIX_LENGTH = 3;
const NUM_SEPARATORS = 1;
const NAME_LENGTH = 64;
const PATH_LENGTH = PREFIX_LENGTH + NUM_SEPARATORS + NAME_LENGTH;
describe('writeAttachmentData', () => {
let TEMPORARY_DIRECTORY = null;
before(() => {
// Sync!
TEMPORARY_DIRECTORY = tempy.directory();
TEMPORARY_DIRECTORY = tmp.dirSync().name;
});
after(async () => {
@ -26,33 +30,25 @@ describe('writeAttachmentData', () => {
it('should write file to disk and return path', async () => {
const input = stringToArrayBuffer('test string');
const tempDirectory = Path.join(TEMPORARY_DIRECTORY, 'writeAttachmentData');
const expectedPath = Path.join(
tempDirectory,
'd55/d5579c46dfcc7f18207013e65b44e4cb4e2c2298f4ac457ba8f82743f31e930b'
);
const outputPath = await writeAttachmentData(tempDirectory)(input);
const output = await FSE.readFile(outputPath);
assert.strictEqual(outputPath, expectedPath);
assert.lengthOf(Path.relative(tempDirectory, outputPath), PATH_LENGTH);
const inputBuffer = Buffer.from(input);
assert.isTrue(isEqual(inputBuffer, output));
});
describe('_getAttachmentName', () => {
it('should return correct name', () => {
const input = Buffer.from('test string', 'utf8');
const expected = 'd5579c46dfcc7f18207013e65b44e4cb4e2c2298f4ac457ba8f82743f31e930b';
assert.strictEqual(_getAttachmentName(input), expected);
it('should return random file name with correct length', () => {
assert.lengthOf(_getAttachmentName(), NAME_LENGTH);
});
});
describe('_getAttachmentPath', () => {
it('should return correct path', () => {
const input = Buffer.from('test string', 'utf8');
const expected = 'd55/d5579c46dfcc7f18207013e65b44e4cb4e2c2298f4ac457ba8f82743f31e930b';
assert.strictEqual(_getAttachmentPath(input), expected);
assert.lengthOf(_getAttachmentPath(), PATH_LENGTH);
});
});
});