finder
Version:
Highly configurable file finder for NodeJS (works synchronously or asynchronously)
552 lines (477 loc) • 13.3 kB
JavaScript
/*jslint nodejs:true, indent:4 */
/**
* Imports
*/
var fs = require('fs'),
path = require('path'),
slice = Array.prototype.slice,
isArray = Array.isArray,
args = function (_args) {
return isArray(_args) ? _args : slice.call(_args);
};
var NameFilter, Finder;
function _matchNoPatterns(str, patterns) {
var i, length, pattern;
for (i = 0, length = patterns.length; i < length; i += 1) {
pattern = patterns[i];
if (pattern.test(str)) {
return false;
}
}
return true;
}
function _walkRecursiveAsync(filePath, filter, callback, _depth, _env) {
_env.leafs += 1;
fs.stat(filePath, function (error, stats) {
var fileInfo = {file: filePath, stat: stats, depth: _depth};
//There is an error
if (error) {
_env.leafs -= 1;
if (_env.leafs <= 0) {
callback(null, _env.result);
}
return;
}
//For file
if (stats.isFile()) {
if (filter(fileInfo)) {
_env.result.push(fileInfo);
}
_env.leafs -= 1;
if (_env.leafs <= 0) {
callback(null, _env.result);
return;
}
//For directory
} else if (stats.isDirectory()) {
if (filter(fileInfo)) {
if (_depth > 0) {
_env.result.push(fileInfo);
}
// Directory - walk recursive
fs.readdir(filePath, function (error, files) {
var i, length;
_env.leafs -= 1;
for (i = 0; i < files.length; i += 1) {
_walkRecursiveAsync(path.join(filePath, files[i]), filter, callback, _depth + 1, _env);
}
//Check if finished
if (_env.leafs <= 0) {
callback(null, _env.result);
return;
}
});
} else {
_env.leafs -= 1;
}
} else {
console.log('TODO implement this type' + stats);
}
if (_env.leafs <= 0) {
callback(null, _env.result);
}
});
}
function _walkRecursiveSync(filePath, filter, _depth) {
var fileInfo, results;
results = [];
fileInfo = {file: filePath, stat: fs.statSync(filePath), depth: _depth};
if (fileInfo.stat.isFile()) {
if (filter(fileInfo)) {
results.push(fileInfo);
}
} else if (fileInfo.stat.isDirectory()) {
if (filter(fileInfo)) {
if (_depth > 0) {
results.push(fileInfo);
}
// Directory - walk recursive
fs.readdirSync(filePath).forEach(function (file) {
results = results.concat(_walkRecursiveSync(path.join(filePath, file), filter, _depth + 1));
});
}
}
return results;
}
/*******************************************************************************
* NameFilter class
*
*
* Usage:
*
* @class
******************************************************************************/
/**
* @constructor
*/
function NameFilter(matchMode, names) {
names = slice.call(arguments);
this._matchMode = NameFilter.MODE_ALL;
if (matchMode === NameFilter.MODE_ALL || matchMode === NameFilter.MODE_ANY) {
this._matchMode = matchMode;
names = names.slice(1);
}
this._names = [];
this.addNames(names);
}
/**
*
* @param names
* @return
*/
NameFilter.prototype.addNames = function (names) {
names = slice.call(arguments);
if (isArray(names[0])) {
names = names[0];
}
names.forEach(function (filter) {
if (filter instanceof NameFilter) {
this._names.push(filter);
} else {
this._names.push(NameFilter._toRegExp(filter));
}
}, this);
return this;
};
/**
*
* @param {string} str
* @return {boolean}
*/
NameFilter.prototype.test = function (str) {
var i, length, pattern, patterns = this._names;
if (this._matchMode === NameFilter.MODE_ALL) {
for (i = 0, length = patterns.length; i < length; i += 1) {
pattern = patterns[i];
if (!pattern.test(str)) {//Can be RegExp or other NameFilter
return false;
}
}
return true;
} else {
for (i = 0, length = patterns.length; i < length; i += 1) {
pattern = patterns[i];
if (pattern.test(str)) {//Can be RegExp or other NameFilter
return true;
}
}
return false;
}
};
NameFilter.MODE_ALL = false;
NameFilter.MODE_ANY = true;
NameFilter._toRegExp = function (stringOrRegexp) {
if (stringOrRegexp instanceof RegExp) {
return stringOrRegexp;
}
if (typeof(stringOrRegexp) !== 'string') {
throw new Error(stringOrRegexp + ' should be a string or RegExp');
}
if (stringOrRegexp.match('/^([^a-zA-Z0-9\\\\]).+?\\1[ims]?$/')) {
return stringOrRegexp;
}
var strictLeadingDot = true,
strictWildcardSlash = true,
firstByte = true,
escaping = false,
inCurlies = 0,
regexp = '',
char, i;
for (i = 0; i < stringOrRegexp.length; i += 1) {
char = stringOrRegexp[i];
if (firstByte) {
if (strictLeadingDot && char !== '.') {
regexp += "(?=[^\\.])";// (?=[^\.])
}
firstByte = false;
}
if (char === '/') {
firstByte = true;
}
if (char === '.' || char === '(' || char === ')' || char === '|' || char === '+' || char === '^' || char === '$') {
regexp += "\\" + char + "";
} else if (char === '*') {
regexp += escaping ? '\\*' : (strictWildcardSlash ? '[^/]*' : '.*');
} else if (char === '?') {
regexp += escaping ? '\\?' : (strictWildcardSlash ? '[^/]' : '.');
} else if (char === '{') {
regexp += escaping ? '\\{' : '(';
if (!escaping) {
inCurlies += 1;
}
} else if (char === '}' && inCurlies) {
regexp += escaping ? '}' : ')';
if (!escaping) {
inCurlies -= 1;
}
} else if (char === ',' && inCurlies) {
regexp += escaping ? ',' : '|';
} else if (char === '\\') {
if (escaping) {
regexp += '\\\\';
escaping = false;
} else {
escaping = true;
}
continue;
} else {
regexp += char;
}
escaping = false;
}
return new RegExp('^' + regexp + '$');
};
/*******************************************************************************
* Finder class
*
*
* Usage:
*
* <pre>
*
* //Async
*
* var finderInstance = new Finder();
* finderInstance.type(Finder.FILE).names('*.js').fetch(__dirname, function (error, files) {
* files.forEach(function (file) {
* console.log(file);
* });
* });
*
* //Sync
*
* console.log(finderInstance.type(Finder.DIR).notNames('.svn').fetchSync());
*
* </pre>
*
* @class
******************************************************************************/
/**
* Finder constructor
*
* @constructor
*/
function Finder() {
this.reset();
}
Finder.FILE = 'file';
Finder.DIR = 'dir';
Finder.ANY = 'any';
/**
* Reset all parameters
*
* @return this
*/
Finder.prototype.reset = function () {
this._type = Finder.ANY;
this._nameList = new NameFilter(NameFilter.MODE_ALL);
this._namesNot = [];
this._filters = [];
this._includes = [];
this._excludes = [];
this._depthMin = null;
this._depthMax = null;
return this;
};
/**
* Filter by type
*
* @param {int} type Finder.DIR|Finder.FILE|Finder.ANY
* @return this
*/
Finder.prototype.type = function (type) {
if (type !== Finder.DIR && type !== Finder.FILE && type !== Finder.ANY) {
throw new Error('type should be in [Finder.DIR, Finder.FILE, Finder.ANY]');
}
this._type = type;
return this;
};
/**
* Filter with depth between depthMin and depthMax
*
* @param {int} depthMin
* @param {int} depthMax
* @return this
*/
Finder.prototype.depth = function (depthMin, depthMax) {
if (depthMin !== undefined && (typeof(depthMin) !== 'number' || depthMin < 0)) {
throw new Error('depthMin should be a positive number');
}
if (depthMax !== undefined && (typeof(depthMax) !== 'number' || depthMax < 0)) {
throw new Error('depthMax should be a positive number');
}
this._depthMin = depthMin;
this._depthMax = depthMax;
return this;
};
/**
* Include by name
*
* @param {string|RegExp, ...}
* @return this
*/
Finder.prototype.names = function () {
this._nameList.addNames.apply(this._nameList, arguments);
return this;
};
/**
* Exclude by name
*
* @param {string|RegExp, ...}
* @return this
*/
Finder.prototype.notNames = function () {
var patterns = args(arguments);
patterns.forEach(function (pattern) {
this._namesNot.push(NameFilter._toRegExp(pattern));
}, this);
return this;
};
/**
* Add a custom filter
*
* @param {Function} filter
* @return this
*/
Finder.prototype.filter = function (filter) {
if (!(filter instanceof Function)) {
throw new Error('filter should be Function');
}
this._filters.push(filter);
return this;
};
/**
* Exclude directories from the
*
* @param {string|RegExp, ...}
* @return
*/
Finder.prototype.exclude = function () {
var patterns = args(arguments),
excludes = this._excludes;
patterns.forEach(function (pattern) {
excludes.push(NameFilter._toRegExp(pattern));
}, this);
return this;
};
/**
* Fetch results asynchronously
*
* @param {Array|string} directories
* @param {Function} callback
* @return this
*/
Finder.prototype.fetch = function (directories, callback) {
if (!isArray(directories)) {
directories = [directories];
}
if (directories.length === 0) {
callback(null, []);
return this;
}
var onWalkCount, onWalk,
results = [],
preFilter = this._preFilter.bind(this);
onWalkCount = directories.length;
onWalk = function (error, files) {
onWalkCount -= 1;
results = results.concat(files);
if (onWalkCount <= 0 && callback) {
results = results.filter(this._postFilter, this);
results = results.map(function (result) {
return result.file;
});
results.sort();
callback(null, results);
}
}.bind(this);
directories.forEach(function (directory) {
_walkRecursiveAsync(
directory,
preFilter,
onWalk,
0, {
result: [],
leafs: 0
}
);
});
return this;
};
/**
* Fetch results synchronously
*
* @param {Array|string} directories
* @return
*/
Finder.prototype.fetchSync = function (directories) {
if (!isArray(directories)) {
directories = [directories];
}
if (directories.length === 0) {
return [];
}
var results = [], directoryResult = [], preFilter = this._preFilter.bind(this);
directories.forEach(function (directory) {
var directoryResult = _walkRecursiveSync(
directory,
preFilter,
0);
directoryResult = directoryResult.filter(this._postFilter, this);
directoryResult = directoryResult.map(function (result) {
return result.file;
});
results = results.concat(directoryResult);
}, this);
results.sort();
return results;
};
Finder.prototype._postFilter = function (file) {
var filePath, filePathBase, stat, depth, i;
//filter type
stat = file.stat;
if (this._type !== Finder.ANY) {
if ((this._type === Finder.FILE) && !stat.isFile()) {
return false;
}
if ((this._type === Finder.DIR) && !stat.isDirectory()) {
return false;
}
}
//Filter depth
depth = file.depth;
if (this._depthMin !== null && depth < this._depthMin) {
return false;
}
//Name filters
filePath = file.file;
filePathBase = path.basename(filePath);
if (!this._nameList.test(filePathBase)) {
return false;
}
if (!_matchNoPatterns(filePathBase, this._namesNot)) {
return false;
}
//Custom filters
for (i = 0; i < this._filters.length; i += 1) {
if (!this._filters[i](file)) {
return false;
}
}
return true;
};
Finder.prototype._preFilter = function (file) {
if (!_matchNoPatterns(path.basename(file.file), this._excludes)) {
return false;
}
//Filter depth
if (this._depthMax !== null && file.depth > this._depthMax) {
return false;
}
return true;
};
/**
* Exports
*/
exports.Finder = Finder;
exports.NameFilter = NameFilter;