carbone
Version:
Fast, Simple and Powerful report generator. Injects JSON and produces PDF, DOCX, XLSX, ODT, PPTX, ODS, ...!
456 lines (415 loc) • 14.5 kB
JavaScript
var fs = require('fs');
var path = require('path');
var assert = require('assert');
var crypto = require('crypto');
var params = require('./params');
var exec = require('child_process').exec;
var debug = require('debug')('carbone:helper');
var sameUIDCounter = 0;
var prevTimestamp = 0;
// Random values to pre-allocate
const randomPool = Buffer.alloc(256);
var randomPoolPointer = randomPool.length;
var helper = {
/**
* This factor is used to modify asserts in performance unit tests.
* If those tests are executed on slow machines, this factor is higher
* to reduce required level of performance in the test.
*/
CPU_PERFORMANCE_FACTOR : 1.0,
/**
* Generate a unique id
* @return {String} unique id
*/
getUID : function () {
var _timestamp = Date.now();
var _uid = params.uidPrefix + '_' +_timestamp + '_' + process.pid + '_';
if (_timestamp === prevTimestamp) {
_uid += ++sameUIDCounter;
}
else {
_uid += '0';
sameUIDCounter = 0;
}
prevTimestamp = _timestamp;
return _uid;
},
/**
* Generate a constant-length base64 random string without forbidden characters for filesystem
*
* [/+] are replaced by [-_]
*
* Length : 22 characters
* It is 5 times slower than getUID above but a lot more secure to generate public filename
*
* It uses Cryptographically Secure Pseudo-Random Number Generator, with unbiased (ie secure) tranformation
* Same collision probability as standard 128 bits uuid-v4
* Doc: https://gist.github.com/joepie91/7105003c3b26e65efcea63f3db82dfba
*
* @return {String} The random string
*/
RANDOM_STRING_LENGTH : 22,
getRandomString : function () {
if (randomPoolPointer > randomPool.length - 16) {
// It is alot simpler to use sync version
// Otherwose, we must maintain a table of callback
// This sync function is called 1/16 times
crypto.randomFillSync(randomPool);
randomPoolPointer = 0;
}
let _randomStr = randomPool.toString('base64', randomPoolPointer, (randomPoolPointer += 16));
return _randomStr.slice(0, -2).replace(/\//g,'_').replace(/\+/g,'-');
},
/**
* Encode any string to a base64 string, safe for generating POSIX compatible filenames
* https://fr.wikipedia.org/wiki/Base64
*
* @param {String} str The string
* @return {String} A safe filename
*/
encodeSafeFilename : function (str) {
return Buffer.from(str||'').toString('base64').replace(/=/g,'').replace(/\//g,'_').replace(/\+/g,'-');
},
/**
* Decode a base64 safe filename to the original string
*
* @param {String} filename The filename
* @return {String} Return original string
*/
decodeSafeFilename : function (filename) {
return Buffer.from((filename||'').replace(/_/g,'/').replace(/-/g,'+'), 'base64').toString('utf8');
},
/**
* Beautiful assert between two objects
* Be careful, it will inspect with a depth of 100
*
* @param {object} result : result
* @param {object} expected : expected
* @return {type} throw an error if the two objects are different
*/
assert : function (result, expected) {
assert.strictEqual(JSON.stringify(result, null, 2), JSON.stringify(expected , null, 2));
},
/**
* Read all files of a directory
*
* @param {string} dir : path to the directory we want to scan
* @param {string} extension (optional) : filter on file extension. Example: 'sql' (without the point). Can be a regex
* @return {object} : {'file/path/name.js':'content of the file', ...}
*/
readFileDirSync : function (dir, extension) {
var _that = this;
var _res = {};
var _files = _that.walkDirSync(dir,extension);
for (var i = 0; i < _files.length; i++) {
var _filePath = _files[i];
var _code = fs.readFileSync(_filePath, 'utf8');
_res[_filePath] = _code;
}
return _res;
},
cleanJavascriptVariable : function (attributeName) {
return attributeName.replace(/[^a-zA-Z0-9$_]/g, '_');
},
/**
* Remove quotes from string
*
* @param {Strring} str string with or without quotes
* @return {String} string without surrounding quotes
*/
removeQuote : function (str) {
if (typeof(str) === 'string') {
return str.replace(/^ *['"]?/, '').replace(/['"]? *$/, '');
}
return str;
},
/**
* Remove a directory and all its content
* Be careful, this method is synchrone
*
* @param {type} dir : directory path
*/
rmDirRecursive : function (dir) {
if (!fs.existsSync(dir)) {
return;
}
var _list = fs.readdirSync(dir);
for (var i = 0; i < _list.length; i++) {
var _filename = path.join(dir, _list[i]);
var _stat = fs.statSync(_filename);
if (_stat.isFile()) {
// if this is a file, remove it
fs.unlinkSync(_filename);
}
else if (_stat.isDirectory()) {
// if the is a dircetory, call the function recursively
this.rmDirRecursive(_filename);
}
}
fs.rmdirSync(dir);
},
/**
* Remove a directory and all its content using the system command.
* It should be faster accrording to some person but I have not verified.
* At least, it is asynchrone.
*
* @param {type} dir : directory path
* @param {function} callback : (error, stdout, stderr)
*/
rmDirRecursiveAsync : function (dir, callback) {
exec('rm -rf '+dir, callback);
},
/**
* Get value of an object securely
*
* @param {Object} rootObj object to read
* @param {String} path path rootObj.a.b.c
* @return {Mixed} value
*/
getValueOfPath : function (rootObj, path) {
if (typeof(rootObj) !== 'object' || typeof(path) !== 'string') {
return undefined;
}
var _currentObj = rootObj;
var _attrs = path.split('.');
for (var i = 0; i < _attrs.length; i++) {
var _attr = _attrs[i];
if (_currentObj[_attr] === undefined) {
debug('[[C_ERROR]] '+_attr+' not defined');
return '';
}
_currentObj = _currentObj[_attr];
}
return _currentObj;
},
/**
* Scan a directory and all sub-directory
* It does not return files and directories which start by a point. Example: .trash, .cache, .svn, ...
* This function is synchrone
*
* @param {string} dir : path to the directory we want to scan
* @param {string} extension (optional) : filter on file extension. Example: 'js' (without the point)
* @return {array} Array of files name with their path : ['/path/to/file1.js', '/path/to/file2.js']
*/
walkDirSync : function (dir, extension) {
var _files = [];
// if the path does not exist, return an empty table
if (!fs.existsSync(dir)) {
return _files;
}
// eliminate all files which start by a point.
var _regExp = /^[^.]/;
if (extension instanceof RegExp) {
_regExp = extension;
}
else if (extension) {
// we must use new RegExp because extension is variable
_regExp = new RegExp('^[^\\.].+\\.'+extension+'$');
}
walkDirRecursive(dir);
// recursively called by himself until all sub-directories
function walkDirRecursive (dir) {
// read the directory
var _list = fs.readdirSync(dir);
for (var i = 0; i < _list.length; i++) {
var _filename = path.join(dir, _list[i]);
var _stat = fs.statSync(_filename);
var _baseName = path.basename(_filename); // get the base name in order to eliminate folder which starts by a point.
if (_stat.isFile() && _regExp.test(_baseName)) {
// if this is a file, push it in the table
_files.push(_filename);
}
else if (_stat.isDirectory()) {
// if this is a directory, call the function recursively
walkDirRecursive(_filename);
}
}
}
return _files;
},
/**
* Copy an entire directory with all its content somewhere else
*
* @param {string} dirSource : directory source. Example /usr/lib/node
* @param {string} dirDest : directory destination. Example /usr
* In this example, it will copy the "node" directory or file in /usr/node
*/
copyDirSync : function (dirSource, dirDest) {
var _sourceList = this.walkDirSync(dirSource);
var _parentSourceDir = path.dirname(dirSource);
// replace the name of the files which contains {{=data.tableName}}
for (var i = 0; i < _sourceList.length; i++) {
// get the relative path
var _relativePath = path.relative(_parentSourceDir, _sourceList[i]);
// re-positionned the sub directories to the destination directory
var _destPath = path.join(dirDest, _relativePath);
// Get file info
var _stat = fs.statSync(_sourceList[i]);
if (_stat.isFile()) {
// if this is a file, copy its content
var _fileContent = fs.readFileSync(_sourceList[i], 'utf8');
this.writeFileDirSync(dirDest, _destPath, _fileContent);
}
else if (_stat.isDirectory()) {
this.writeFileDirSync(dirDest, _destPath);
}
}
},
/**
* Write a file and create directory if they does not exist (NOT TESTED DIRECTLY)
*
* @param {string} root : Root where to stop searching for directory creation
* @param {string} fileOrDirPath : file or directory to write with an absolute path
* @param {string} content : content of the file
*/
writeFileDirSync : function (root, fileOrDirPath, content) {
var _lastDirname = fileOrDirPath;
var _dirToCreate = [];
while (_lastDirname!=='/' && _lastDirname!==root) {
_lastDirname = path.dirname(_lastDirname);
if (!fs.existsSync(_lastDirname)) {
_dirToCreate.push(_lastDirname);
}
}
// create directories
for (var i = _dirToCreate.length-1; i >= 0; i--) {
fs.mkdirSync(_dirToCreate[i], '0755');
}
// If the is a file, create it
if (typeof (content) !== 'undefined') {
fs.writeFileSync(fileOrDirPath, content);
}
},
replaceAll : function (str, find, replace) {
return str.replace(new RegExp(find.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&'), 'g'), replace);
},
/**
* Escape Regular expression characters
* @param {String} str regular expression written as usual
* @return {String} escape version
*/
regexEscape : function (str) {
return str.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
},
/**
* Remove duplicated item of an already sorted array
* @param {Array} sortedArrayToClean array already SORTED
* @return {Array} new array without duplicated elements
*/
removeDuplicatedRows : function (sortedArrayToClean) {
if (sortedArrayToClean.length === 0) {
return sortedArrayToClean;
}
var _previousItem = sortedArrayToClean[0];
var _arrayCleaned = [_previousItem];
for (var i = 1; i < sortedArrayToClean.length; i++) {
var _item = sortedArrayToClean[i];
if (_item !== _previousItem) {
_arrayCleaned.push(_item);
_previousItem = _item;
}
}
return _arrayCleaned;
},
/**
* find closest string
* @param {String} str string to search
* @param {Array|Object} choices array of string to search
* @return {String} closest string from choices
*/
findClosest : function (str, choices) {
var _choices = choices;
if ( !(choices instanceof Array) ) {
_choices = Object.keys(choices);
}
if (_choices.length === 0 || str === '') {
return '';
}
var _minDistance = str.length;
var _closest = 0;
for (var i = 0; i < _choices.length; i++) {
var _choice = _choices[i];
var _distance = helper.distance(str, _choice);
if (_distance < _minDistance) {
_minDistance = _distance;
_closest = i;
}
}
return _choices[_closest];
},
/**
* Compute distance between two string
* //https://github.com/awnist/distance
*
* @param {String} s1 string to compare
* @param {String} s2 string
* @return {Integer} distance
*/
distance : function (s1, s2) {
var c = 0;
var lcs = 0;
var offset1 = 0;
var offset2 = 0;
var i = 0;
var maxOffset = 5;
if (!(s1 !== null) || s1.length === 0) {
if (!(s2 !== null) || s2.length === 0) {
return 0;
}
else {
return s2.length;
}
}
if (!(s2 !== null) || s2.length === 0) {
return s1.length;
}
while ((c + offset1 < s1.length) && (c + offset2 < s2.length)) {
if (s1[c + offset1] === s2[c + offset2]) {
lcs++;
}
else {
offset1 = offset2 = i = 0;
while (i < maxOffset) {
if ((c + i < s1.length) && (s1[c + i] === s2[c])) {
offset1 = i;
break;
}
if ((c + i < s2.length) && (s1[c] === s2[c + i])) {
offset2 = i;
break;
}
i++;
}
}
c++;
}
return (s1.length + s2.length) / 2 - lcs;
},
/**
* Measure CPU performance on this machine and update helper.CPU_PERFORMANCE_FACTOR
*
* This function is called in tests.
*/
updatePerformanceFactor : function () {
const _referenceTime = 20; // time
const _nbRows = 1000000;
let _data = [];
// do 4 loops, and divide the time by 4 to get a rougly constant mean
const _start = process.hrtime();
for (let i = 0; i < _nbRows; i++) { _data.push( Math.floor(Math.random() * 1000)); } // eslint-disable-line
for (let i = 0; i < _nbRows; i++) { _data[i] += Math.floor(Math.random() * 1000); } // eslint-disable-line
for (let i = 0; i < _nbRows; i++) { _data[i] += Math.floor(Math.random() * 1000); } // eslint-disable-line
for (let i = 0; i < _nbRows; i++) { _data[i] += Math.floor(Math.random() * 1000); } // eslint-disable-line
const _diff = process.hrtime(_start);
const _elapsed = parseInt(((_diff[0] * 1e9 + _diff[1]) / 1e6) / 4, 10);
helper.CPU_PERFORMANCE_FACTOR = _elapsed / _referenceTime;
// print _data to make sure V8 does not remove useless code
console.log('\nRaw CPU perf test : ' + _elapsed + ' ms ('+ _data[_nbRows/2] + ')');
console.log('CPU_PERFORMANCE_FACTOR = '+ helper.CPU_PERFORMANCE_FACTOR);
}
};
// Update Performance factor for unit tests
if (typeof global.it === 'function' && typeof global.describe === 'function') {
helper.updatePerformanceFactor();
}
module.exports = helper;