pixl-tools
Version:
A set of miscellaneous utility functions for Node.js.
1,401 lines (1,203 loc) • 41.6 kB
JavaScript
// Misc Tools for Node.js
// Copyright (c) 2015 - 2021 Joseph Huckaby
// Released under the MIT License
const fs = require('fs');
const Path = require('path');
const cp = require('child_process');
const crypto = require('crypto');
const ErrNo = require('errno');
const os = require('os');
const hostname = os.hostname();
const picomatch = require('picomatch');
const MONTH_NAMES = [
'January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December' ];
const SHORT_MONTH_NAMES = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May',
'June', 'July', 'Aug', 'Sept', 'Oct', 'Nov', 'Dec' ];
const DAY_NAMES = ['Sunday', 'Monday', 'Tuesday', 'Wednesday',
'Thursday', 'Friday', 'Saturday'];
const SHORT_DAY_NAMES = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
const EASE_ALGOS = {
Linear: function(_amount) { return _amount; },
Quadratic: function(_amount) { return Math.pow(_amount, 2); },
Cubic: function(_amount) { return Math.pow(_amount, 3); },
Quartetic: function(_amount) { return Math.pow(_amount, 4); },
Quintic: function(_amount) { return Math.pow(_amount, 5); },
Sine: function(_amount) { return 1 - Math.sin((1 - _amount) * Math.PI / 2); },
Circular: function(_amount) { return 1 - Math.sin(Math.acos(_amount)); }
};
const EASE_MODES = {
EaseIn: function(_amount, _algo) { return EASE_ALGOS[_algo](_amount); },
EaseOut: function(_amount, _algo) { return 1 - EASE_ALGOS[_algo](1 - _amount); },
EaseInOut: function(_amount, _algo) {
return (_amount <= 0.5) ? EASE_ALGOS[_algo](2 * _amount) / 2 : (2 - EASE_ALGOS[_algo](2 * (1 - _amount))) / 2;
}
};
const BIN_DIRS = ['/usr/local/bin', '/usr/bin', '/bin', '/usr/sbin', '/sbin'];
module.exports = {
"async": require('async'),
hostname: hostname,
user_cache: {},
group_cache: {},
_uniqueIDCounter: 0,
_shortIDCounter: Math.floor( Math.random() * Math.pow(36, 2) ),
NEVER_MATCH: /(?!)/,
noop: function() {},
timeNow: function(floor) {
// return current epoch time
var epoch = (new Date()).getTime() / 1000;
return floor ? Math.floor(epoch) : epoch;
},
getRandomEntropy(salt) {
// get random string using some readily-available bits of entropy
this._uniqueIDCounter++;
return [
'SALT_7fb1b7485647b1782c715474fba28fd1',
this.timeNow(),
Math.random(),
hostname,
process.pid,
this._uniqueIDCounter,
salt || ''
].join('-');
},
generateUniqueID: function(len, salt) {
// generate unique ID using SHA256 and some readily-available bits of entropy
return crypto.createHash('sha256').update( this.getRandomEntropy(salt) ).digest('hex').substring(0, len || 64);
},
generateUniqueBase64: function(bytes, salt) {
// generate unique url-safe base64 ID using some readily-available bits of entropy
var buf = crypto.createHash('sha256').update( this.getRandomEntropy(salt) ).digest();
var output = bytes ? buf.slice(0, bytes).toString('base64') : buf.toString('base64');
return output.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); // url-safe
},
generateShortID: function(prefix) {
// generate short id using high-res server time, and a static counter,
// both converted to alphanumeric lower-case (base-36), ends up being ~10 chars.
// allows for *up to* 1,296 unique ids per millisecond (give or take).
this._shortIDCounter++;
if (this._shortIDCounter >= Math.pow(36, 2)) this._shortIDCounter = 0;
return [
prefix || '',
this.zeroPad( (new Date()).getTime().toString(36), 8 ),
this.zeroPad( this._shortIDCounter.toString(36), 2 )
].join('');
},
digestHex: function(str, algo, len) {
// digest string using SHA256 (default) or other algo, return hex hash
var output = crypto.createHash( algo || 'sha256' ).update( str ).digest('hex');
return len ? output.substring(0, len) : output;
},
digestBase64: function(str, algo, bytes) {
// digest string using SHA256 (default) or other algo, return url-safe base64 string
var buf = crypto.createHash( algo || 'sha256' ).update( str ).digest();
var output = bytes ? buf.slice(0, bytes).toString('base64') : buf.toString('base64');
return output.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); // url-safe
},
numKeys: function(hash) {
// count keys in hash
// Object.keys(hash).length may be faster, but this uses far less memory
var count = 0;
for (var key in hash) { count++; }
return count;
},
firstKey: function(hash) {
// return first key in hash (key order is undefined)
for (var key in hash) return key;
return null; // no keys in hash
},
hashKeysToArray: function(hash) {
// convert hash keys to array (discard values)
var arr = [];
for (var key in hash) arr.push(key);
return arr;
},
hashValuesToArray: function(hash) {
// convert hash values to array (discard keys)
var arr = [];
for (var key in hash) arr.push( hash[key] );
return arr;
},
isaHash: function(arg) {
// determine if arg is a hash or hash-like
return( !!arg && (typeof(arg) == 'object') && (typeof(arg.length) == 'undefined') );
},
isaArray: function(arg) {
// determine if arg is an array or is array-like
return( !!arg && (typeof(arg) == 'object') && (typeof(arg.length) != 'undefined') );
},
copyHash: function(hash, deep) {
// copy hash to new one, with optional deep mode (uses JSON)
if (deep) {
// deep copy
return JSON.parse( JSON.stringify(hash) );
}
else {
// shallow copy
var output = {};
for (var key in hash) {
output[key] = hash[key];
}
return output;
}
},
copyHashRemoveKeys: function(hash, remove) {
// shallow copy hash, excluding some keys
var output = {};
for (var key in hash) {
if (!remove[key]) output[key] = hash[key];
}
return output;
},
copyHashRemoveProto: function(hash) {
// shallow copy hash, but remove __proto__ and family from copy
var output = Object.create(null);
for (var key in hash) {
output[key] = hash[key];
}
return output;
},
mergeHashes: function(a, b) {
// shallow-merge keys from a and b into c and return c
// b has precedence over a
if (!a) a = {};
if (!b) b = {};
var c = {};
for (var key in a) c[key] = a[key];
for (var key in b) c[key] = b[key];
return c;
},
mergeHashInto: function(a, b) {
// shallow-merge keys from b into a
for (var key in b) a[key] = b[key];
},
parseQueryString: function(url) {
// parse query string into key/value pairs and return as object
var query = {};
url.replace(/^.*\?/, '').replace(/([^\=]+)\=([^\&]*)\&?/g, function(match, key, value) {
query[key] = decodeURIComponent(value);
if (query[key].match(/^\-?\d+$/)) query[key] = parseInt(query[key]);
else if (query[key].match(/^\-?\d*\.\d+$/)) query[key] = parseFloat(query[key]);
return '';
} );
return query;
},
composeQueryString: function(query) {
// compose key/value pairs into query string
var qs = '';
for (var key in query) {
qs += (qs.length ? '&' : '?') + key + '=' + encodeURIComponent(query[key]);
}
return qs;
},
findObjectsIdx: function(arr, crit, max) {
// find idx of all objects that match crit keys/values
var idxs = [];
var num_crit = 0;
for (var a in crit) num_crit++;
for (var idx = 0, len = arr.length; idx < len; idx++) {
var matches = 0;
for (var key in crit) {
if (arr[idx][key] == crit[key]) matches++;
}
if (matches == num_crit) {
idxs.push(idx);
if (max && (idxs.length >= max)) return idxs;
}
} // foreach elem
return idxs;
},
findObjectIdx: function(arr, crit) {
// find idx of first matched object, or -1 if not found
var idxs = this.findObjectsIdx(arr, crit, 1);
return idxs.length ? idxs[0] : -1;
},
findObject: function(arr, crit) {
// return first found object matching crit keys/values, or null if not found
var idx = this.findObjectIdx(arr, crit);
return (idx > -1) ? arr[idx] : null;
},
findObjects: function(arr, crit) {
// find and return all objects that match crit keys/values
var idxs = this.findObjectsIdx(arr, crit);
var objs = [];
for (var idx = 0, len = idxs.length; idx < len; idx++) {
objs.push( arr[idxs[idx]] );
}
return objs;
},
findObjectsDeep: function(arr, crit, max) {
// find and return all objects that match crit paths/values
var results = [];
var num_crit = 0;
for (var a in crit) num_crit++;
for (var idx = 0, len = arr.length; idx < len; idx++) {
var matches = 0;
for (var key in crit) {
if (this.getPath(arr[idx], key) == crit[key]) matches++;
}
if (matches == num_crit) {
results.push(arr[idx]);
if (max && (results.length >= max)) return results;
}
} // foreach elem
return results;
},
findObjectDeep: function(arr, crit) {
// return first found object matching crit paths/values, or null if not found
var results = this.findObjectsDeep(arr, crit, 1);
return results.length ? results[0] : null;
},
deleteObject: function(arr, crit) {
// walk array looking for nested object matching criteria object
// delete first object found
var idx = this.findObjectIdx(arr, crit);
if (idx > -1) {
arr.splice( idx, 1 );
return true;
}
return false;
},
deleteObjects: function(arr, crit) {
// delete all objects in obj array matching criteria
// FUTURE: This is not terribly efficient -- could use a rewrite.
var count = 0;
while (this.deleteObject(arr, crit)) count++;
return count;
},
alwaysArray: function(obj) {
// if obj is not an array, wrap it in one and return it
return this.isaArray(obj) ? obj : [obj];
},
lookupPath: function(path, obj) {
// LEGACY METHOD, included for backwards compatibility only -- use getPath() instead
// walk through object tree, psuedo-XPath-style
// supports arrays as well as objects
// return final object or value
// always start query with a slash, i.e. /something/or/other
path = path.replace(/\/$/, ""); // strip trailing slash
while (/\/[^\/]+/.test(path) && (typeof(obj) == 'object')) {
// find first slash and strip everything up to and including it
var slash = path.indexOf('/');
path = path.substring( slash + 1 );
// find next slash (or end of string) and get branch name
slash = path.indexOf('/');
if (slash == -1) slash = path.length;
var name = path.substring(0, slash);
// advance obj using branch
if ((typeof(obj.length) == 'undefined') || name.match(/\D/)) {
// obj is probably a hash
if (typeof(obj[name]) != 'undefined') obj = obj[name];
else return null;
}
else {
// obj is array
var idx = parseInt(name, 10);
if (isNaN(idx)) return null;
if (typeof(obj[idx]) != 'undefined') obj = obj[idx];
else return null;
}
} // while path contains branch
return obj;
},
sub: function(text, args, fatal, fallback, filter) {
// perform simple [placeholder] substitution using supplied
// args object and return transformed text
var self = this;
var result = true;
var value = '';
if (typeof(text) == 'undefined') text = '';
text = '' + text;
if (!args) args = {};
if (fallback && filter) fallback = filter(fallback);
text = text.replace(/\[([^\]]+)\]/g, function(m_all, name) {
value = self.getPath(args, name);
if (value === undefined) {
result = false;
return fallback || m_all;
}
else if (filter) return filter(value);
else return value;
} );
if (!result && fatal) return null;
else return text;
},
substitute: function(text, args, fatal) {
// LEGACY METHOD, included for backwards compatibility only -- use sub() instead
// perform simple [placeholder] substitution using supplied
// args object and return transformed text
if (typeof(text) == 'undefined') text = '';
text = '' + text;
if (!args) args = {};
while (text.indexOf('[') > -1) {
var open_bracket = text.indexOf('[');
var close_bracket = text.indexOf(']');
if (close_bracket < open_bracket) {
// error, mismatched brackets, we must abort
return fatal ? null : text.replace(/__APLB__/g, '[').replace(/__APRB__/g, ']');
}
var before = text.substring(0, open_bracket);
var after = text.substring(close_bracket + 1, text.length);
var name = text.substring( open_bracket + 1, close_bracket );
var value = '';
// prevent infinite loop with nested open brackets
name = name.replace(/\[/g, '__APLB__');
if (name.indexOf('/') == 0) {
value = this.lookupPath(name, args);
if (value === null) {
if (fatal) return null;
else value = '__APLB__' + name + '__APRB__';
}
}
else if (typeof(args[name]) != 'undefined') value = args[name];
else {
if (fatal) return null;
else value = '__APLB__' + name + '__APRB__';
}
text = before + value + after;
} // while text contains [
return text.replace(/__APLB__/g, '[').replace(/__APRB__/g, ']');
},
setPath: function(target, path, value) {
// set path using dir/slash/syntax or dot.path.syntax
// support inline dots and slashes if backslash-escaped
var parts = (path.indexOf("\\") > -1) ? path.replace(/\\\./g, '__PXDOT__').replace(/\\\//g, '__PXSLASH__').split(/[\.\/]/).map( function(elem) {
return elem.replace(/__PXDOT__/g, '.').replace(/__PXSLASH__/g, '/');
} ) : path.split(/[\.\/]/);
var key = parts.pop();
// traverse path
while (parts.length) {
var part = parts.shift();
if (part) {
if (!(part in target)) {
// auto-create nodes
target[part] = {};
}
if (typeof(target[part]) != 'object') {
// path runs into non-object
return false;
}
target = target[part];
}
}
target[key] = value;
return true;
},
deletePath: function(target, path) {
// delete path using dir/slash/syntax or dot.path.syntax
// support inline dots and slashes if backslash-escaped
var parts = (path.indexOf("\\") > -1) ? path.replace(/\\\./g, '__PXDOT__').replace(/\\\//g, '__PXSLASH__').split(/[\.\/]/).map( function(elem) {
return elem.replace(/__PXDOT__/g, '.').replace(/__PXSLASH__/g, '/');
} ) : path.split(/[\.\/]/);
var key = parts.pop();
// traverse path
while (parts.length) {
var part = parts.shift();
if (part) {
if (!(part in target)) {
// path runs into non-existent object
return false;
}
if (typeof(target[part]) != 'object') {
// path runs into non-object
return false;
}
target = target[part];
}
}
delete target[key];
return true;
},
getPath: function(target, path) {
// get path using dir/slash/syntax or dot.path.syntax
// support inline dots and slashes if backslash-escaped
var parts = (path.indexOf("\\") > -1) ? path.replace(/\\\./g, '__PXDOT__').replace(/\\\//g, '__PXSLASH__').split(/[\.\/]/).map( function(elem) {
return elem.replace(/__PXDOT__/g, '.').replace(/__PXSLASH__/g, '/');
} ) : path.split(/[\.\/]/);
var key = parts.pop();
// traverse path
while (parts.length) {
var part = parts.shift();
if (part) {
if (typeof(target[part]) != 'object') {
// path runs into non-object
return undefined;
}
target = target[part];
}
}
return target[key];
},
formatDate: function(thingy, template) {
// format date using get_date_args
// e.g. '[yyyy]/[mm]/[dd]' or '[dddd], [mmmm] [mday], [yyyy]' or '[hour12]:[mi] [ampm]'
return this.sub( template, this.getDateArgs(thingy) );
},
getDateArgs: function(thingy) {
// return hash containing year, mon, mday, hour, min, sec
// given epoch seconds, date object or date string
if (!thingy) thingy = new Date();
var date = (typeof(thingy) == 'object') ? thingy : (new Date( (typeof(thingy) == 'number') ? (thingy * 1000) : thingy ));
var args = {
epoch: Math.floor( date.getTime() / 1000 ),
year: date.getFullYear(),
mon: date.getMonth() + 1,
mday: date.getDate(),
wday: date.getDay(),
hour: date.getHours(),
min: date.getMinutes(),
sec: date.getSeconds(),
msec: date.getMilliseconds(),
offset: 0 - (date.getTimezoneOffset() / 60)
};
args.yyyy = '' + args.year;
args.yy = args.year % 100;
if (args.yy < 10) args.yy = "0" + args.yy; else args.yy = '' + args.yy;
if (args.mon < 10) args.mm = "0" + args.mon; else args.mm = '' + args.mon;
if (args.mday < 10) args.dd = "0" + args.mday; else args.dd = '' + args.mday;
if (args.hour < 10) args.hh = "0" + args.hour; else args.hh = '' + args.hour;
if (args.min < 10) args.mi = "0" + args.min; else args.mi = '' + args.min;
if (args.sec < 10) args.ss = "0" + args.sec; else args.ss = '' + args.sec;
if (args.hour >= 12) {
args.ampm = 'pm';
args.hour12 = args.hour - 12;
if (!args.hour12) args.hour12 = 12;
}
else {
args.ampm = 'am';
args.hour12 = args.hour;
if (!args.hour12) args.hour12 = 12;
}
args.AMPM = args.ampm.toUpperCase();
args.yyyy_mm_dd = args.yyyy + '/' + args.mm + '/' + args.dd;
args.hh_mi_ss = args.hh + ':' + args.mi + ':' + args.ss;
args.tz = 'GMT' + (args.offset >= 0 ? '+' : '') + args.offset;
// add formatted month and weekdays
args.mmm = SHORT_MONTH_NAMES[ args.mon - 1 ];
args.mmmm = MONTH_NAMES[ args.mon - 1];
args.ddd = SHORT_DAY_NAMES[ args.wday ];
args.dddd = DAY_NAMES[ args.wday ];
return args;
},
getTimeFromArgs: function(args) {
// return epoch given args like those returned from getDateArgs()
var then = new Date(
args.year,
args.mon - 1,
args.mday,
args.hour,
args.min,
args.sec,
0
);
return Math.floor( then.getTime() / 1000 );
},
normalizeTime: function(epoch, zero_args) {
// quantize time into any given precision
// examples:
// hour: { min:0, sec:0 }
// day: { hour:0, min:0, sec:0 }
var args = this.getDateArgs(epoch);
for (var key in zero_args) args[key] = zero_args[key];
// mday is 1-based
if (!args['mday']) args['mday'] = 1;
return this.getTimeFromArgs(args);
},
getTextFromBytes: function(bytes, precision) {
// convert raw bytes to english-readable format
// set precision to 1 for ints, 10 for 1 decimal point (default), 100 for 2, etc.
bytes = Math.floor(bytes);
if (!precision) precision = 10;
if (bytes >= 1024) {
bytes = Math.floor( (bytes / 1024) * precision ) / precision;
if (bytes >= 1024) {
bytes = Math.floor( (bytes / 1024) * precision ) / precision;
if (bytes >= 1024) {
bytes = Math.floor( (bytes / 1024) * precision ) / precision;
if (bytes >= 1024) {
bytes = Math.floor( (bytes / 1024) * precision ) / precision;
return bytes + ' TB';
}
else return bytes + ' GB';
}
else return bytes + ' MB';
}
else return bytes + ' K';
}
else return bytes + this.pluralize(' byte', bytes);
},
getBytesFromText: function(text) {
// parse text into raw bytes, e.g. "1 K" --> 1024
if (text.toString().match(/^\d+$/)) return parseInt(text); // already in bytes
var multipliers = {
b: 1,
k: 1024,
m: 1024 * 1024,
g: 1024 * 1024 * 1024,
t: 1024 * 1024 * 1024 * 1024
};
var bytes = 0;
text = text.toString().replace(/([\d\.]+)\s*(\w)\w*\s*/g, function(m_all, m_g1, m_g2) {
var mult = multipliers[ m_g2.toLowerCase() ] || 0;
bytes += (parseFloat(m_g1) * mult);
return '';
} );
return Math.floor(bytes);
},
commify: function(number) {
// add US-formatted commas to integer, like 1,234,567
if (!number) number = 0;
number = '' + number;
if (number.length > 3) {
var mod = number.length % 3;
var output = (mod > 0 ? (number.substring(0,mod)) : '');
for (var i=0 ; i < Math.floor(number.length / 3); i++) {
if ((mod == 0) && (i == 0))
output += number.substring(mod+ 3 * i, mod + 3 * i + 3);
else
output+= ',' + number.substring(mod + 3 * i, mod + 3 * i + 3);
}
return (output);
}
else return number;
},
shortFloat: function(value, places) {
// Shorten floating-point decimal to N places max
if (!places) places = 2;
var mult = Math.pow(10, places);
return( Math.floor(parseFloat(value || 0) * mult) / mult );
},
pct: function(count, max, floor) {
// Return formatted percentage given a number along a sliding scale from 0 to 'max'
var pct = (count * 100) / (max || 1);
if (!pct.toString().match(/^\d+(\.\d+)?$/)) { pct = 0; }
return '' + (floor ? Math.floor(pct) : this.shortFloat(pct)) + '%';
},
zeroPad: function(value, len) {
// Pad a number with zeroes to achieve a desired total length (max 10)
return ('0000000000' + value).slice(0 - len);
},
clamp: function(val, min, max) {
// simple math clamp implementation
return Math.max(min, Math.min(max, val));
},
lerp: function(start, end, amount) {
// simple linear interpolation algo
return start + ((end - start) * this.clamp(amount, 0, 1));
},
getTextFromSeconds: function(sec, abbrev, no_secondary) {
// convert raw seconds to human-readable relative time
var neg = '';
sec = Math.floor(sec);
if (sec<0) { sec =- sec; neg = '-'; }
var p_text = abbrev ? "sec" : "second";
var p_amt = sec;
var s_text = "";
var s_amt = 0;
if (sec > 59) {
var min = Math.floor(sec / 60);
sec = sec % 60;
s_text = abbrev ? "sec" : "second";
s_amt = sec;
p_text = abbrev ? "min" : "minute";
p_amt = min;
if (min > 59) {
var hour = Math.floor(min / 60);
min = min % 60;
s_text = abbrev ? "min" : "minute";
s_amt = min;
p_text = abbrev ? "hr" : "hour";
p_amt = hour;
if (hour > 23) {
var day = Math.floor(hour / 24);
hour = hour % 24;
s_text = abbrev ? "hr" : "hour";
s_amt = hour;
p_text = "day";
p_amt = day;
} // hour>23
} // min>59
} // sec>59
var text = p_amt + " " + p_text;
if ((p_amt != 1) && !abbrev) text += "s";
if (s_amt && !no_secondary) {
text += ", " + s_amt + " " + s_text;
if ((s_amt != 1) && !abbrev) text += "s";
}
return(neg + text);
},
getSecondsFromText: function(text) {
// parse text into raw seconds, e.g. "1 minute" --> 60
if (text.toString().match(/^\d+$/)) return parseInt(text); // already in seconds
var multipliers = {
s: 1,
m: 60,
h: 60 * 60,
d: 60 * 60 * 24,
w: 60 * 60 * 24 * 7,
y: 60 * 60 * 24 * 365
};
var seconds = 0;
text = text.toString().replace(/([\d\.]+)\s*(\w)\w*\s*/g, function(m_all, m_g1, m_g2) {
var mult = multipliers[ m_g2.toLowerCase() ] || 0;
seconds += (parseFloat(m_g1) * mult);
return '';
} );
return Math.floor(seconds);
},
getNiceRemainingTime: function(elapsed, counter, counter_max, abbrev, shorten) {
// estimate remaining time given starting epoch, a counter and the
// counter maximum (i.e. percent and 100 would work)
// return in english-readable format
if (counter == counter_max) return 'Complete';
if (counter == 0) return 'n/a';
var sec_remain = Math.floor(((counter_max - counter) * elapsed) / counter);
return this.getTextFromSeconds( sec_remain, abbrev, shorten );
},
randArray: function(arr) {
// return random element from array
return arr[ Math.floor(Math.random() * arr.length) ];
},
pluralize: function(word, num) {
// apply english pluralization to word if 'num' is not equal to 1
if (num != 1) {
return word.replace(/y$/, 'ie') + 's';
}
else return word;
},
escapeRegExp: function(text) {
// escape text for regular expression
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
},
ucfirst: function(text) {
// capitalize first character only, lower-case rest
return text.substring(0, 1).toUpperCase() + text.substring(1, text.length).toLowerCase();
},
getErrorDescription: function(err) {
// attempt to get better error description using 'errno' module
var msg = err.message;
if (err.errno && ErrNo.code[err.errno]) {
msg = this.ucfirst(ErrNo.code[err.errno].description) + " (" + err.message + ")";
}
else if (err.code && ErrNo.code[err.code]) {
msg = this.ucfirst(ErrNo.code[err.code].description) + " (" + err.message + ")";
}
return msg;
},
bufferSplit: function(buf, chunk) {
// Split a buffer like string split (no reg exp support tho)
// WARNING: Splits use SAME MEMORY SPACE as original buffer
var idx = -1;
var lines = [];
while ((idx = buf.indexOf(chunk)) > -1) {
lines.push( buf.subarray(0, idx) );
buf = buf.subarray( idx + chunk.length, buf.length );
}
lines.push(buf);
return lines;
},
fileEachLine: function(file, opts, iterator, callback) {
// asynchronously process file line by line, using very little memory
var self = this;
if (!callback && (typeof(opts) == 'function')) {
// support 3-arg convention: file, iterator, callback
callback = iterator;
iterator = opts;
opts = {};
}
if (!opts) opts = {};
if (!opts.buffer_size) opts.buffer_size = 1024;
if (!opts.eol) opts.eol = os.EOL;
if (!('encoding' in opts)) opts.encoding = 'utf8';
var chunk = Buffer.alloc(opts.buffer_size);
var lastChunk = null;
var processNextLine = null;
var processChunk = null;
var readNextChunk = null;
var lines = [];
fs.open(file, "r", function(err, fh) {
if (err) {
if ((err.code == 'ENOENT') && (opts.ignore_not_found)) return callback();
else return callback(err);
}
processNextLine = function() {
// process single line from buffer
var line = lines.shift();
if (opts.encoding) line = line.toString( opts.encoding );
iterator(line, function(err) {
if (err) {
fs.close(fh, function() {});
return callback(err);
}
// if (lines.length) setImmediate( processNextLine ); // ensure async
if (lines.length) process.nextTick( processNextLine );
else readNextChunk();
});
};
processChunk = function(err, num_bytes, chunk) {
if (err) {
fs.close(fh, function() {});
return callback(err);
}
var eof = (num_bytes != opts.buffer_size);
var data = chunk.subarray(0, num_bytes);
if (lastChunk && lastChunk.length) {
data = Buffer.concat([lastChunk, data], lastChunk.length + data.length);
lastChunk = null;
}
if (data.length) {
lines = self.bufferSplit( data, opts.eol );
// see if data ends on EOL -- if not, we have a partial block
// fill buffer for next read (unless at EOF)
if (data.subarray(0 - opts.eol.length).toString() == opts.eol) {
lines.pop(); // remove empty line caused by split
}
else if (!eof) {
// more to read, save excess for next loop iteration
var line = lines.pop();
lastChunk = Buffer.from(line);
}
if (lines.length) processNextLine();
else readNextChunk();
}
else {
// close file and complete
fs.close(fh, callback);
}
};
readNextChunk = function() {
// read chunk from file
fs.read(fh, chunk, 0, opts.buffer_size, null, processChunk);
};
// begin reading
readNextChunk();
}); // fs.open
},
getpwnam: function(username, use_cache) {
// Simulate POSIX getpwnam by querying getent on linux, or /usr/bin/id on darwin / OSX.
// Accepts username or uid, and can optionally cache results for repeat queries for same user.
// Response keys: username, password, uid, gid, name, dir, shell
var user = null;
// sanitize username to prevent abuse
username = username.toString().replace(/[^\w\-\.]/g, '');
if (use_cache && this.user_cache[username]) {
return this.copyHash( this.user_cache[username] );
}
if (process.platform === 'linux') {
// use getent on linux
var cols = null;
var getent = this.findBinSync('getent');
if (!getent) return null; // no getent!
var opts = { timeout: 1000, encoding: 'utf8' };
try { cols = cp.execSync(getent + ' passwd ' + username, opts).trim().split(':'); }
catch (err) { return null; }
if ((username == cols[0]) || (username == Number(cols[2]))) {
user = {
username: cols[0],
password: cols[1],
uid: Number(cols[2]),
gid: Number(cols[3]),
name: cols[4] && cols[4].split(',')[0],
dir: cols[5],
shell: cols[6]
};
}
else {
// user not found
return null;
}
}
else if (process.platform === 'darwin') {
// use /usr/bin/id on darwin / OSX
var cols = null;
var opts = { timeout: 1000, encoding: 'utf8', stdio: 'pipe' };
try { cols = cp.execSync('/usr/bin/id -P ' + username, opts).trim().split(':'); }
catch (err) { return null; }
if ((username == cols[0]) || (username == Number(cols[2]))) {
user = {
username: cols[0],
password: cols[1],
uid: Number(cols[2]),
gid: Number(cols[3]),
name: cols[7],
dir: cols[8],
shell: cols[9]
};
}
else {
// something went wrong
return null;
}
}
else {
// unsupported platform
return null;
}
if (use_cache) {
this.user_cache[ user.username ] = user;
this.user_cache[ user.uid ] = user;
return this.copyHash( user );
}
else {
return user;
}
},
getgrnam: function(name, use_cache) {
// Simulate POSIX getgrnam by querying getent on linux, or /etc/group on darwin / OSX.
// Accepts group name or gid, and can optionally cache results for repeat queries for same group.
// Response keys: name, gid
var group = null;
// sanitize group name to prevent abuse
name = name.toString().replace(/[^\w\-\.]/g, '');
if (use_cache && this.group_cache[name]) {
return this.copyHash( this.group_cache[name] );
}
if (process.platform === 'linux') {
// use getent on linux
var lines = null;
var cols = null;
var getent = this.findBinSync('getent');
if (!getent) return null; // no getent!
var opts = { timeout: 1000, encoding: 'utf8' };
try { cols = cp.execSync(getent + ' group ' + name, opts).trim().split(':'); }
catch (err) { return null; }
if ((name == cols[0]) || (name == Number(cols[2]))) {
group = {
name: cols[0],
gid: Number(cols[2])
};
}
else {
// group not found
return null;
}
}
else if (process.platform === 'darwin') {
// use /etc/group on darwin / OSX
if (!fs.existsSync('/etc/group')) return null; // no /etc/group!
var lines = fs.readFileSync('/etc/group', 'utf8').trim().split(/\n/);
for (var idx = 0, len = lines.length; idx < len; idx++) {
var cols = lines[idx].split(':');
if ((name == cols[0]) || (name == Number(cols[2]))) {
group = {
name: cols[0],
gid: Number(cols[2])
};
idx = len;
}
}
return group;
}
else {
// unsupported platform
return null;
}
if (use_cache) {
this.group_cache[ group.name ] = group;
this.group_cache[ group.gid ] = group;
return this.copyHash( group );
}
else {
return group;
}
},
tween: function(start, end, amount, mode, algo) {
// Calculate the "tween" (value between two other values) using a variety of algorithms.
// Useful for computing positions for animation frames.
// Omit mode and algo for 'lerp' (simple linear interpolation).
if (!mode) mode = 'EaseOut';
if (!algo) algo = 'Linear';
amount = this.clamp( amount, 0.0, 1.0 );
return start + (EASE_MODES[mode]( amount, algo ) * (end - start));
},
findFiles: function(dir, opts, callback) {
// find all files matching filespec, optionally recurse into subdirs
// opts: { filespec, recurse, all (dotfiles), filter, dirs, stats }
var files = [];
if (!callback) { callback = opts; opts = {}; }
if (!opts) opts = {};
if (!opts.filespec) opts.filespec = /.+/;
else if (typeof(opts.filespec) == 'string') opts.filespec = new RegExp(opts.filespec);
if (!("recurse" in opts)) opts.recurse = true;
this.walkDir( dir,
function(file, stats, callback) {
var filename = Path.basename(file);
if (!opts.all && filename.match(/^\./)) return callback(false); // skip dotfiles
var info = { path: file, size: stats.size, mtime: stats.mtimeMs / 1000 };
if (stats.isDirectory()) {
info.dir = true;
if (opts.dirs && filename.match(opts.filespec)) files.push( opts.stats ? info : file );
return callback( opts.recurse );
}
else {
if (filename.match( opts.filespec )) {
if (opts.filter && (opts.filter(file, stats) === false)) return callback(false); // user skip
else files.push( opts.stats ? info : file );
}
}
callback();
},
function(err) {
callback(err, files);
}
); // walkDir
},
findFilesSync: function(dir, opts) {
// find all files matching filespec sync, optionally recurse into subdirs
// opts: { filespec, recurse, all (dotfiles), filter, dirs, stats }
var files = [];
if (!opts) opts = {};
if (!opts.filespec) opts.filespec = /.+/;
else if (typeof(opts.filespec) == 'string') opts.filespec = new RegExp(opts.filespec);
if (!("recurse" in opts)) opts.recurse = true;
this.walkDirSync(dir, function(file, stats) {
var filename = Path.basename(file);
if (!opts.all && filename.match(/^\./)) return false; // skip dotfiles
var info = { path: file, size: stats.size, mtime: stats.mtimeMs / 1000 };
if (stats.isDirectory()) {
info.dir = true;
if (opts.dirs && filename.match(opts.filespec)) files.push( opts.stats ? info : file );
return opts.recurse;
}
else {
if (filename.match( opts.filespec )) {
if (opts.filter && (opts.filter(file, stats) === false)) return false; // user skip
else files.push( opts.stats ? info : file );
}
}
}); // walkDirSync
return files;
},
walkDir: function(dir, iterator, callback) {
// walk directory tree, fire iterator for every file, then callback at end
// iterator is passed: (path, stats, callback)
// pass false to iterator callback to prevent descending into a dir
var self = this;
fs.readdir(dir, function(err, files) {
if (err) return callback(err);
if (!files || !files.length) return callback();
self.async.eachSeries( files,
function(filename, callback) {
var file = Path.join( dir, filename );
fs.stat( file, function(err, stats) {
if (err) return callback();
iterator( file, stats, function(cont) {
// recurse for dir
if (stats.isDirectory() && (cont !== false)) {
self.walkDir( file, iterator, callback );
}
else callback();
} );
} );
}, callback
); // eachSeries
} ); // fs.readdir
},
walkDirSync: function(dir, iterator) {
// walk directory tree sync, fire iterator for every file
// iterator is passed: (path, stats)
// return false from iterator to prevent descending into a dir
var self = this;
var files = fs.readdirSync(dir);
if (!files || !files.length) return;
files.forEach( function(filename) {
var file = Path.join( dir, filename );
var stats = null;
try { stats = fs.statSync(file); }
catch(e) { return; }
var cont = iterator(file, stats);
if (stats.isDirectory() && (cont !== false)) {
self.walkDirSync( file, iterator );
}
}); // forEach
},
glob: function(filespec, opts, callback) {
// find files using glob pattern
if (!callback) { callback = opts; opts = {}; }
var pmatch = picomatch(filespec, opts || {});
var pinfo = picomatch.scan(filespec);
var dir = pinfo.base || '.';
if (dir === filespec) dir = Path.dirname(dir);
var recurse = !!pinfo.glob.match(/(\*\*|\/)/);
this.findFiles( dir, { recurse, dirs: true }, function(err, files) {
if (err) return callback(err);
callback(null, files.filter( function(file) { return pmatch(file); } ));
}); // findFiles
},
globSync: function(filespec, opts) {
// find files using glob pattern, sync
var pmatch = picomatch(filespec, opts || {});
var pinfo = picomatch.scan(filespec);
var dir = pinfo.base || '.';
if (dir === filespec) dir = Path.dirname(dir);
var recurse = !!pinfo.glob.match(/(\*\*|\/)/);
var files = this.findFilesSync( dir, { recurse, dirs: true } );
return files.filter( function(file) { return pmatch(file); } );
},
rimraf: function(filespec, opts, callback) {
// multi-recursive delete (rm -rf)
if (!callback) { callback = opts; opts = {}; }
var self = this;
this.glob(filespec, function(err, files) {
if (err) return callback(err);
self.async.eachSeries(files, function(file, callback) {
fs.rm( file, { force: true, recursive: true }, callback );
}, callback );
}); // glob
},
rimrafSync: function(filespec) {
// multi-recursive delete (rm -rf) sync
this.glob.sync(filespec).forEach( function(file) {
fs.rmSync( file, { force: true, recursive: true } );
}); // glob
},
mkdirp: function(path, opts, callback) {
// Recursively create directories
if (!callback) {
callback = opts;
opts = null;
}
if (!opts) opts = { mode: 0o777 };
if (typeof(opts) == 'number') opts = { mode: opts };
opts.recursive = true;
fs.mkdir( path, opts, callback );
},
mkdirpSync: function(path, opts) {
// Recursively create directories, sync
if (!opts) opts = { mode: 0o777 };
if (typeof(opts) == 'number') opts = { mode: opts };
opts.recursive = true;
return fs.mkdirSync( path, opts );
},
writeFileAtomic: function(file, data, opts, callback) {
// write a file atomically
var temp_file = file + '.tmp.' + process.pid + '.' + this.generateShortID();
if (!callback) {
// opts is optional
callback = opts;
opts = {};
}
fs.writeFile( temp_file, data, opts, function(err) {
if (err) return callback(err);
fs.rename( temp_file, file, function(err) {
if (err) {
// cleanup temp file before returning
fs.unlink( temp_file, function() { callback(err); } );
}
else callback();
});
}); // fs.writeFile
},
writeFileAtomicSync: function(file, data, opts) {
// write a file atomically and synchronously
var temp_file = file + '.tmp.' + process.pid + '.' + this.generateShortID();
if (!opts) opts = {};
fs.writeFileSync( temp_file, data, opts );
try {
fs.renameSync( temp_file, file );
}
catch (err) {
// try to cleanup temp file before throwing
fs.unlinkSync( temp_file );
throw err;
}
},
parseJSON: function(text) {
// parse JSON with improved error messages (i.e. line numbers)
text = text.toString().replace(/\r\n/g, "\n"); // Unix line endings
var json = null;
try { json = JSON.parse(text); }
catch (err) {
var lines = text.split(/\n/).map( function(line) { return line + "\n"; } );
var err_msg = (err.message || err.toString()).replace(/\bat\s+position\s+(\d+)/, function(m_all, m_g1) {
var pos = parseInt(m_g1);
var offset = 0;
var loc = null;
for (var idx = 0, len = lines.length; idx < len; idx++) {
offset += lines[idx].length;
if (offset >= pos) {
loc = { line: idx + 1 };
offset -= lines[idx].length;
loc.column = (pos - offset) + 1;
idx = len;
}
}
if (loc) {
return "on line " + loc.line + " column " + loc.column;
}
else return m_all;
});
throw new Error(err_msg);
}
return json;
},
findBin: function(bin, callback) {
// locate binary executable using PATH and known set of common dirs
var dirs = (process.env.PATH || '').split(/\:/).concat(BIN_DIRS).filter( function(item) {
return item.match(/\S/);
} );
var found = false;
this.async.eachSeries( dirs,
function(dir, callback) {
var file = Path.join(dir, bin);
fs.stat( file, function(err, stats) {
if (!err && stats) {
found = file;
return callback("ABORT");
}
callback();
} ); // fs.stat
},
function() {
if (found) callback( false, found );
else callback( new Error("Binary executable not found: " + bin) );
}
); // eachSeries
},
findBinSync: function(bin) {
// locate binary executable using PATH and known set of common dirs
var dirs = (process.env.PATH || '').split(/\:/).concat(BIN_DIRS).filter( function(item) {
return item.match(/\S/);
} );
for (var idx = 0, len = dirs.length; idx < len; idx++) {
var file = Path.join(dirs[idx], bin);
if (fs.existsSync(file)) return file;
}
return false;
},
sortBy: function(orig, key, opts) {
// sort array of objects by key, asc or desc, and optionally return NEW array
// opts: { dir, type, copy }
if (!opts) opts = {};
if (!opts.dir) opts.dir = 1;
if (!opts.type) opts.type = 'string';
var arr = opts.copy ? Array.from(orig) : orig;
arr.sort( function(a, b) {
switch(opts.type) {
case 'string':
return( (''+a[key]).localeCompare(b[key]) * opts.dir );
break;
case 'number':
return (a[key] - b[key]) * opts.dir;
break;
}
} );
return arr;
},
includesAny: function(haystack, needles) {
// return true if haystack contains any needles
// (like Array.includes, but searches first array for ANY matches in second array)
for (var idx = 0, len = needles.length; idx < len; idx++) {
if (haystack.includes(needles[idx])) return true;
}
return false;
}
}; // module.exports
// some utility functions need to be fully portable
module.exports.glob = module.exports.glob.bind(module.exports);
module.exports.rimraf = module.exports.rimraf.bind(module.exports);
module.exports.mkdirp = module.exports.mkdirp.bind(module.exports);
// *.sync calling conventions, also portable
module.exports.glob.sync = module.exports.globSync.bind(module.exports);
module.exports.rimraf.sync = module.exports.rimrafSync.bind(module.exports);
module.exports.mkdirp.sync = module.exports.mkdirpSync.bind(module.exports);