From 3cf9226bf4fa900c36407c1e3991ae84ad7847bf Mon Sep 17 00:00:00 2001 From: Vikas Shashidhar Date: Thu, 28 Dec 2017 17:29:27 +0530 Subject: [PATCH] Revert "Merge pull request #269 from keerthi16/skynet" This reverts commit 1388c835ef7904806560f6ab2c304d8851bd7739, reversing changes made to c9d6229d6db8eee8b50d1d44a3d567cd691f68e2. --- demo/search.html | 284 +++++++++++++++ installer/win/Symphony-x64.aip | 19 +- js/compressionLib/index.js | 74 ++++ js/cryptoLib/crypto.js | 192 ++++++++++ js/cryptoLib/index.js | 125 +++++++ js/main.js | 25 +- js/preload/preloadMain.js | 19 + js/search/queue.js | 39 ++ js/search/search.js | 583 ++++++++++++++++++++++++++++++ js/search/searchConfig.js | 63 ++++ js/search/searchLibrary.js | 73 ++++ js/search/searchUtils.js | 166 +++++++++ js/search/utils/checkDiskSpace.js | 44 +++ js/windowMgr.js | 16 +- package.json | 15 +- 15 files changed, 1728 insertions(+), 9 deletions(-) create mode 100644 demo/search.html create mode 100644 js/compressionLib/index.js create mode 100644 js/cryptoLib/crypto.js create mode 100644 js/cryptoLib/index.js create mode 100644 js/search/queue.js create mode 100644 js/search/search.js create mode 100644 js/search/searchConfig.js create mode 100644 js/search/searchLibrary.js create mode 100644 js/search/searchUtils.js create mode 100644 js/search/utils/checkDiskSpace.js diff --git a/demo/search.html b/demo/search.html new file mode 100644 index 00000000..d0c34550 --- /dev/null +++ b/demo/search.html @@ -0,0 +1,284 @@ + + + + + Search + + + +
+

Symphony Electron Search API Demo

+
+

Search

+
+
+ +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ + +
+
+
+ + + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+
+ +
+
+ +
+ +
+ +
+
+
+

Results:

+

+ + + + + + + +
+
+
+
+ + + + diff --git a/installer/win/Symphony-x64.aip b/installer/win/Symphony-x64.aip index 3d56d306..7ad2734c 100644 --- a/installer/win/Symphony-x64.aip +++ b/installer/win/Symphony-x64.aip @@ -58,6 +58,7 @@ + @@ -126,18 +127,25 @@ + + + + + + + - + @@ -200,7 +208,7 @@ - + @@ -233,14 +241,20 @@ + + + + + + @@ -263,6 +277,7 @@ + diff --git a/js/compressionLib/index.js b/js/compressionLib/index.js new file mode 100644 index 00000000..fe76bb0e --- /dev/null +++ b/js/compressionLib/index.js @@ -0,0 +1,74 @@ +const child = require('child_process'); +const path = require('path'); +const isMac = require('../utils/misc.js').isMac; +const isDevEnv = require('../utils/misc.js').isDevEnv; +const searchConfig = require('../search/searchConfig.js'); +const ROOT_PATH = isDevEnv ? path.join(__dirname, '..', '..') : searchConfig.FOLDERS_CONSTANTS.USER_DATA_PATH; + +/** + * Using the child process to execute the tar and lz4 + * compression and the final output of this function + * will be compressed file with ext: .tar.lz4 + * @param pathToFolder + * @param outputPath + * @param callback + */ +function compression(pathToFolder, outputPath, callback) { + if (isMac) { + child.exec(`cd "${ROOT_PATH}" && tar cf - "${pathToFolder}" | "${searchConfig.LIBRARY_CONSTANTS.MAC_LIBRARY_FOLDER}/lz4.exec" > "${outputPath}.tar.lz4"`, (error, stdout, stderr) => { + if (error) { + return callback(new Error(error), null); + } + return callback(null, { + stderr: stderr.toString().trim(), + stdout: stdout.toString().trim() + }); + }) + } else { + child.exec(`cd "${ROOT_PATH}" && "${searchConfig.LIBRARY_CONSTANTS.WIN_LIBRARY_FOLDER}\\tar-win.exe" cf - "${pathToFolder}" | "${searchConfig.LIBRARY_CONSTANTS.LZ4_PATH}" > "${outputPath}.tar.lz4"`, (error, stdout, stderr) => { + if (error) { + return callback(new Error(error), null); + } + return callback(null, { + stderr: stderr.toString().trim(), + stdout: stdout.toString().trim() + }); + }) + } +} + +/** + * This function decompress the file + * and the ext should be .tar.lz4 + * the output will be the user index folder + * @param pathName + * @param callback + */ +function deCompression(pathName, callback) { + if (isMac) { + child.exec(`cd "${ROOT_PATH}" && "${searchConfig.LIBRARY_CONSTANTS.MAC_LIBRARY_FOLDER}/lz4.exec" -d "${pathName}" | tar -xf - `, (error, stdout, stderr) => { + if (error) { + return callback(new Error(error), null); + } + return callback(null, { + stderr: stderr.toString().trim(), + stdout: stdout.toString().trim() + }); + }) + } else { + child.exec(`cd "${ROOT_PATH}" && "${searchConfig.LIBRARY_CONSTANTS.LZ4_PATH}" -d "${pathName}" | "${searchConfig.LIBRARY_CONSTANTS.WIN_LIBRARY_FOLDER}\\tar-win.exe" xf - `, (error, stdout, stderr) => { + if (error) { + return callback(new Error(error), null); + } + return callback(null, { + stderr: stderr.toString().trim(), + stdout: stdout.toString().trim() + }); + }) + } +} + +module.exports = { + compression, + deCompression +}; diff --git a/js/cryptoLib/crypto.js b/js/cryptoLib/crypto.js new file mode 100644 index 00000000..ada68415 --- /dev/null +++ b/js/cryptoLib/crypto.js @@ -0,0 +1,192 @@ +/** + * AES GCM Stream + * This module exports encrypt and decrypt stream constructors which can be + * used to protect data with authenticated encryption. + */ +'use strict'; + +let stream = require('stream'); +let Transform = stream.Transform; +let util = require('util'); +let crypto = require('crypto'); + +let KEY_LENGTH = 32; // bytes +let GCM_NONCE_LENGTH = 12; //bytes +let GCM_MAC_LENGTH = 16; //bytes + +let keyEncoding = 'base64'; + +/** + * Private helper method to validate a key passed into the Encrypt and Decrypt streams. + * Strings are converted it into a buffer, buffers are returned as they are. + * @param key + * @throws Missing, Encoding, or Length errors + * @returns Buffer + */ +let validateAndConvertKey = function(key) { + if (key && key instanceof Buffer && key.length === KEY_LENGTH) { + return key; + } else if (key && typeof key === 'string') { + let bufKey = new Buffer(key, keyEncoding); + if (bufKey.length !== KEY_LENGTH) { + let encodingErrorMessage = 'Provided key string is either of an unknown encoding (expected: ' + + keyEncoding + ') or the wrong length.'; + throw new Error(encodingErrorMessage); + } + return bufKey; + } + let message = 'The key options property is required! Expected ' + + keyEncoding + ' encoded string or a buffer.'; + throw new Error(message); +}; + +exports.encrypt = EncryptionStream; +exports.decrypt = DecryptionStream; + +/** + * createSalt + * Helper method that returns a salt + * @returns string + * @throws error + */ +exports.createSalt = function(length) { + try { + return crypto.randomBytes(length); + } catch (ex) { + throw ex; + } +}; + +/** + * EncryptionStream + * A constructor which returns an encryption stream + * The stream first outputs a 12 byte nonce then encrypted cipher text. + * When the stream is flushed it outputs a 16 byte MAC. + * @param options Object Object.key is the only required param + * @returns {EncryptionStream} + * @constructor + */ +function EncryptionStream(options) { + if (!(this instanceof EncryptionStream)) { + return new EncryptionStream(options); + } + + let nonce = options.nonce || exports.createSalt(12); + + this._key = validateAndConvertKey(options.key); + this._cipher = crypto.createCipheriv('aes-256-gcm', this._key, nonce); + + Transform.call(this, options); + this.push(nonce); +} +util.inherits(EncryptionStream, Transform); + +EncryptionStream.prototype._transform = function(chunk, enc, cb) { + this.push(this._cipher.update(chunk)); + cb(); +}; + +EncryptionStream.prototype._flush = function(cb) { + // final must be called on the cipher before generating a MAC + this._cipher.final(); // this will never output data + this.push(this._cipher.getAuthTag()); // 16 bytes + + cb(); +}; + +/** + * DecryptionStream + * A constructor which returns a decryption stream + * The stream assumes the first 12 bytes of data are the nonce and the final + * 16 bytes received is the MAC. + * @param options Object Object.key is the only required param + * @returns {DecryptionStream} + * @constructor + */ +function DecryptionStream(options) { + if (!(this instanceof DecryptionStream)) { + return new DecryptionStream(options); + } + + this._started = false; + this._nonce = new Buffer(GCM_NONCE_LENGTH); + this._nonceBytesRead = 0; + this._cipherTextChunks = []; + this._key = validateAndConvertKey(options.key); + + Transform.call(this, options); +} +util.inherits(DecryptionStream, Transform); + +DecryptionStream.prototype._transform = function(chunk, enc, cb) { + let chunkLength = chunk.length; + let chunkOffset = 0; + let _chunk = chunk; + if (!this._started) { + if (this._nonceBytesRead < GCM_NONCE_LENGTH) { + let nonceRemaining = GCM_NONCE_LENGTH - this._nonceBytesRead; + chunkOffset = chunkLength <= nonceRemaining ? chunkLength : nonceRemaining; + _chunk.copy(this._nonce, this._nonceBytesRead, 0, chunkOffset); + _chunk = _chunk.slice(chunkOffset); + chunkLength = _chunk.length; + this._nonceBytesRead += chunkOffset; + } + + + if (this._nonceBytesRead === GCM_NONCE_LENGTH) { + this._decipher = crypto.createDecipheriv('aes-256-gcm', this._key, this._nonce); + this._started = true; + } + } + + // We can't use an else because we have no idea how long our chunks will be + // all we know is that once we've got a nonce we can start storing cipher text + if (this._started) { + this._cipherTextChunks.push(_chunk); + } + + cb(); +}; + +DecryptionStream.prototype._flush = function(cb) { + let mac = pullOutMac(this._cipherTextChunks); + if (!mac) { + return this.emit('error', new Error('Decryption failed: bad cipher text.')); + } + this._decipher.setAuthTag(mac); + let decrypted = this._cipherTextChunks.map(function(item) { + return this._decipher.update(item); + }, this); + try { + this._decipher.final(); + } catch (e) { + return cb(); + } + decrypted.forEach(function(item) { + this.push(item); + }, this); + return cb(); +}; + +function pullOutMac(array) { + let macBits = []; + let macByteCount = 0; + let current, macStartIndex; + while (macByteCount !== GCM_MAC_LENGTH && array.length) { + current = array.pop(); + if (macByteCount + current.length <= GCM_MAC_LENGTH) { + macBits.push(current); + macByteCount += current.length; + } else { + macStartIndex = (macByteCount + current.length) - GCM_MAC_LENGTH; + macBits.push(current.slice(macStartIndex)); + array.push(current.slice(0, macStartIndex)); + macByteCount += (current.length - macStartIndex); + } + } + if (macByteCount !== GCM_MAC_LENGTH) { + return null; + } + macBits.reverse(); + return Buffer.concat(macBits, GCM_MAC_LENGTH); +} \ No newline at end of file diff --git a/js/cryptoLib/index.js b/js/cryptoLib/index.js new file mode 100644 index 00000000..6a393328 --- /dev/null +++ b/js/cryptoLib/index.js @@ -0,0 +1,125 @@ +'use strict'; +const path = require('path'); +const fs = require('fs'); +const lz4 = require('../compressionLib'); +const isDevEnv = require('../utils/misc.js').isDevEnv; +const crypto = require('./crypto'); +const log = require('../log.js'); +const logLevels = require('../enums/logLevels.js'); +const searchConfig = require('../search/searchConfig.js'); + +const DUMP_PATH = isDevEnv ? path.join(__dirname, '..', '..') : searchConfig.FOLDERS_CONSTANTS.USER_DATA_PATH; + +class Crypto { + + /** + * Constructor + * @param userId + * @param key + */ + constructor(userId, key) { + this.indexDataFolder = `${searchConfig.FOLDERS_CONSTANTS.PREFIX_NAME_PATH}_${userId}_${searchConfig.INDEX_VERSION}`; + this.permanentIndexName = `${searchConfig.FOLDERS_CONSTANTS.PREFIX_NAME}_${userId}_${searchConfig.INDEX_VERSION}`; + this.dump = DUMP_PATH; + this.key = key; + this.encryptedIndex = `${DUMP_PATH}/${this.permanentIndexName}.enc`; + this.dataFolder = searchConfig.FOLDERS_CONSTANTS.INDEX_PATH; + } + + /** + * Compressing the user index folder and + * encrypting it + * @returns {Promise} + */ + encryption(key) { + return new Promise((resolve, reject) => { + + if (!fs.existsSync(this.indexDataFolder)){ + log.send(logLevels.ERROR, 'Crypto: User index folder not found'); + reject(); + return; + } + + lz4.compression(`${searchConfig.FOLDERS_CONSTANTS.INDEX_FOLDER_NAME}/${this.permanentIndexName}`, + `${this.permanentIndexName}`, (error, response) => { + if (error) { + log.send(logLevels.ERROR, 'Crypto: Error while compressing to lz4: ' + error); + reject(error); + return; + } + + if (response && response.stderr) { + log.send(logLevels.WARN, 'Crypto: Child process stderr while compression, ' + response.stderr); + } + const input = fs.createReadStream(`${this.dump}/${this.permanentIndexName}${searchConfig.TAR_LZ4_EXT}`); + const outputEncryption = fs.createWriteStream(this.encryptedIndex); + let config = { + key: key + }; + const encrypt = crypto.encrypt(config); + + let encryptionProcess = input.pipe(encrypt).pipe(outputEncryption); + + encryptionProcess.on('finish', (err) => { + if (err) { + log.send(logLevels.ERROR, 'Crypto: Error while encrypting the compressed file: ' + err); + reject(new Error(err)); + return; + } + fs.unlinkSync(`${this.dump}/${this.permanentIndexName}${searchConfig.TAR_LZ4_EXT}`); + resolve('Success'); + }); + }); + }); + } + + /** + * Decrypting the .enc file and unzipping + * removing the .enc file and the dump files + * @returns {Promise} + */ + decryption() { + return new Promise((resolve, reject) => { + + if (!fs.existsSync(this.encryptedIndex)){ + log.send(logLevels.ERROR, 'Crypto: Encrypted file not found'); + reject(); + return; + } + + const input = fs.createReadStream(this.encryptedIndex); + const output = fs.createWriteStream(`${this.dump}/decrypted${searchConfig.TAR_LZ4_EXT}`); + let config = { + key: this.key + }; + const decrypt = crypto.decrypt(config); + + let decryptionProcess = input.pipe(decrypt).pipe(output); + + decryptionProcess.on('finish', () => { + + if (!fs.existsSync(`${this.dump}/decrypted${searchConfig.TAR_LZ4_EXT}`)){ + log.send(logLevels.ERROR, 'decrypted.tar.lz4 file not found'); + reject(); + return; + } + + lz4.deCompression(`${this.dump}/decrypted${searchConfig.TAR_LZ4_EXT}`,(error, response) => { + if (error) { + log.send(logLevels.ERROR, 'Crypto: Error while deCompression, ' + error); + // no return, need to unlink if error + } + + if (response && response.stderr) { + log.send(logLevels.WARN, 'Crypto: Child process stderr while deCompression, ' + response.stderr); + } + fs.unlink(`${this.dump}/decrypted${searchConfig.TAR_LZ4_EXT}`, () => { + resolve('success'); + }); + }) + }); + }); + } +} + +module.exports = Crypto; \ No newline at end of file diff --git a/js/main.js b/js/main.js index 7cd4b337..8d3184de 100644 --- a/js/main.js +++ b/js/main.js @@ -5,6 +5,7 @@ const electron = require('electron'); const app = electron.app; const crashReporter = electron.crashReporter; const nodeURL = require('url'); +const shellPath = require('shell-path'); const squirrelStartup = require('electron-squirrel-startup'); const AutoLaunch = require('auto-launch'); const urlParser = require('url'); @@ -17,9 +18,23 @@ const protocolHandler = require('./protocolHandler'); const getCmdLineArg = require('./utils/getCmdLineArg.js'); const log = require('./log.js'); const logLevels = require('./enums/logLevels.js'); +const { deleteIndexFolder } = require('./search/search.js'); require('electron-dl')(); +//setting the env path child_process issue https://github.com/electron/electron/issues/7688 +shellPath() + .then((path) => { + process.env.PATH = path + }) + .catch(() => { + process.env.PATH = [ + './node_modules/.bin', + '/usr/local/bin', + process.env.PATH + ].join(':'); + }); + // used to check if a url was opened when the app was already open let isAppAlreadyOpen = false; @@ -40,13 +55,13 @@ getConfigField('url') .catch(app.quit); function initializeCrashReporter(podUrl) { - + getConfigField('crashReporter') .then((crashReporterConfig) => { crashReporter.start({companyName: crashReporterConfig.companyName, submitURL: crashReporterConfig.submitURL, uploadToServer: crashReporterConfig.uploadToServer, extra: {'process': 'main', podUrl: podUrl}}); log.send(logLevels.INFO, 'initialized crash reporter on the main process!'); }) - .catch((err) => { + .catch((err) => { log.send(logLevels.ERROR, 'Unable to initialize crash reporter in the main process. Error is -> ' + err); }); @@ -149,6 +164,12 @@ app.on('activate', function() { } }); +app.on('will-quit', function (e) { + e.preventDefault(); + deleteIndexFolder(); + app.exit(); +}); + // adds 'symphony' as a protocol // in the system. plist file in macOS // and registry keys in windows diff --git a/js/preload/preloadMain.js b/js/preload/preloadMain.js index 0ed79a1d..165dd10b 100644 --- a/js/preload/preloadMain.js +++ b/js/preload/preloadMain.js @@ -129,6 +129,25 @@ function createAPI() { process.crash(); }, + /** + * Provides api for client side searching + * using the SymphonySearchEngine library + * details in ./search/search.js & ./search/searchLibrary.js + */ + Search: remote.require('./search/search.js').Search, + + /** + * Provides api for search module utils + * like checking free space / search user config data to the client app + * details in ./search/searchUtils.js & ./search/searchConfig.js + */ + SearchUtils: remote.require('./search/searchUtils.js').SearchUtils, + + /** + * Function to clear the user index data + */ + deleteIndexFolder: remote.require('./search/search.js').deleteIndexFolder, + /** * Brings window forward and gives focus. * @param {String} windowName Name of window. Note: main window name is 'main' diff --git a/js/search/queue.js b/js/search/queue.js new file mode 100644 index 00000000..65452808 --- /dev/null +++ b/js/search/queue.js @@ -0,0 +1,39 @@ +let messagesData = []; + +let makeBoundTimedCollector = function(isIndexing, timeout, callback) { + let timer; + + return function (...args) { + if (!timer){ + timer = setTimeout(function(){ + if (!isIndexing) { + flush(getQueue()); + } + }, timeout); + } + + let queue = getQueue(); + queue.push(args[0]); + + if (!isIndexing()) { + flush(queue); + } + }; + + function flush(queue) { + clearTimeout(timer); + timer = null; + resetQueue(); + callback(JSON.stringify(queue)); + } + + function getQueue(){ + return messagesData; + } + + function resetQueue(){ + messagesData = []; + } +}; + +module.exports = makeBoundTimedCollector; diff --git a/js/search/search.js b/js/search/search.js new file mode 100644 index 00000000..239a9eba --- /dev/null +++ b/js/search/search.js @@ -0,0 +1,583 @@ +'use strict'; + +const fs = require('fs'); +const randomString = require('randomstring'); +const childProcess = require('child_process'); +const path = require('path'); +const isDevEnv = require('../utils/misc.js').isDevEnv; +const isMac = require('../utils/misc.js').isMac; +const makeBoundTimedCollector = require('./queue'); +const searchConfig = require('./searchConfig'); +const log = require('../log.js'); +const logLevels = require('../enums/logLevels.js'); + +const libSymphonySearch = require('./searchLibrary'); +const Crypto = require('../cryptoLib'); + +const INDEX_VALIDATOR = searchConfig.LIBRARY_CONSTANTS.INDEX_VALIDATOR; + +/** + * This search class communicates with the SymphonySearchEngine C library via node-ffi. + * There should be only 1 instance of this class in the Electron + */ +class Search { + + /** + * Constructor for the SymphonySearchEngine library + * @param userId (for the index folder name) + * @param key + */ + constructor(userId, key) { + this.isInitialized = false; + this.userId = userId; + this.key = key; + this.indexFolderName = `${searchConfig.FOLDERS_CONSTANTS.PREFIX_NAME_PATH}_${this.userId}_${searchConfig.INDEX_VERSION}`; + this.dataFolder = searchConfig.FOLDERS_CONSTANTS.INDEX_PATH; + this.realTimeIndex = searchConfig.FOLDERS_CONSTANTS.TEMP_REAL_TIME_INDEX; + this.batchIndex = searchConfig.FOLDERS_CONSTANTS.TEMP_BATCH_INDEX_FOLDER; + this.messageData = []; + this.isRealTimeIndexing = false; + this.crypto = new Crypto(userId, key); + this.decryptAndInit(); + this.collector = makeBoundTimedCollector(this.checkIsRealTimeIndexing.bind(this), + searchConfig.REAL_TIME_INDEXING_TIME, this.realTimeIndexing.bind(this)); + } + + /** + * Decrypting the existing user .enc file + * and initialing the library + */ + decryptAndInit() { + this.crypto.decryption().then(() => { + this.init(); + }).catch(() => { + this.init(); + }); + } + + /** + * returns isInitialized boolean + * @returns {boolean} + */ + isLibInit() { + return this.isInitialized; + } + + /** + * This init function + * initialise the SymphonySearchEngine library + * and creates a folder in the userData + */ + init() { + libSymphonySearch.symSEInit(); + libSymphonySearch.symSEEnsureFolderExists(this.dataFolder); + Search.deleteIndexFolders(this.realTimeIndex); + Search.deleteIndexFolders(this.batchIndex); + Search.indexValidator(this.indexFolderName); + Search.indexValidator(this.realTimeIndex); + let indexDateStartFrom = new Date().getTime() - searchConfig.SEARCH_PERIOD_SUBTRACTOR; + // Deleting all the messages except 3 Months from now + libSymphonySearch.symSEDeleteMessages(this.indexFolderName, null, + searchConfig.MINIMUM_DATE, indexDateStartFrom.toString()); + this.isInitialized = true; + } + + /** + * An array of messages is passed for indexing + * it will be indexed in a temporary index folder + * @param {Array} messages + * @returns {Promise} + */ + indexBatch(messages) { + return new Promise((resolve, reject) => { + if (!messages) { + log.send(logLevels.ERROR, 'Batch Indexing: Messages not provided'); + reject(new Error('Batch Indexing: Messages is required')); + return; + } + + try { + let msg = JSON.parse(messages); + if (!(msg instanceof Array)) { + log.send(logLevels.ERROR, 'Batch Indexing: Messages must be an array'); + reject(new Error('Batch Indexing: Messages must be an array')); + return; + } + } catch(e) { + log.send(logLevels.ERROR, 'Batch Indexing: parse error -> ' + e); + reject(new Error(e)); + return; + } + + if (!this.isInitialized) { + log.send(logLevels.ERROR, 'Library not initialized'); + reject(new Error('Library not initialized')); + return; + } + + const indexId = randomString.generate(searchConfig.BATCH_RANDOM_INDEX_PATH_LENGTH); + libSymphonySearch.symSECreatePartialIndexAsync(this.batchIndex, indexId, messages, (err, res) => { + if (err) { + log.send(logLevels.ERROR, 'Batch Indexing: error ->' + err); + reject(new Error(err)); + return; + } + resolve(res); + }); + }); + } + + /** + * Merging the temporary + * created from indexBatch() + */ + mergeIndexBatches() { + return new Promise((resolve, reject) => { + libSymphonySearch.symSEMergePartialIndexAsync(this.indexFolderName, this.batchIndex, (err, res) => { + if (err) { + log.send(logLevels.ERROR, 'Error merging the index ->' + err); + reject(new Error(err)); + return; + } + Search.deleteIndexFolders(this.batchIndex); + resolve(res); + }); + }); + } + + /** + * Batching the real time + * messages for queue and flush + * @param {Object} message + */ + batchRealTimeIndexing(message) { + this.collector(message); + } + + /** + * Returns the current state of the + * real-time indexing + * @returns {boolean} + */ + checkIsRealTimeIndexing() { + return this.isRealTimeIndexing; + } + + /** + * An array of messages to be indexed + * in real time + * @param message + */ + realTimeIndexing(message) { + if (!message) { + log.send(logLevels.ERROR, 'RealTime Indexing: Messages not provided'); + return new Error('RealTime Indexing: Messages is required'); + } + + try { + let msg = JSON.parse(message); + if (!(msg instanceof Array)) { + log.send(logLevels.ERROR, 'RealTime Indexing: Messages must be an array real-time indexing'); + return (new Error('RealTime Indexing: Messages must be an array')); + } + } catch(e) { + log.send(logLevels.ERROR, 'RealTime Indexing: parse error -> ' + e); + return (new Error(e)); + } + + if (!this.isInitialized) { + log.send(logLevels.ERROR, 'Library not initialized'); + return new Error('Library not initialized'); + } + + this.isRealTimeIndexing = true; + return libSymphonySearch.symSEIndexRealTimeAsync(this.realTimeIndex, message, (err, result) => { + this.isRealTimeIndexing = false; + if (err) { + log.send(logLevels.ERROR, 'RealTime Indexing: error -> ' + err); + return new Error(err); + } + return result; + }); + } + + /** + * Reading a json file + * for the demo search app only + * @param {String} batch + * @returns {Promise} + */ + readJson(batch) { + return new Promise((resolve, reject) => { + let dirPath = path.join(searchConfig.FOLDERS_CONSTANTS.EXEC_PATH, isMac ? '..' : '', 'msgsjson', batch); + let messageFolderPath = isDevEnv ? path.join('./msgsjson', batch) : dirPath; + let files = fs.readdirSync(messageFolderPath); + this.messageData = []; + files.forEach((file) => { + let tempPath = path.join(messageFolderPath, file); + let data = fs.readFileSync(tempPath, "utf8"); + if (data) { + try { + this.messageData.push(JSON.parse(data)); + } catch (err) { + reject(new Error(err)) + } + } else { + reject(new Error('Error reading batch')) + } + }); + resolve(this.messageData); + }); + } + + /** + * Encrypting the index after the merging the index + * to the main user index + */ + encryptIndex(key) { + return this.crypto.encryption(key); + } + + /** + * This returns the search results + * which returns a char * + * @param {String} query + * @param {Array} senderIds + * @param {Array} threadIds + * @param {String} fileType + * @param {String} startDate + * @param {String} endDate + * @param {Number} limit + * @param {Number} offset + * @param {Number} sortOrder + * @returns {Promise} + */ + searchQuery(query, senderIds, threadIds, fileType, startDate, + endDate, limit, offset, sortOrder) { + + let _limit = limit; + let _offset = offset; + let _sortOrder = sortOrder; + + return new Promise((resolve, reject) => { + if (!this.isInitialized) { + log.send(logLevels.ERROR, 'Library not initialized'); + reject(new Error('Library not initialized')); + return; + } + + if (!fs.existsSync(this.indexFolderName) || !fs.existsSync(this.realTimeIndex)) { + log.send(logLevels.ERROR, 'Index folder does not exist.'); + reject('Index folder does not exist.'); + return; + } + + let q = Search.constructQuery(query, senderIds, threadIds, fileType); + + if (q === undefined) { + reject(new Error('Search query error')); + return; + } + + let searchPeriod = new Date().getTime() - searchConfig.SEARCH_PERIOD_SUBTRACTOR; + let startDateTime = searchPeriod; + if (startDate) { + startDateTime = new Date(parseInt(startDate, 10)).getTime(); + if (!startDateTime || startDateTime < searchPeriod) { + startDateTime = searchPeriod; + } + } + + let endDateTime = searchConfig.MAXIMUM_DATE; + if (endDate) { + let eTime = new Date(parseInt(endDate, 10)).getTime(); + if (eTime) { + endDateTime = eTime; + } + } + + if (!_limit && _limit === "" && typeof _limit !== 'number' && Math.round(_limit) !== _limit) { + _limit = 25; + } + + if (!_offset && _offset === "" && typeof _offset !== 'number' && Math.round(_offset) !== _offset) { + _offset = 0 + } + + if (!_sortOrder && _sortOrder === "" && typeof _sortOrder !== 'number' && Math.round(_sortOrder) !== _sortOrder) { + _sortOrder = searchConfig.SORT_BY_SCORE; + } + + const returnedResult = libSymphonySearch.symSESearch(this.indexFolderName, this.realTimeIndex, q, startDateTime.toString(), endDateTime.toString(), _offset, _limit, _sortOrder); + try { + let ret = returnedResult.readCString(); + resolve(JSON.parse(ret)); + } finally { + libSymphonySearch.symSEFreeResult(returnedResult); + } + }); + } + + /** + * returns the latest message timestamp + * from the indexed data + * @returns {Promise} + */ + getLatestMessageTimestamp() { + return new Promise((resolve, reject) => { + if (!this.isInitialized) { + log.send(logLevels.ERROR, 'Library not initialized'); + reject('Not initialized'); + return; + } + + if (!fs.existsSync(this.indexFolderName)) { + log.send(logLevels.ERROR, 'Index folder does not exist.'); + reject('Index folder does not exist.'); + return; + } + + libSymphonySearch.symSEGetLastMessageTimestampAsync(this.indexFolderName, (err, res) => { + if (err) { + log.send(logLevels.ERROR, 'Error getting the index timestamp ->' + err); + reject(new Error(err)); + } + const returnedResult = res; + try { + let ret = returnedResult.readCString(); + resolve(ret); + } finally { + libSymphonySearch.symSEFreeResult(returnedResult); + } + }); + }); + } + + deleteRealTimeFolder() { + Search.deleteIndexFolders(this.realTimeIndex); + Search.indexValidator(this.realTimeIndex); + } + + /** + * This the query constructor + * for the search function + * @param {String} searchQuery + * @param {Array} senderId + * @param {Array} threadId + * @param {String} fileType + * @returns {string} + */ + static constructQuery(searchQuery, senderId, threadId, fileType) { + + let searchText = ""; + let textQuery = ""; + if(searchQuery !== undefined) { + searchText = searchQuery.trim().toLowerCase(); //to prevent injection of AND and ORs + textQuery = Search.getTextQuery(searchText); + } + let q = ""; + let hashTags = Search.getHashTags(searchText); + let hashCashTagQuery = ""; + + if(hashTags.length > 0) { + hashCashTagQuery = " OR tags:("; + hashTags.forEach((item) => { + hashCashTagQuery = hashCashTagQuery + "\"" + item + "\" " + }); + hashCashTagQuery += ")"; + } + + let hasAttachments = false; + let additionalAttachmentQuery = ""; + if(fileType) { + hasAttachments = true; + if(fileType.toLowerCase() === "attachment") { + additionalAttachmentQuery = "(hasfiles:true)"; + } else { + additionalAttachmentQuery = "(filetype:(" + fileType +"))"; + } + } + + + if (searchText.length > 0 ) { + q = "((text:(" + textQuery + "))" + hashCashTagQuery ; + if(hasAttachments) { + q += " OR (filename:(" + searchText + "))" ; + } + q = q + ")"; + } + + q = Search.appendFilterQuery(q, "senderId", senderId); + q = Search.appendFilterQuery(q, "threadId", threadId); + + if(q === "") { + if(hasAttachments) { + q = additionalAttachmentQuery; + } else { + q = undefined; //will be handled in the search function + } + } else { + if(hasAttachments){ + q = q + " AND " + additionalAttachmentQuery + } + } + return q; + } + + /** + * appending the senderId and threadId for the query + * @param {String} searchText + * @param {String} fieldName + * @param {Array} valueArray + * @returns {string} + */ + static appendFilterQuery(searchText, fieldName, valueArray) { + let q = ""; + if (valueArray && valueArray.length > 0 ) { + + q += "(" + fieldName +":("; + valueArray.forEach((item)=>{ + q+= "\"" + item + "\" "; + }); + q += "))"; + if(searchText.length > 0 ) { + q = searchText + " AND " + q; + } + + } else { + q = searchText; + } + + return q; + } + + // hashtags can have any characters(before the latest release it was + // not like this). So the only regex is splitting the search query based on + // whitespaces + /** + * return the hash cash + * tags from the query + * @param {String} searchText + * @returns {Array} + */ + static getHashTags(searchText) { + let hashTags = []; + let tokens = searchText.toLowerCase() + .trim() + .replace(/\s\s+/g, ' ') + .split(' ').filter((el) => {return el.length !== 0}); + tokens.forEach((item) => { + if (item.startsWith('#') || item.startsWith('$')) { + hashTags.push(item); + } + }); + return hashTags; + } + + /** + * If the search query does not have double quotes (implying phrase search), + * then create all tuples of the terms in the search query + * @param {String} searchText + * @returns {String} + */ + + static getTextQuery(searchText) { + let s1 = searchText.trim().toLowerCase(); + //if contains quotes we assume it will be a phrase search + if(searchText.indexOf("\"") !== -1 ) { + return s1; + } + //else we will create tuples + let s2 = s1.replace(/\s{2,}/g," ").trim(); + let tokens = s2.split(" "); + + let i,j = 0; + let out = ""; + for(i = tokens.length; i > 0; i--) {// number of tokens in a tuple + for(j = 0; j < tokens.length-i + 1 ; j++){ //start from index + if(out !== ""){ + out += " "; + } + out += Search.putTokensInRange(tokens, j, i); + } + } + return out; + } + + /** + * Helper function for getTextQuery() + * Given a list of tokens create a tuple given the start index of the + * token list and given the number of tokens to create. + * @param {Array} tokens + * @param {Number} start + * @param {Number} numTokens + * @returns {String} + */ + static putTokensInRange(tokens, start, numTokens) { + let out = "\""; + for(let i = 0; i < numTokens; i++) { + if(i !== 0) { + out += " "; + } + out+= tokens[start+i]; + } + out += "\""; + return out; + } + + /** + * Validate the index folder exist or not + * @param {String} file + * @returns {*} + */ + static indexValidator(file) { + let data; + let result = childProcess.execFileSync(INDEX_VALIDATOR, [file]).toString(); + try { + data = JSON.parse(result); + if (data.status === 'OK') { + return data; + } + log.send(logLevels.ERROR, 'Unable validate index folder'); + return new Error('Unable validate index folder') + } catch (err) { + throw new Error(err); + } + } + + /** + * Removing all the folders and files inside the data folder + * @param location + */ + static deleteIndexFolders(location) { + if (fs.existsSync(location)) { + fs.readdirSync(location).forEach((file) => { + let curPath = location + "/" + file; + if (fs.lstatSync(curPath).isDirectory()) { + Search.deleteIndexFolders(curPath); + } else { + fs.unlinkSync(curPath); + } + }); + fs.rmdirSync(location); + } + } + +} + +/** + * Deleting the data index folder + * when the app is closed/signed-out/navigates + */ +function deleteIndexFolder() { + Search.deleteIndexFolders(searchConfig.FOLDERS_CONSTANTS.INDEX_PATH); +} + +/** + * Exporting the search library + * @type {{Search: Search}} + */ +module.exports = { + Search: Search, + deleteIndexFolder: deleteIndexFolder +}; diff --git a/js/search/searchConfig.js b/js/search/searchConfig.js new file mode 100644 index 00000000..9be1bf89 --- /dev/null +++ b/js/search/searchConfig.js @@ -0,0 +1,63 @@ +const electron = require('electron'); +const app = electron.app; +const path = require('path'); +const userData = path.join(app.getPath('userData')); +const execPath = path.dirname(app.getPath('exe')); +const { isDevEnv, isMac } = require('../utils/misc.js'); + +const INDEX_FOLDER_NAME = 'data'; + +const winLibraryPath = isDevEnv ? path.join(__dirname, '..', '..', 'library') : path.join(execPath, 'library'); +const macLibraryPath = isDevEnv ? path.join(__dirname, '..', '..', 'library') : path.join(execPath, '..', 'library'); + +const arch = process.arch === 'ia32'; + +const winIndexValidatorArch = arch ? 'indexvalidator-x86.exe' : 'indexvalidator-x64.exe'; +const indexValidatorPath = isMac ? path.join(macLibraryPath, 'indexvalidator.exec') : path.join(winLibraryPath, winIndexValidatorArch); + +const winLZ4ArchPath = arch ? 'lz4-win-x86.exe' : 'lz4-win-x64.exe'; +const lz4Path = path.join(winLibraryPath, winLZ4ArchPath); + +const indexFolderPath = isDevEnv ? `./${INDEX_FOLDER_NAME}` : path.join(userData, INDEX_FOLDER_NAME); + +const winSearchLibArchPath = arch ? 'libsymphonysearch-x86.dll' : 'libsymphonysearch-x64.dll'; +const libraryPath = isMac ? path.join(macLibraryPath, 'libsymphonysearch.dylib') : path.join(winLibraryPath, winSearchLibArchPath); + +const userConfigFileName = 'search_users_config.json'; +const userConfigFile = isDevEnv ? path.join(__dirname, '..', '..', userConfigFileName) : path.join(userData, userConfigFileName); + +const libraryPaths = { + INDEX_VALIDATOR: indexValidatorPath, + LZ4_PATH: lz4Path, + MAC_LIBRARY_FOLDER: macLibraryPath, + WIN_LIBRARY_FOLDER: winLibraryPath, + SEARCH_LIBRARY_PATH: libraryPath +}; + +const folderPaths = { + INDEX_PATH: indexFolderPath, + TEMP_BATCH_INDEX_FOLDER: indexFolderPath + '/temp_batch_indexes', + TEMP_REAL_TIME_INDEX: indexFolderPath + '/temp_realtime_index', + PREFIX_NAME: 'search_index', + PREFIX_NAME_PATH: indexFolderPath + '/search_index', + EXEC_PATH: execPath, + USER_DATA_PATH: userData, + INDEX_FOLDER_NAME: INDEX_FOLDER_NAME, + USER_CONFIG_FILE: userConfigFile +}; + +const searchConfig = { + SEARCH_PERIOD_SUBTRACTOR: 3 * 31 * 24 * 60 * 60 * 1000, + REAL_TIME_INDEXING_TIME: 60000, + MINIMUM_DATE: '0000000000000', + MAXIMUM_DATE: '9999999999999', + INDEX_VERSION: 'v1', + SORT_BY_SCORE: 0, + BATCH_RANDOM_INDEX_PATH_LENGTH: 20, + LIBRARY_CONSTANTS: libraryPaths, + FOLDERS_CONSTANTS: folderPaths, + TAR_LZ4_EXT: '.tar.lz4', + MINIMUM_DISK_SPACE: 300000000 // in bytes +}; + +module.exports = searchConfig; diff --git a/js/search/searchLibrary.js b/js/search/searchLibrary.js new file mode 100644 index 00000000..8d6da093 --- /dev/null +++ b/js/search/searchLibrary.js @@ -0,0 +1,73 @@ +'use strict'; + +const ffi = require('ffi'); +const ref = require('ref'); + +const searchConfig = require('../search/searchConfig.js'); + +const symLucyIndexer = ref.types.void; +const symLucyIndexerPtr = ref.refType(symLucyIndexer); + +/** + * Initializing the C SymphonySearchEngine library + * using the node-ffi + */ +let libSymphonySearch = ffi.Library(searchConfig.LIBRARY_CONSTANTS.SEARCH_LIBRARY_PATH, { + //init + 'symSE_init': ['void', []], + 'symSE_remove_folder': ['int', ['string']], + 'symSE_ensure_index_exists': ['int', ['string']], + 'symSE_ensure_folder_exists': ['int', ['string']], + //first time indexing and delta indexing + 'symSE_get_indexer': [symLucyIndexerPtr, ['string']], //will be removed + 'symSE_create_partial_index': ['int', ['string', 'string', 'string']], + 'symSE_merge_partial_index': ['int', ['string', 'string']], + //real time indexing + 'symSE_index_realtime': ['int', ['string', 'string']], + 'symSE_merge_temp_index': ['int', ['string', 'string']], + 'symSE_clear_temp_index': ['int', ['string']], + //Search, + 'symSE_search': ['char *', ['string', 'string', 'string', 'string', 'string', 'int', 'int', 'int']], + //Deletion + 'symSE_delete_messages': ['int', ['string', 'string', 'string', 'string']], + //Index commit/optimize + 'symSE_commit_index': ['int', [symLucyIndexerPtr, 'int']], //will be removed + //freePointer + 'symSE_free_results': ['int', ['char *']], + + //Latest messages timestamp + 'symSE_get_last_message_timestamp': ['char *', ['string']] +}); + +module.exports = { + symSEInit: libSymphonySearch.symSE_init, + symSERemoveFolder: libSymphonySearch.symSE_remove_folder, + symSEEnsureIndexExists: libSymphonySearch.symSE_ensure_index_exists, + symSEEnsureFolderExists: libSymphonySearch.symSE_ensure_folder_exists, + symSEGetIndexer: libSymphonySearch.symSE_get_indexer, + symSECreatePartialIndex: libSymphonySearch.symSE_create_partial_index, + symSEMergePartialIndex: libSymphonySearch.symSE_merge_partial_index, + symSEIndexRealTime: libSymphonySearch.symSE_index_realtime, + symSEMergeTempIndex: libSymphonySearch.symSE_merge_temp_index, + symSEClearTempIndex: libSymphonySearch.symSE_clear_temp_index, + symSESearch: libSymphonySearch.symSE_search, + symSEDeleteMessages: libSymphonySearch.symSE_delete_messages, + symSECommitIndex: libSymphonySearch.symSE_commit_index, + symSEFreeResult: libSymphonySearch.symSE_free_results, + symSEGetLastMessageTimestamp: libSymphonySearch.symSE_get_last_message_timestamp, + symSEInitAsync: libSymphonySearch.symSE_init.async, + symSERemoveFolderAsync: libSymphonySearch.symSE_remove_folder.async, + symSEEnsureIndexExistsAsync: libSymphonySearch.symSE_ensure_index_exists.async, + symSEEnsureFolderExistsAsync: libSymphonySearch.symSE_ensure_folder_exists.async, + symSEGetIndexerAsync: libSymphonySearch.symSE_get_indexer.async, + symSECreatePartialIndexAsync: libSymphonySearch.symSE_create_partial_index.async, + symSEMergePartialIndexAsync: libSymphonySearch.symSE_merge_partial_index.async, + symSEIndexRealTimeAsync: libSymphonySearch.symSE_index_realtime.async, + symSEMergeTempIndexAsync: libSymphonySearch.symSE_merge_temp_index.async, + symSEClearTempIndexAsync: libSymphonySearch.symSE_clear_temp_index.async, + symSESearchAsync: libSymphonySearch.symSE_search.async, + symSEDeleteMessagesAsync: libSymphonySearch.symSE_delete_messages.async, + symSECommitIndexAsync: libSymphonySearch.symSE_commit_index.async, + symSEFreeResultAsync: libSymphonySearch.symSE_free_results.async, + symSEGetLastMessageTimestampAsync: libSymphonySearch.symSE_get_last_message_timestamp.async +}; \ No newline at end of file diff --git a/js/search/searchUtils.js b/js/search/searchUtils.js new file mode 100644 index 00000000..3ab3985f --- /dev/null +++ b/js/search/searchUtils.js @@ -0,0 +1,166 @@ +const fs = require('fs'); +const { checkDiskSpace } = require('./utils/checkDiskSpace.js'); +const searchConfig = require('./searchConfig.js'); +const { isMac } = require('../utils/misc.js'); + +/** + * Utils to validate users config data and + * available disk space to enable electron search + */ +class SearchUtils { + + constructor() { + this.path = searchConfig.FOLDERS_CONSTANTS.USER_DATA_PATH; + } + + /** + * This function returns true if the available disk space + * is more than the constant MINIMUM_DISK_SPACE + * @returns {Promise} + */ + checkFreeSpace() { + return new Promise((resolve, reject) => { + if (!isMac) { + this.path = this.path.substring(0, 2); + } + checkDiskSpace(this.path, function (error, res) { + + if (error) { + return reject(new Error(error)); + } + + return resolve(res >= searchConfig.MINIMUM_DISK_SPACE); + }); + }); + } + + /** + * This function return the user search config + * @param userId + * @returns {Promise} + */ + getSearchUserConfig(userId) { + return new Promise((resolve, reject) => { + readFile.call(this, userId, resolve, reject); + }); + } + + /** + * This function updates the user config file + * with the provided data + * @param userId + * @param data + * @returns {Promise} + */ + updateUserConfig(userId, data) { + return new Promise((resolve, reject) => { + updateConfig.call(this, userId, data, resolve, reject); + }); + } +} + +/** + * This function reads the search user config file and + * return the object + * @param userId + * @param resolve + * @param reject + */ +function readFile(userId, resolve, reject) { + if (fs.existsSync(`${searchConfig.FOLDERS_CONSTANTS.USER_CONFIG_FILE}`)) { + fs.readFile(`${searchConfig.FOLDERS_CONSTANTS.USER_CONFIG_FILE}`, 'utf8', (err, data) => { + if (err) { + return reject(new Error('Error reading the ')) + } + let usersConfig = []; + try { + usersConfig = JSON.parse(data); + } catch (e) { + createUserConfigFile(userId); + return reject('can not parse user config file data: ' + data + ', error: ' + e); + } + if (!usersConfig[userId]) { + createUser(userId, usersConfig); + return reject(null); + } + return resolve(usersConfig[userId]); + }) + } else { + createUserConfigFile(userId); + resolve(null); + } +} + +/** + * If the config has no object for the provided userId this function + * creates an empty object with the key as the userId + * @param userId + * @param oldConfig + */ +function createUser(userId, oldConfig) { + let configPath = searchConfig.FOLDERS_CONSTANTS.USER_CONFIG_FILE; + let newConfig = Object.assign({}, oldConfig); + newConfig[userId] = {}; + + let jsonNewConfig = JSON.stringify(newConfig, null, ' '); + + fs.writeFile(configPath, jsonNewConfig, 'utf8', (err) => { + if (err) { + throw new err; + } + }); +} + +/** + * This function creates the config + * file if not present + * @param userId + * @param data + */ +function createUserConfigFile(userId, data) { + let createStream = fs.createWriteStream(searchConfig.FOLDERS_CONSTANTS.USER_CONFIG_FILE); + if (data) { + createStream.write(`{"${userId}": ${JSON.stringify(data)}}`); + } else { + createStream.write(`{"${userId}": {}}`); + } + createStream.end(); +} + +/** + * Function to update user config data + * @param userId + * @param data + * @param resolve + * @param reject + * @returns {*} + */ +function updateConfig(userId, data, resolve, reject) { + let configPath = searchConfig.FOLDERS_CONSTANTS.USER_CONFIG_FILE; + if (!fs.existsSync(configPath)) { + createUserConfigFile(userId, data); + return reject(null); + } + + let oldConfig; + let oldData = fs.readFileSync(configPath, 'utf8'); + + try { + oldConfig = JSON.parse(oldData); + } catch (e) { + createUserConfigFile(userId, data); + return reject('can not parse user config file data: ' + e); + } + + let newConfig = Object.assign({}, oldConfig); + newConfig[userId] = data; + + let jsonNewConfig = JSON.stringify(newConfig, null, ' '); + + fs.writeFileSync(configPath, jsonNewConfig, 'utf8'); + return resolve(newConfig[userId]); +} + +module.exports = { + SearchUtils: SearchUtils +}; diff --git a/js/search/utils/checkDiskSpace.js b/js/search/utils/checkDiskSpace.js new file mode 100644 index 00000000..08fd297e --- /dev/null +++ b/js/search/utils/checkDiskSpace.js @@ -0,0 +1,44 @@ +const { exec } = require('child_process'); +const { isMac } = require('../../utils/misc'); + +function checkDiskSpace(path, callback) { + if (!path) { + return "Please provide path" + } + + if (isMac) { + exec("df -k '" + path.replace(/'/g,"'\\''") + "'", (error, stdout, stderr) => { + if (error) { + if (stderr.indexOf("No such file or directory") !== -1) { + return callback("No such file or directory : " + error) + } + return callback("Error : " + error) + } + + let data = stdout.trim().split("\n"); + + let disk_info_str = data[data.length - 1].replace( /[\s\n\r]+/g,' '); + let freeSpace = disk_info_str.split(' '); + return callback(null, freeSpace[3] * 1024); + }); + } else { + exec(`fsutil volume diskfree ${path}`, (error, stdout, stderr) => { + if (error) { + if (stderr.indexOf("No such file or directory") !== -1) { + return callback("No such file or directory : " + error) + } + return callback("Error : " + error) + } + let data = stdout.trim().split("\n"); + + let disk_info_str = data[data.length - 1].split(':'); + return callback(null, disk_info_str[1]); + }); + } + + return null; +} + +module.exports = { + checkDiskSpace: checkDiskSpace +}; \ No newline at end of file diff --git a/js/windowMgr.js b/js/windowMgr.js index 4dcaf605..d756c179 100644 --- a/js/windowMgr.js +++ b/js/windowMgr.js @@ -21,6 +21,7 @@ const eventEmitter = require('./eventEmitter'); const throttle = require('./utils/throttle.js'); const { getConfigField, updateConfigField } = require('./config.js'); const { isMac, isNodeEnv } = require('./utils/misc'); +const { deleteIndexFolder } = require('./search/search.js'); const { isWhitelisted } = require('./utils/whitelistHandler'); // show dialog when certificate errors occur @@ -242,7 +243,7 @@ function doCreateMainWindow(initialUrl, initialBounds) { } mainWindow.on('closed', destroyAllWindows); - + // if an user has set a custom downloads directory, // we get that data from the user config file getConfigField('downloadsDirectory') @@ -252,10 +253,10 @@ function doCreateMainWindow(initialUrl, initialBounds) { .catch((error) => { log.send(logLevels.ERROR, 'Could not find the downloads directory config -> ' + error); }); - + // Manage File Downloads mainWindow.webContents.session.on('will-download', (event, item, webContents) => { - + // When download is in progress, send necessary data to indicate the same webContents.send('downloadProgress'); @@ -273,6 +274,14 @@ function doCreateMainWindow(initialUrl, initialBounds) { item.setSavePath(downloadsDirectory + "/" + newFileName); // Send file path to construct the DOM in the UI when the download is complete + + // if the user has set a custom downloads directory, save file to that directory + // if otherwise, we save it to the operating system's default downloads directory + if (downloadsDirectory) { + item.setSavePath(downloadsDirectory + "/" + item.getFilename()); + } + + // Send file path when download is complete item.once('done', (e, state) => { if (state === 'completed') { let data = { @@ -467,6 +476,7 @@ function doCreateMainWindow(initialUrl, initialBounds) { // whenever the main window is navigated for ex: window.location.href or url redirect mainWindow.webContents.on('will-navigate', function(event, navigatedURL) { + deleteIndexFolder(); isWhitelisted(navigatedURL) .catch(() => { event.preventDefault(); diff --git a/package.json b/package.json index a5e3474c..f5f97751 100644 --- a/package.json +++ b/package.json @@ -10,10 +10,12 @@ "dev": "npm run prebuild && cross-env ELECTRON_DEV=true electron .", "demo-win": "npm run prebuild && cross-env ELECTRON_DEV=true electron . --url=file:///demo/index.html", "demo-mac": "npm run prebuild && cross-env ELECTRON_DEV=true electron . --url=file://$(pwd)/demo/index.html", + "search-win": "npm run prebuild && cross-env ELECTRON_DEV=true electron . --url=file:///demo/search.html", + "search-mac": "npm run prebuild && cross-env ELECTRON_DEV=true electron . --url=file://$(pwd)/demo/search.html", "unpacked-mac": "npm run prebuild && npm run test && build --mac --dir", "packed-mac": "npm run unpacked-mac && packagesbuild -v installer/mac/symphony-mac-packager.pkgproj", "unpacked-win": "npm run prebuild && npm run test && build --win --x64 --dir", - "unpacked-win-x86": "npm run prebuild && npm run test && build --win --ia32", + "unpacked-win-x86": "npm run prebuild && npm run test && build --win --ia32 --dir", "prebuild": "npm run rebuild && npm run browserify-preload", "browserify-preload": "browserify -o js/preload/_preloadMain.js -x electron --insert-global-vars=__filename,__dirname js/preload/preloadMain.js --exclude electron-spellchecker", "rebuild": "electron-rebuild -f", @@ -38,7 +40,12 @@ "!node_modules/@paulcbetts/cld/build/deps${/*}", "!node_modules/@paulcbetts/spellchecker/vendor${/*}" ], - "extraFiles": "config/Symphony.config", + "extraFiles": [ + "config/Symphony.config", + "library/libsymphonysearch.dylib", + "library/indexvalidator.exec", + "library/lz4.exec" + ], "appId": "symphony-electron-desktop", "mac": { "target": "dmg", @@ -102,6 +109,7 @@ "electron-log": "^2.2.7", "electron-spellchecker": "^1.1.2", "electron-squirrel-startup": "^1.0.0", + "ffi": "^2.2.0", "filesize": "^3.5.10", "keymirror": "0.1.1", "lodash.difference": "^4.5.0", @@ -109,6 +117,9 @@ "lodash.omit": "^4.5.0", "lodash.pick": "^4.4.0", "parse-domain": "^2.0.0", + "randomstring": "^1.1.5", + "ref": "^1.3.4", + "shell-path": "^2.1.0", "winreg": "^1.2.3" }, "optionalDependencies": {