@mjcctech/meteor-desktop
Version:
Build a Meteor's desktop client with hot code push.
278 lines (256 loc) • 8.56 kB
JavaScript
;module.export({exists:()=>exists,rmWithRetries:()=>rmWithRetries,readDir:()=>readDir,getFileList:()=>getFileList,readAndGetFileHash:()=>readAndGetFileHash,computeHashForHashesSet:()=>computeHashForHashesSet,readAndHashFiles:()=>readAndHashFiles,readFilesAndComputeHash:()=>readFilesAndComputeHash,symlinkExists:()=>symlinkExists});var path;module.link('path',{default(v){path=v}},0);var fs;module.link('fs',{default(v){fs=v}},1);var crypto;module.link('crypto',{default(v){crypto=v}},2);var shell;module.link('shelljs',{default(v){shell=v}},3);/* eslint-disable consistent-return */
/**
* Exists
* @param {string} pathToCheck
* @returns {boolean}
*/
function exists(pathToCheck) {
try {
fs.accessSync(pathToCheck);
return true;
} catch (e) {
return false;
}
}
/**
* Simple wrapper for shelljs.rm with additional retries in case of failure.
* It is useful when something is concurrently reading the dir you want to remove.
*/
function rmWithRetries(...args) {
let retries = 0;
return new Promise((resolve, reject) => {
function rm(...rmArgs) {
try {
shell.config.fatal = true;
shell.rm(...rmArgs);
shell.config.reset();
resolve();
} catch (e) {
retries += 1;
if (retries < 5) {
setTimeout(() => {
rm(...rmArgs);
}, 100);
} else {
shell.config.reset();
reject(e);
}
}
}
rm(...args);
});
}
function readDir(dir, callback) {
if (!callback) {
return new Promise((resolve, reject) => {
readDir(dir, (err, data, stats) => {
if (err) {
reject(err);
} else {
resolve({ data, stats });
}
});
});
}
let list = [];
let allStats = {};
fs.readdir(dir, (err, files) => {
if (err) {
return callback(err);
}
let pending = files.length;
if (!pending) {
return callback(null, list, allStats);
}
files.forEach((file) => {
const filePath = path.join(dir, file);
fs.stat(filePath, (_err, stats) => {
if (_err) {
return callback(_err);
}
if (stats.isDirectory()) {
readDir(filePath, (__err, res, _allStats) => {
if (__err) {
return callback(__err);
}
list = list.concat(res);
allStats = Object.assign(allStats, _allStats);
pending -= 1;
if (!pending) {
return callback(null, list, allStats);
}
});
} else {
list.push(filePath);
allStats[filePath] = {
size: stats.size,
dates: [
stats.birthtime.getTime(),
stats.ctime.getTime(),
stats.mtime.getTime()
]
};
pending -= 1;
if (!pending) {
return callback(null, list, allStats);
}
}
});
});
});
}
/**
* Returns a file list from a directory.
* @param {string} dir - dir path
* @param {boolean} sort - whether to apply sort
* @returns {Promise<Array>}
*/
function getFileList(dir, sort = false) {
return new Promise((resolve, reject) => {
readDir(dir, (error, files) => {
if (error) {
reject(error);
return;
}
// eslint-disable-next-line no-param-reassign
let resultantFilesList;
if (sort) {
const stripLength = (dir.substr(0, 2) === './') ? dir.length - 1 : dir.length + 1;
let pathsUnified = files.map((pth => pth.substr(stripLength).replace(/[\\/]/gm, '-')));
const temporaryIndex = {};
files.forEach((file, i) => {
temporaryIndex[pathsUnified[i]] = file;
});
pathsUnified = pathsUnified.sort();
const filesSorted = [];
pathsUnified.forEach((key) => {
filesSorted.push(temporaryIndex[key]);
});
resultantFilesList = filesSorted;
} else {
resultantFilesList = files;
}
resolve(resultantFilesList);
});
});
}
/**
* Returns file's hash.
* @param {string} file - file path
* @param {boolean} returnFileContents - include file contents in the resultant object
* @returns {Promise<Object>}
*/
function readAndGetFileHash(file, returnFileContents = false) {
return new Promise((resolve, reject) => {
fs.readFile(file, (err, data) => {
if (err) {
reject(err);
return;
}
const hash = crypto.createHash('sha1');
hash.update(data);
const returnObject = { hash: hash.digest('hex') };
if (returnFileContents) {
returnObject.contents = data.toString('utf8');
}
resolve(returnObject);
});
});
}
/**
* Calculates a hash from objects values in specified order.
* @param {Array} orderOfKeys
* @param {Object} hashSet
* @param {Function} keyFilter
* @returns {string}
*/
function computeHashForHashesSet(orderOfKeys, hashSet, keyFilter = key => key) {
const hash = crypto.createHash('sha1');
const hashesJoined = orderOfKeys.reduce(
// eslint-disable-next-line no-param-reassign,no-return-assign
(tmpHash, key) => (tmpHash += hashSet[keyFilter(key)], tmpHash), ''
);
hash.update(hashesJoined);
return hash.digest('hex');
}
/**
* Reads files from disk and computes hashes for them.
* @param {Array} files - array with file paths
* @returns {Promise<Object>}
*/
function readAndHashFiles(files, fileFilter) {
const fileHashes = {};
const fileContents = {};
const promises = [];
function readSingleFile(file) {
return new Promise((resolve, reject) => {
readAndGetFileHash(file, file.endsWith('.js') && !file.endsWith('.test.js'))
.then((result) => {
let fileName = file;
if (fileFilter) {
fileName = fileFilter(fileName);
}
fileHashes[fileName] = result.hash;
if (result.contents) {
fileContents[fileName] = result.contents;
}
resolve();
})
.catch(reject);
});
}
files.forEach((file) => {
promises.push(readSingleFile(file));
});
return new Promise((resolve, reject) => {
Promise.all(promises)
.then(() => {
resolve({ files, fileContents, fileHashes });
})
.catch(reject);
});
}
/**
* Reads files from .desktop and computes a version hash.
*
* @param {string} dir - path
* @param {Function} fileFilter
* @returns {Promise<Object>}
*/
function readFilesAndComputeHash(dir, fileFilter) {
return new Promise((resolve, reject) => {
getFileList(dir, true)
.catch(reject)
.then(files => readAndHashFiles(files, fileFilter))
.catch(reject)
.then((result) => {
// eslint-disable-next-line no-param-reassign
result.hash = computeHashForHashesSet(result.files, result.fileHashes, fileFilter);
resolve(result);
});
});
}
/**
* Symlink exists
* @param {string} pathToCheck
* @returns {boolean}
*/
function symlinkExists(pathToCheck) {
try {
fs.readlinkSync(pathToCheck);
return true;
} catch (e) {
return false;
}
}
module.exportDefault({
getFileList,
rmWithRetries,
exists,
readDir,
readAndGetFileHash,
computeHashForHashesSet,
readAndHashFiles,
readFilesAndComputeHash,
symlinkExists
});