mirror of
https://github.com/finos/SymphonyElectron.git
synced 2024-11-30 12:43:59 -06:00
Merge pull request #277 from VikasShashidhar/master
Revert "Merge pull request #269 from keerthi16/skynet"
This commit is contained in:
commit
f4ff287547
284
demo/search.html
Normal file
284
demo/search.html
Normal file
@ -0,0 +1,284 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Search</title>
|
||||
<style>
|
||||
table, th, td {
|
||||
border: 1px solid black;
|
||||
border-collapse: collapse;
|
||||
padding: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
input {
|
||||
padding: 5px;
|
||||
margin: 2px 0;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
label {
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 10px;
|
||||
background-color: rgba(0, 64, 98, 0.7);
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div style="padding: 20px;">
|
||||
<h1>Symphony Electron Search API Demo</h1>
|
||||
<div>
|
||||
<p>Search</p>
|
||||
</div>
|
||||
<div>
|
||||
<label for="query">Query:</label><input id="query" size=60>
|
||||
</div>
|
||||
<br>
|
||||
<div>
|
||||
<label for="has">Has:</label><input id="has">
|
||||
</div>
|
||||
<br>
|
||||
<div>
|
||||
<label for="offset">Offset:</label><input id="offset" type="number" value="0" size=5>
|
||||
<label for="limit">Limit:</label><input id="limit" type="number" value="25" size=5>
|
||||
</div>
|
||||
<br>
|
||||
<div>
|
||||
<label for="start">Start:</label><input id="start" type="date">
|
||||
<label for="end">End:</label><input id="end" type="date">
|
||||
</div>
|
||||
<br>
|
||||
<div>
|
||||
<label for="senderId">SenderId:</label><input id="senderId" placeholder='["abc", "123"]'>
|
||||
</div>
|
||||
<br>
|
||||
<div>
|
||||
<label for="threadId">ThreadId:</label><input id="threadId" placeholder='["abc", "123"]'>
|
||||
</div>
|
||||
<br>
|
||||
<div>
|
||||
<button id='search'>Search</button>
|
||||
</div>
|
||||
<br>
|
||||
<div>
|
||||
<label for="var1">var1:</label><input id="var1" type="number" value="0" size=5>
|
||||
</div>
|
||||
<br>
|
||||
<div>
|
||||
<label for="realTimeIndexing">Real Time Indexing:</label><input placeholder="Pass array of messages:"
|
||||
id="realTimeIndexing">
|
||||
<button id='sendMessage'>Send</button>
|
||||
</div>
|
||||
<br>
|
||||
<div>
|
||||
<label for="batchNumber">Batch Number: </label><input placeholder="Ex: batch1, batch2" id="batchNumber">
|
||||
<button id='index'>Index Messages</button>
|
||||
<button id='merge'>Merge</button>
|
||||
</div>
|
||||
<br>
|
||||
<div>
|
||||
<label for="batchNumber">Get Latest Message Timestamp</label>
|
||||
<button id='getLatestMessageTimestamp'>Click</button>
|
||||
</div>
|
||||
<br>
|
||||
<div>
|
||||
<label>Check Free Space: </label>
|
||||
<button id='checkFreeSpace'>Click</button>
|
||||
</div>
|
||||
<br>
|
||||
<div>
|
||||
<label>View User Config</label>
|
||||
<button id='viewUserConfig'>Click</button>
|
||||
</div>
|
||||
<br>
|
||||
<div>
|
||||
<div>
|
||||
<label>Update User Config</label>
|
||||
</div>
|
||||
<br>
|
||||
<label for="rotationId">Rotation Id: </label><input placeholder="Ex: 0, 1" id="rotationId">
|
||||
<br>
|
||||
<label for="version">Version: </label><input placeholder="Ex: 1, 2" id="version">
|
||||
<br>
|
||||
<button id='updateUserConfig'>Save</button>
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
<p>Results:</p>
|
||||
<p id="results"></p>
|
||||
<table id="table" class="hidden" style="width:100%">
|
||||
<tr>
|
||||
<th>ThreadId</th>
|
||||
<th>SenderId</th>
|
||||
<th>Text</th>
|
||||
</tr>
|
||||
</table>
|
||||
<br>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var search = new ssf.Search("12345678910112", "jjjehdnctsjyieoalskcjdhsnahsadndfnusdfsdfsd=");
|
||||
var searchUtils = new ssf.SearchUtils();
|
||||
var buttonEl = document.getElementById('search');
|
||||
var merge = document.getElementById('merge');
|
||||
var buttonIndex = document.getElementById('index');
|
||||
var queryEl = document.getElementById('query');
|
||||
var offsetEl = document.getElementById('offset');
|
||||
var limitEl = document.getElementById('limit');
|
||||
var startEl = document.getElementById('start');
|
||||
var endEl = document.getElementById('end');
|
||||
var resultsEl = document.getElementById('results');
|
||||
var senderIdEl = document.getElementById('senderId');
|
||||
var threadIdEl = document.getElementById('threadId');
|
||||
var var1El = document.getElementById('var1');
|
||||
var table = document.getElementById('table');
|
||||
var sendMessage = document.getElementById('sendMessage');
|
||||
var realTimeIndexing = document.getElementById('realTimeIndexing');
|
||||
var batchNumber = document.getElementById('batchNumber');
|
||||
var timestamp = document.getElementById('getLatestMessageTimestamp');
|
||||
var has = document.getElementById('has');
|
||||
var checkFreeSpace = document.getElementById('checkFreeSpace');
|
||||
var viewUserConfig = document.getElementById('viewUserConfig');
|
||||
var version = document.getElementById('version');
|
||||
var rotationId = document.getElementById('rotationId');
|
||||
var updateUserConfig = document.getElementById('updateUserConfig');
|
||||
|
||||
|
||||
buttonIndex.addEventListener('click', function () {
|
||||
let batchIndex = batchNumber.value;
|
||||
search.readJson(batchIndex).then(function (res) {
|
||||
search.indexBatch(JSON.stringify(res)).then(function () {
|
||||
resultsEl.innerHTML = "Index created";
|
||||
});
|
||||
}).catch(function (err) {
|
||||
console.log(err);
|
||||
});
|
||||
});
|
||||
|
||||
buttonEl.addEventListener('click', function () {
|
||||
if (!search.isLibInit()) {
|
||||
search.init();
|
||||
}
|
||||
let out;
|
||||
table.innerHTML = '';
|
||||
table.classList.remove('hidden');
|
||||
let startDate = new Date(startEl.value);
|
||||
let endDate = new Date(endEl.value);
|
||||
let threadIdObj, senderIdObj;
|
||||
if (senderIdEl.value && senderIdEl.value !== "" && senderIdEl.value.replace(/ /g, "").length > 0) {
|
||||
senderIdObj = JSON.parse(senderIdEl.value);
|
||||
}
|
||||
if (threadIdEl.value && threadIdEl.value !== "" && threadIdEl.value.replace(/ /g, "").length > 0) {
|
||||
threadIdObj = JSON.parse(threadIdEl.value);
|
||||
}
|
||||
let _has = has.value || null;
|
||||
search.searchQuery(queryEl.value, senderIdObj, threadIdObj, _has, startDate, endDate, limitEl.value, offsetEl.value, 0).then(function (result) {
|
||||
if (result.messages.length < 1) {
|
||||
resultsEl.innerHTML = "No results found"
|
||||
}
|
||||
if (result.messages.length > 0) {
|
||||
out = result;
|
||||
var th = document.createElement('tr');
|
||||
var th1 = document.createElement('td');
|
||||
th1.innerText = "ThreadId";
|
||||
var th2 = document.createElement('td');
|
||||
th2.innerText = 'SenderId';
|
||||
var th3 = document.createElement('td');
|
||||
th3.innerText = 'Text';
|
||||
th.appendChild(th1);
|
||||
th.appendChild(th2);
|
||||
th.appendChild(th3);
|
||||
table.appendChild(th);
|
||||
out.messages.forEach(function (msg) {
|
||||
var tr = document.createElement('tr');
|
||||
var t1 = document.createElement('td');
|
||||
t1.innerText = msg.threadId;
|
||||
var t2 = document.createElement('td');
|
||||
t2.innerText = msg.senderId;
|
||||
var t3 = document.createElement('td');
|
||||
t3.innerText = msg.text;
|
||||
tr.appendChild(t1);
|
||||
tr.appendChild(t2);
|
||||
tr.appendChild(t3);
|
||||
table.appendChild(tr);
|
||||
});
|
||||
}
|
||||
}).catch(function (err) {
|
||||
resultsEl.innerHTML = 'Error: ' + err;
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
sendMessage.addEventListener('click', function () {
|
||||
search.deleteRealTimeFolder();
|
||||
if (realTimeIndexing.value !== "") {
|
||||
let message = realTimeIndexing.value;
|
||||
search.batchRealTimeIndexing(JSON.parse(message));
|
||||
resultsEl.innerHTML = 'success';
|
||||
} else {
|
||||
resultsEl.innerHTML = "Please check the entered value"
|
||||
}
|
||||
});
|
||||
|
||||
merge.addEventListener('click', function () {
|
||||
search.mergeIndexBatches().then(function () {
|
||||
search.encryptIndex('jjjehdnctsjyieoalskcjdhsnahsadndfnusdfsdfsd=').then(function () {
|
||||
searchUtils.updateUserConfig(12345678910112, {rotationId:0, version: 1}).then(function (res) {
|
||||
resultsEl.innerHTML = JSON.stringify(res);
|
||||
}).catch(function (err) {
|
||||
resultsEl.innerHTML = JSON.stringify(err);
|
||||
});
|
||||
});
|
||||
}).catch(function (err) {
|
||||
resultsEl.innerHTML = 'Error: ' + err;
|
||||
});
|
||||
});
|
||||
|
||||
timestamp.addEventListener('click', function () {
|
||||
search.getLatestMessageTimestamp().then(function (res) {
|
||||
resultsEl.innerHTML = res;
|
||||
}).catch(function (err) {
|
||||
resultsEl.innerHTML = 'Error: ' + err;
|
||||
});
|
||||
});
|
||||
|
||||
checkFreeSpace.addEventListener('click', function () {
|
||||
searchUtils.checkFreeSpace().then(function (res) {
|
||||
resultsEl.innerHTML = res ? "Free Disk Space Available" : "Insufficient Disk Space";
|
||||
}).catch(function (err) {
|
||||
resultsEl.innerHTML = err;
|
||||
});
|
||||
});
|
||||
|
||||
viewUserConfig.addEventListener('click', function () {
|
||||
searchUtils.getSearchUserConfig(12345678910112).then(function (res) {
|
||||
resultsEl.innerHTML = JSON.stringify(res);
|
||||
}).catch(function (err) {
|
||||
resultsEl.innerHTML = JSON.stringify(err);
|
||||
});
|
||||
});
|
||||
|
||||
updateUserConfig.addEventListener('click', function () {
|
||||
var data = {
|
||||
rotationId: rotationId.value || 0,
|
||||
version: version.value || 1
|
||||
};
|
||||
searchUtils.updateUserConfig(12345678910112, data).then(function (res) {
|
||||
resultsEl.innerHTML = JSON.stringify(res);
|
||||
}).catch(function (err) {
|
||||
resultsEl.innerHTML = JSON.stringify(err);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -58,6 +58,7 @@
|
||||
<ROW Directory="config_Dir" Directory_Parent="APPDIR" DefaultDir="config"/>
|
||||
<ROW Directory="jobber_Dir" Directory_Parent="vendor_Dir" DefaultDir="jobber"/>
|
||||
<ROW Directory="lib_Dir" Directory_Parent="spawnrx_Dir" DefaultDir="lib"/>
|
||||
<ROW Directory="library_Dir" Directory_Parent="APPDIR" DefaultDir="library"/>
|
||||
<ROW Directory="locales_Dir" Directory_Parent="APPDIR" DefaultDir="locales"/>
|
||||
<ROW Directory="node_modules_Dir" Directory_Parent="app.asar.unpacked_Dir" DefaultDir="NODE_M~1|node_modules"/>
|
||||
<ROW Directory="paulcbetts_Dir" Directory_Parent="node_modules_Dir" DefaultDir="@PAULC~1|@paulcbetts"/>
|
||||
@ -126,18 +127,25 @@
|
||||
<ROW Component="ffmpeg.dll" ComponentId="{A1C4A332-3490-44D8-A5C9-9523889B488B}" Directory_="APPDIR" Attributes="256" KeyPath="ffmpeg.dll"/>
|
||||
<ROW Component="index.d.ts" ComponentId="{74D261F1-A6C5-49DD-8554-E48337CC04AF}" Directory_="src_Dir" Attributes="0" KeyPath="index.d.ts" Type="0"/>
|
||||
<ROW Component="index.js" ComponentId="{77C48E00-B684-4E72-ACF4-15DD0253EF43}" Directory_="lib_Dir" Attributes="0" KeyPath="index.js" Type="0"/>
|
||||
<ROW Component="indexvalidatorx64.exe" ComponentId="{0266F3CF-5462-4381-9971-2353034D7E1D}" Directory_="library_Dir" Attributes="256" KeyPath="indexvalidatorx64.exe"/>
|
||||
<ROW Component="indexvalidatorx86.exe" ComponentId="{DE7AEDE9-CF30-4DB1-BC38-6CA6F1CDCAAC}" Directory_="library_Dir" Attributes="0" KeyPath="indexvalidatorx86.exe"/>
|
||||
<ROW Component="libEGL.dll" ComponentId="{8EEC76AB-3601-4D11-B13E-32EC2A38C539}" Directory_="APPDIR" Attributes="256" KeyPath="libEGL.dll"/>
|
||||
<ROW Component="libGLESv2.dll" ComponentId="{0E8B8B21-B4C0-45C9-95D3-637FD93A4EC0}" Directory_="APPDIR" Attributes="256" KeyPath="libGLESv2.dll"/>
|
||||
<ROW Component="libsymphonysearchx64.dll" ComponentId="{A8C99D17-FA62-4996-8FAE-52D1DCF9BF26}" Directory_="library_Dir" Attributes="256" KeyPath="libsymphonysearchx64.dll"/>
|
||||
<ROW Component="libsymphonysearchx86.dll" ComponentId="{AB1E061B-1558-4A57-B4C3-C2952E7471B2}" Directory_="library_Dir" Attributes="0" KeyPath="libsymphonysearchx86.dll"/>
|
||||
<ROW Component="lz4winx64.exe" ComponentId="{8B78B313-EAE9-4533-AFEB-56F9E0CA73A1}" Directory_="library_Dir" Attributes="256" KeyPath="lz4winx64.exe"/>
|
||||
<ROW Component="lz4winx86.exe" ComponentId="{907532E8-1E4B-48A5-B48B-7B05BE80D1A1}" Directory_="library_Dir" Attributes="0" KeyPath="lz4winx86.exe"/>
|
||||
<ROW Component="msvcp140.dll" ComponentId="{93A6289C-CF23-4BB8-A579-7FDDD1D15591}" Directory_="APPDIR" Attributes="256" KeyPath="msvcp140.dll"/>
|
||||
<ROW Component="node.dll" ComponentId="{C0972355-339E-438C-94A3-74174DE4C6B6}" Directory_="APPDIR" Attributes="256" KeyPath="node.dll"/>
|
||||
<ROW Component="node_modules" ComponentId="{A4EB33A8-FEA8-40A5-94EF-705EBE64DDC1}" Directory_="node_modules_Dir" Attributes="0"/>
|
||||
<ROW Component="npmignore" ComponentId="{849CFFE2-EBC3-4430-AC8B-DEE1B66DF589}" Directory_="spawnrx_Dir" Attributes="0" KeyPath="npmignore" Type="0"/>
|
||||
<ROW Component="tarwin.exe" ComponentId="{4C98F3B1-1A73-4761-86C0-DE0FC18A8800}" Directory_="library_Dir" Attributes="0" KeyPath="tarwin.exe"/>
|
||||
<ROW Component="ucrtbase.dll" ComponentId="{16D802A3-DAD4-4BF4-AD64-88D6F63F5D1E}" Directory_="APPDIR" Attributes="256" KeyPath="ucrtbase.dll"/>
|
||||
<ROW Component="vcruntime140.dll" ComponentId="{2542FC82-8D71-4351-8514-2C0D12772ED5}" Directory_="APPDIR" Attributes="256" KeyPath="vcruntime140.dll"/>
|
||||
</COMPONENT>
|
||||
<COMPONENT cid="caphyon.advinst.msicomp.MsiFeatsComponent">
|
||||
<ROW Feature="D564007E3BBE4F85950A09B470A7CA65" Title="Visual C++ Redistributable for Visual Studio 2013 x86" Description="Visual C++ Redistributable for Visual Studio 2013 x86" Display="3" Level="1" Attributes="0"/>
|
||||
<ROW Feature="MainFeature" Title="MainFeature" Description="Description" Display="1" Level="1" Directory_="APPDIR" Attributes="0" Components="AI_CustomARPName AI_DisableModify Jobber.exe PodUrl ProductInformation ScreenSnippet.exe Symphony Symphony.config Symphony.exe am.pak ambient.d.ts apimswincoreconsolel110.dll apimswincoredatetimel110.dll apimswincoredebugl110.dll apimswincoreerrorhandlingl110.dll apimswincorefilel110.dll apimswincorefilel120.dll apimswincorefilel210.dll apimswincorehandlel110.dll apimswincoreheapl110.dll apimswincoreinterlockedl110.dll apimswincorelibraryloaderl110.dll apimswincorelocalizationl120.dll apimswincorememoryl110.dll apimswincorenamedpipel110.dll apimswincoreprocessenvironmentl110.dll apimswincoreprocessthreadsl110.dll apimswincoreprocessthreadsl111.dll apimswincoreprofilel110.dll apimswincorertlsupportl110.dll apimswincorestringl110.dll apimswincoresynchl110.dll apimswincoresynchl120.dll apimswincoresysinfol110.dll apimswincoretimezonel110.dll apimswincoreutill110.dll apimswincrtconiol110.dll apimswincrtconvertl110.dll apimswincrtenvironmentl110.dll apimswincrtfilesysteml110.dll apimswincrtheapl110.dll apimswincrtlocalel110.dll apimswincrtmathl110.dll apimswincrtmultibytel110.dll apimswincrtprivatel110.dll apimswincrtprocessl110.dll apimswincrtruntimel110.dll apimswincrtstdiol110.dll apimswincrtstringl110.dll apimswincrttimel110.dll apimswincrtutilityl110.dll appupdate.yml blink_image_resources_200_percent.pak cld.node d3dcompiler_47.dll ffmpeg.dll index.d.ts index.js libEGL.dll libGLESv2.dll msvcp140.dll node.dll node_modules npmignore ucrtbase.dll vcruntime140.dll"/>
|
||||
<ROW Feature="MainFeature" Title="MainFeature" Description="Description" Display="1" Level="1" Directory_="APPDIR" Attributes="0" Components="AI_CustomARPName AI_DisableModify Jobber.exe PodUrl ProductInformation ScreenSnippet.exe Symphony Symphony.config Symphony.exe am.pak ambient.d.ts apimswincoreconsolel110.dll apimswincoredatetimel110.dll apimswincoredebugl110.dll apimswincoreerrorhandlingl110.dll apimswincorefilel110.dll apimswincorefilel120.dll apimswincorefilel210.dll apimswincorehandlel110.dll apimswincoreheapl110.dll apimswincoreinterlockedl110.dll apimswincorelibraryloaderl110.dll apimswincorelocalizationl120.dll apimswincorememoryl110.dll apimswincorenamedpipel110.dll apimswincoreprocessenvironmentl110.dll apimswincoreprocessthreadsl110.dll apimswincoreprocessthreadsl111.dll apimswincoreprofilel110.dll apimswincorertlsupportl110.dll apimswincorestringl110.dll apimswincoresynchl110.dll apimswincoresynchl120.dll apimswincoresysinfol110.dll apimswincoretimezonel110.dll apimswincoreutill110.dll apimswincrtconiol110.dll apimswincrtconvertl110.dll apimswincrtenvironmentl110.dll apimswincrtfilesysteml110.dll apimswincrtheapl110.dll apimswincrtlocalel110.dll apimswincrtmathl110.dll apimswincrtmultibytel110.dll apimswincrtprivatel110.dll apimswincrtprocessl110.dll apimswincrtruntimel110.dll apimswincrtstdiol110.dll apimswincrtstringl110.dll apimswincrttimel110.dll apimswincrtutilityl110.dll appupdate.yml blink_image_resources_200_percent.pak cld.node d3dcompiler_47.dll ffmpeg.dll index.d.ts index.js indexvalidatorx64.exe indexvalidatorx86.exe libEGL.dll libGLESv2.dll libsymphonysearchx64.dll libsymphonysearchx86.dll lz4winx64.exe lz4winx86.exe msvcp140.dll node.dll node_modules npmignore tarwin.exe ucrtbase.dll vcruntime140.dll"/>
|
||||
<ATTRIBUTE name="CurrentFeature" value="MainFeature"/>
|
||||
</COMPONENT>
|
||||
<COMPONENT cid="caphyon.advinst.msicomp.MsiFilesComponent">
|
||||
@ -200,7 +208,7 @@
|
||||
<ROW File="build.cmd" Component_="npmignore" FileName="build.cmd" Attributes="0" SourcePath="..\..\dist\win-unpacked\resources\app.asar.unpacked\node_modules\spawn-rx\build.cmd" SelfReg="false" NextFile="build.sh"/>
|
||||
<ROW File="build.sh" Component_="npmignore" FileName="build.sh" Attributes="0" SourcePath="..\..\dist\win-unpacked\resources\app.asar.unpacked\node_modules\spawn-rx\build.sh" SelfReg="false" NextFile="CODE_OF_CONDUCT.md"/>
|
||||
<ROW File="ca.pak" Component_="am.pak" FileName="ca.pak" Attributes="0" SourcePath="..\..\dist\win-unpacked\locales\ca.pak" SelfReg="false" NextFile="cs.pak"/>
|
||||
<ROW File="cld.node" Component_="cld.node" FileName="CLD~1.NOD|cld.node" Attributes="0" SourcePath="..\..\dist\win-unpacked\resources\app.asar.unpacked\node_modules\@paulcbetts\cld\build\Release\cld.node" SelfReg="false"/>
|
||||
<ROW File="cld.node" Component_="cld.node" FileName="CLD~1.NOD|cld.node" Attributes="0" SourcePath="..\..\dist\win-unpacked\resources\app.asar.unpacked\node_modules\@paulcbetts\cld\build\Release\cld.node" SelfReg="false" NextFile="indexvalidatorx64.exe"/>
|
||||
<ROW File="content_resources_200_percent.pak" Component_="blink_image_resources_200_percent.pak" FileName="CONTEN~1.PAK|content_resources_200_percent.pak" Attributes="0" SourcePath="..\..\dist\win-unpacked\content_resources_200_percent.pak" SelfReg="false" NextFile="content_shell.pak"/>
|
||||
<ROW File="content_shell.pak" Component_="blink_image_resources_200_percent.pak" FileName="CONTEN~2.PAK|content_shell.pak" Attributes="0" SourcePath="..\..\dist\win-unpacked\content_shell.pak" SelfReg="false" NextFile="d3dcompiler_47.dll"/>
|
||||
<ROW File="cs.pak" Component_="am.pak" FileName="cs.pak" Attributes="0" SourcePath="..\..\dist\win-unpacked\locales\cs.pak" SelfReg="false" NextFile="da.pak"/>
|
||||
@ -233,14 +241,20 @@
|
||||
<ROW File="index.js.map" Component_="index.d.ts" FileName="INDEXJ~1.MAP|index.js.map" Attributes="0" SourcePath="..\..\dist\win-unpacked\resources\app.asar.unpacked\node_modules\spawn-rx\lib\src\index.js.map" SelfReg="false" NextFile="package.json"/>
|
||||
<ROW File="index.js_1" Component_="index.d.ts" FileName="index.js" Attributes="0" SourcePath="..\..\dist\win-unpacked\resources\app.asar.unpacked\node_modules\spawn-rx\lib\src\index.js" SelfReg="false" NextFile="index.js.map"/>
|
||||
<ROW File="index.ts" Component_="ambient.d.ts" FileName="index.ts" Attributes="0" SourcePath="..\..\dist\win-unpacked\resources\app.asar.unpacked\node_modules\spawn-rx\src\index.ts" SelfReg="false" NextFile="tsconfig.json"/>
|
||||
<ROW File="indexvalidatorx64.exe" Component_="indexvalidatorx64.exe" FileName="INDEXV~1.EXE|indexvalidator-x64.exe" Attributes="0" SourcePath="..\..\library\indexvalidator-x64.exe" SelfReg="false" NextFile="indexvalidatorx86.exe" DigSign="true"/>
|
||||
<ROW File="indexvalidatorx86.exe" Component_="indexvalidatorx86.exe" FileName="INDEXV~2.EXE|indexvalidator-x86.exe" Attributes="0" SourcePath="..\..\library\indexvalidator-x86.exe" SelfReg="false" NextFile="libsymphonysearchx64.dll" DigSign="true"/>
|
||||
<ROW File="it.pak" Component_="am.pak" FileName="it.pak" Attributes="0" SourcePath="..\..\dist\win-unpacked\locales\it.pak" SelfReg="false" NextFile="ja.pak"/>
|
||||
<ROW File="ja.pak" Component_="am.pak" FileName="ja.pak" Attributes="0" SourcePath="..\..\dist\win-unpacked\locales\ja.pak" SelfReg="false" NextFile="kn.pak"/>
|
||||
<ROW File="kn.pak" Component_="am.pak" FileName="kn.pak" Attributes="0" SourcePath="..\..\dist\win-unpacked\locales\kn.pak" SelfReg="false" NextFile="ko.pak"/>
|
||||
<ROW File="ko.pak" Component_="am.pak" FileName="ko.pak" Attributes="0" SourcePath="..\..\dist\win-unpacked\locales\ko.pak" SelfReg="false" NextFile="lt.pak"/>
|
||||
<ROW File="libEGL.dll" Component_="libEGL.dll" FileName="libEGL.dll" Attributes="0" SourcePath="..\..\dist\win-unpacked\libEGL.dll" SelfReg="false" NextFile="libGLESv2.dll"/>
|
||||
<ROW File="libGLESv2.dll" Component_="libGLESv2.dll" FileName="LIBGLE~1.DLL|libGLESv2.dll" Attributes="0" SourcePath="..\..\dist\win-unpacked\libGLESv2.dll" SelfReg="false" NextFile="LICENSE.electron.txt"/>
|
||||
<ROW File="libsymphonysearchx64.dll" Component_="libsymphonysearchx64.dll" FileName="LIBSYM~1.DLL|libsymphonysearch-x64.dll" Attributes="0" SourcePath="..\..\library\libsymphonysearch-x64.dll" SelfReg="false" NextFile="libsymphonysearchx86.dll"/>
|
||||
<ROW File="libsymphonysearchx86.dll" Component_="libsymphonysearchx86.dll" FileName="LIBSYM~2.DLL|libsymphonysearch-x86.dll" Attributes="0" SourcePath="..\..\library\libsymphonysearch-x86.dll" SelfReg="false" NextFile="lz4winx64.exe"/>
|
||||
<ROW File="lt.pak" Component_="am.pak" FileName="lt.pak" Attributes="0" SourcePath="..\..\dist\win-unpacked\locales\lt.pak" SelfReg="false" NextFile="lv.pak"/>
|
||||
<ROW File="lv.pak" Component_="am.pak" FileName="lv.pak" Attributes="0" SourcePath="..\..\dist\win-unpacked\locales\lv.pak" SelfReg="false" NextFile="ml.pak"/>
|
||||
<ROW File="lz4winx64.exe" Component_="lz4winx64.exe" FileName="LZ4-WI~1.EXE|lz4-win-x64.exe" Attributes="0" SourcePath="..\..\library\lz4-win-x64.exe" SelfReg="false" NextFile="lz4winx86.exe" DigSign="true"/>
|
||||
<ROW File="lz4winx86.exe" Component_="lz4winx86.exe" FileName="LZ4-WI~2.EXE|lz4-win-x86.exe" Attributes="0" SourcePath="..\..\library\lz4-win-x86.exe" SelfReg="false" NextFile="tarwin.exe" DigSign="true"/>
|
||||
<ROW File="ml.pak" Component_="am.pak" FileName="ml.pak" Attributes="0" SourcePath="..\..\dist\win-unpacked\locales\ml.pak" SelfReg="false" NextFile="mr.pak"/>
|
||||
<ROW File="mr.pak" Component_="am.pak" FileName="mr.pak" Attributes="0" SourcePath="..\..\dist\win-unpacked\locales\mr.pak" SelfReg="false" NextFile="ms.pak"/>
|
||||
<ROW File="ms.pak" Component_="am.pak" FileName="ms.pak" Attributes="0" SourcePath="..\..\dist\win-unpacked\locales\ms.pak" SelfReg="false" NextFile="nb.pak"/>
|
||||
@ -263,6 +277,7 @@
|
||||
<ROW File="sv.pak" Component_="am.pak" FileName="sv.pak" Attributes="0" SourcePath="..\..\dist\win-unpacked\locales\sv.pak" SelfReg="false" NextFile="sw.pak"/>
|
||||
<ROW File="sw.pak" Component_="am.pak" FileName="sw.pak" Attributes="0" SourcePath="..\..\dist\win-unpacked\locales\sw.pak" SelfReg="false" NextFile="ta.pak"/>
|
||||
<ROW File="ta.pak" Component_="am.pak" FileName="ta.pak" Attributes="0" SourcePath="..\..\dist\win-unpacked\locales\ta.pak" SelfReg="false" NextFile="te.pak"/>
|
||||
<ROW File="tarwin.exe" Component_="tarwin.exe" FileName="tar-win.exe" Attributes="0" SourcePath="..\..\library\tar-win.exe" SelfReg="false" DigSign="true"/>
|
||||
<ROW File="te.pak" Component_="am.pak" FileName="te.pak" Attributes="0" SourcePath="..\..\dist\win-unpacked\locales\te.pak" SelfReg="false" NextFile="th.pak"/>
|
||||
<ROW File="th.pak" Component_="am.pak" FileName="th.pak" Attributes="0" SourcePath="..\..\dist\win-unpacked\locales\th.pak" SelfReg="false" NextFile="tr.pak"/>
|
||||
<ROW File="tr.pak" Component_="am.pak" FileName="tr.pak" Attributes="0" SourcePath="..\..\dist\win-unpacked\locales\tr.pak" SelfReg="false" NextFile="uk.pak"/>
|
||||
|
74
js/compressionLib/index.js
Normal file
74
js/compressionLib/index.js
Normal file
@ -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
|
||||
};
|
192
js/cryptoLib/crypto.js
Normal file
192
js/cryptoLib/crypto.js
Normal file
@ -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);
|
||||
}
|
125
js/cryptoLib/index.js
Normal file
125
js/cryptoLib/index.js
Normal file
@ -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;
|
25
js/main.js
25
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
|
||||
|
@ -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'
|
||||
|
39
js/search/queue.js
Normal file
39
js/search/queue.js
Normal file
@ -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;
|
583
js/search/search.js
Normal file
583
js/search/search.js
Normal file
@ -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
|
||||
};
|
63
js/search/searchConfig.js
Normal file
63
js/search/searchConfig.js
Normal file
@ -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;
|
73
js/search/searchLibrary.js
Normal file
73
js/search/searchLibrary.js
Normal file
@ -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
|
||||
};
|
166
js/search/searchUtils.js
Normal file
166
js/search/searchUtils.js
Normal file
@ -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<boolean>}
|
||||
*/
|
||||
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<object>}
|
||||
*/
|
||||
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<object>}
|
||||
*/
|
||||
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
|
||||
};
|
44
js/search/utils/checkDiskSpace.js
Normal file
44
js/search/utils/checkDiskSpace.js
Normal file
@ -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
|
||||
};
|
@ -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();
|
||||
|
15
package.json
15
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": {
|
||||
|
Loading…
Reference in New Issue
Block a user