diff --git a/app/attachments.js b/app/attachments.js new file mode 100644 index 000000000000..99a67fda993e --- /dev/null +++ b/app/attachments.js @@ -0,0 +1,84 @@ +const crypto = require('crypto'); +const fse = require('fs-extra'); +const isArrayBuffer = require('lodash/isArrayBuffer'); +const isString = require('lodash/isString'); +const path = require('path'); +const toArrayBuffer = require('to-arraybuffer'); + + +const PATH = 'attachments'; + + +// getPath :: AbsolutePath -> AbsolutePath +exports.getPath = (userDataPath) => { + if (!isString(userDataPath)) { + throw new TypeError('`userDataPath` must be a string'); + } + return path.join(userDataPath, PATH); +}; + +// ensureDirectory :: AbsolutePath -> IO Unit +exports.ensureDirectory = async (userDataPath) => { + if (!isString(userDataPath)) { + throw new TypeError('`userDataPath` must be a string'); + } + await fse.ensureDir(exports.getPath(userDataPath)); +}; + +// readData :: AttachmentsPath -> +// RelativePath -> +// IO (Promise ArrayBuffer) +exports.readData = (root) => { + if (!isString(root)) { + throw new TypeError('`root` must be a path'); + } + + return async (relativePath) => { + if (!isString(relativePath)) { + throw new TypeError('`relativePath` must be a string'); + } + + const absolutePath = path.join(root, relativePath); + const buffer = await fse.readFile(absolutePath); + return toArrayBuffer(buffer); + }; +}; + +// writeData :: AttachmentsPath -> +// ArrayBuffer -> +// IO (Promise RelativePath) +exports.writeData = (root) => { + if (!isString(root)) { + throw new TypeError('`root` must be a path'); + } + + return async (arrayBuffer) => { + if (!isArrayBuffer(arrayBuffer)) { + throw new TypeError('`arrayBuffer` must be an array buffer'); + } + + const buffer = Buffer.from(arrayBuffer); + const name = exports.createName(); + const relativePath = exports.getRelativePath(name); + const absolutePath = path.join(root, relativePath); + await fse.ensureFile(absolutePath); + await fse.writeFile(absolutePath, buffer); + return relativePath; + }; +}; + +// createName :: Unit -> IO String +exports.createName = () => { + const buffer = crypto.randomBytes(32); + return buffer.toString('hex'); +}; + +// getRelativePath :: String -> IO Path +exports.getRelativePath = (name) => { + if (!isString(name)) { + throw new TypeError('`name` must be a string'); + } + + const prefix = name.slice(0, 2); + return path.join(prefix, name); +}; diff --git a/app/types/attachment/write_attachment_data.js b/app/types/attachment/write_attachment_data.js deleted file mode 100644 index 7a595e94f948..000000000000 --- a/app/types/attachment/write_attachment_data.js +++ /dev/null @@ -1,41 +0,0 @@ -const crypto = require('crypto'); -const fse = require('fs-extra'); -const isArrayBuffer = require('lodash/isArrayBuffer'); -const isString = require('lodash/isString'); -const path = require('path'); - - -// _writeAttachmentData :: AttachmentsPath -> -// ArrayBuffer -> -// IO (Promise Path) -exports.writeAttachmentData = (root) => { - if (!isString(root)) { - throw new TypeError('`root` must be a path'); - } - - return async (arrayBuffer) => { - if (!isArrayBuffer(arrayBuffer)) { - throw new TypeError('`arrayBuffer` must be an array buffer'); - } - - const buffer = Buffer.from(arrayBuffer); - const relativePath = exports._getAttachmentPath(); - const absolutePath = path.join(root, relativePath); - await fse.ensureFile(absolutePath); - await fse.writeFile(absolutePath, buffer); - return relativePath; - }; -}; - -// _getAttachmentName :: Unit -> IO String -exports._getAttachmentName = () => { - const buffer = crypto.randomBytes(32); - return buffer.toString('hex'); -}; - -// _getAttachmentPath :: Unit -> IO Path -exports._getAttachmentPath = () => { - const name = exports._getAttachmentName(); - const prefix = name.slice(0, 2); - return path.join(prefix, name); -}; diff --git a/preload.js b/preload.js index f99d51b54756..c2d3d1a33c29 100644 --- a/preload.js +++ b/preload.js @@ -108,6 +108,10 @@ window.autoOrientImage = autoOrientImage; // ES2015+ modules + const attachmentsPath = Attachments.getPath(app.getPath('userData')); + const readAttachmentData = Attachments.readData(attachmentsPath); + const writeAttachmentData = Attachments.writeData(attachmentsPath); + window.Signal = window.Signal || {}; window.Signal.Logs = require('./js/modules/logs'); window.Signal.OS = require('./js/modules/os'); @@ -115,11 +119,10 @@ window.Signal.Crypto = require('./js/modules/crypto'); window.Signal.Migrations = window.Signal.Migrations || {}; - const attachmentsPath = Attachments.getPath(app.getPath('userData')); - const { writeAttachmentData } = require('./app/types/attachment/write_attachment_data'); // Injected context functions to keep `Message` agnostic from Electron: window.Signal.Migrations.context = { - writeAttachmentData: writeAttachmentData(attachmentsPath), + readAttachmentData, + writeAttachmentData, }; window.Signal.Migrations.V17 = require('./js/modules/migrations/17'); diff --git a/test/app/attachments_test.js b/test/app/attachments_test.js new file mode 100644 index 000000000000..a22d4f32cbed --- /dev/null +++ b/test/app/attachments_test.js @@ -0,0 +1,53 @@ +const fse = require('fs-extra'); +const isEqual = require('lodash/isEqual'); +const path = require('path'); +const stringToArrayBuffer = require('string-to-arraybuffer'); +const tmp = require('tmp'); +const { assert } = require('chai'); + +const Attachments = require('../../app/attachments'); + + +const PREFIX_LENGTH = 2; +const NUM_SEPARATORS = 1; +const NAME_LENGTH = 64; +const PATH_LENGTH = PREFIX_LENGTH + NUM_SEPARATORS + NAME_LENGTH; + +describe('Attachments', () => { + describe('writeData', () => { + let TEMPORARY_DIRECTORY = null; + before(() => { + TEMPORARY_DIRECTORY = tmp.dirSync().name; + }); + + after(async () => { + await fse.remove(TEMPORARY_DIRECTORY); + }); + + it('should write file to disk and return path', async () => { + const input = stringToArrayBuffer('test string'); + const tempDirectory = path.join(TEMPORARY_DIRECTORY, 'Attachments_writeData'); + + const outputPath = await Attachments.writeData(tempDirectory)(input); + const output = await fse.readFile(path.join(tempDirectory, outputPath)); + + assert.lengthOf(outputPath, PATH_LENGTH); + + const inputBuffer = Buffer.from(input); + assert.isTrue(isEqual(inputBuffer, output)); + }); + + describe('createName', () => { + it('should return random file name with correct length', () => { + assert.lengthOf(Attachments.createName(), NAME_LENGTH); + }); + }); + + describe('getRelativePath', () => { + it('should return correct path', () => { + const name = '608ce3bc536edbf7637a6aeb6040bdfec49349140c0dd43e97c7ce263b15ff7e'; + assert.lengthOf(Attachments.getRelativePath(name), PATH_LENGTH); + }); + }); + }); +}); diff --git a/test/app/types/attachment/write_attachment_data_test.js b/test/app/types/attachment/write_attachment_data_test.js deleted file mode 100644 index 1e00664e1a99..000000000000 --- a/test/app/types/attachment/write_attachment_data_test.js +++ /dev/null @@ -1,54 +0,0 @@ -const fse = require('fs-extra'); -const isEqual = require('lodash/isEqual'); -const path = require('path'); -const stringToArrayBuffer = require('string-to-arraybuffer'); -const tmp = require('tmp'); -const { assert } = require('chai'); - -const { - writeAttachmentData, - _getAttachmentName, - _getAttachmentPath, -} = require('../../../../app/types/attachment/write_attachment_data'); - - -const PREFIX_LENGTH = 2; -const NUM_SEPARATORS = 1; -const NAME_LENGTH = 64; -const PATH_LENGTH = PREFIX_LENGTH + NUM_SEPARATORS + NAME_LENGTH; - -describe('writeAttachmentData', () => { - let TEMPORARY_DIRECTORY = null; - before(() => { - TEMPORARY_DIRECTORY = tmp.dirSync().name; - }); - - after(async () => { - await fse.remove(TEMPORARY_DIRECTORY); - }); - - it('should write file to disk and return path', async () => { - const input = stringToArrayBuffer('test string'); - const tempDirectory = path.join(TEMPORARY_DIRECTORY, 'writeAttachmentData'); - - const outputPath = await writeAttachmentData(tempDirectory)(input); - const output = await fse.readFile(path.join(tempDirectory, outputPath)); - - assert.lengthOf(outputPath, PATH_LENGTH); - - const inputBuffer = Buffer.from(input); - assert.isTrue(isEqual(inputBuffer, output)); - }); - - describe('_getAttachmentName', () => { - it('should return random file name with correct length', () => { - assert.lengthOf(_getAttachmentName(), NAME_LENGTH); - }); - }); - - describe('_getAttachmentPath', () => { - it('should return correct path', () => { - assert.lengthOf(_getAttachmentPath(), PATH_LENGTH); - }); - }); -});