var fs = require('fs'); var path = require('path'); var Chain = require('traverse-chain'); /** * Outline the APIs. */ var find = module.exports = { // file: function([pat,] root, callback) {} // dir: function([pat,] root, callback) {} // eachfile: function([pat,] root, action) {} // eachdir: function([pat,] root, action) {} // fileSync: function([pat,] root) {} // dirSync: function([pat,] root) {} // use:: function(options) {} }; var fss = {}; /** * Error handler wrapper. */ fss.errorHandler = function(err) { if (err) { if (find.__errorHandler) { find.__errorHandler(err); } else { throw err; } } }; var error = { notExist: function(name) { return new Error(name + ' does not exist.'); } }; var is = (function() { function existed(name) { return fs.existsSync(name); } function fsType(type) { return function(name) { try { return fs.lstatSync(name)['is' + type](); } catch(err) { if (!/^(EPERM|EACCES)$/.test(err.code)) { fss.errorHandler(err); } else { console.warn('Warning: Cannot access %s', name); } } } } function objType(type) { return function(input) { if (type === 'Function') { return typeof input === 'function'; } return ({}).toString.call(input) === '[object ' + type + ']'; } } return { existed: existed, file: fsType('File'), directory: fsType('Directory'), symbolicLink: fsType('SymbolicLink'), string: objType('String'), regexp: objType('RegExp'), func: objType('Function') }; }()); /** * Method injection for handling errors. */ ['readdir', 'lstat'].forEach(function(method) { fss[method] = function(path, callback) { var origin = fs[method]; return origin.apply(fs, [path, function(err) { fss.errorHandler(err); return callback.apply(null, arguments); }]); } }); /** * Enhancement for fs.readlink && fs.readlinkSync. */ fss.readlink = function(name, fn, depth) { if (depth == undefined) depth = 5; if (!is.existed(name) && (depth < 5)) { return fn(path.resolve(name)); } var isSymbolicLink = is.symbolicLink(name); if (!isSymbolicLink) { fn(path.resolve(name)); } else if (depth) { fs.realpath(name, function(err, origin) { if (err && /^(ENOENT|ELOOP|EPERM|EACCES)$/.test(err.code)) { fn(name); } else { if (err) { fss.errorHandler(err); } else { fss.readlink(origin, fn, --depth); } } }); } else { fn(isSymbolicLink ? '' : path.resolve(name)); } } fss.readlinkSync = function(name, depth) { if (depth == undefined) depth = 5; if (!is.existed(name) && depth < 5) { return path.resolve(name); } var isSymbolicLink = is.symbolicLink(name); if (!isSymbolicLink) { return path.resolve(name); } else if (depth) { var origin; try { origin = fs.realpathSync(name); } catch (err) { if (/^(ENOENT|ELOOP|EPERM|EACCES)$/.test(err.code)) { return name; } else { fss.errorHandler(err); } } return fss.readlinkSync(origin, --depth); } else { return isSymbolicLink ? '' : path.resolve(name); } } /** * Check pattern against the path */ var compare = function(pat, name) { var str = path.basename(name); return ( is.regexp(pat) && pat.test(name) || is.string(pat) && pat === str ); }; /** * Traverse a directory recursively and asynchronously. * * @param {String} root * @param {String} type * @param {Function} action * @param {Function} callback * @param {Chain} c * @api private */ var traverseAsync = function(root, type, action, callback, c) { if (!is.existed(root)) { fss.errorHandler(error.notExist(root)) } var originRoot = root; if (is.symbolicLink(root)) { root = fss.readlinkSync(root); } if (is.directory(root)) { fss.readdir(root, function(err, all) { var chain = Chain(); all && all.forEach(function(dir) { dir = path.join(originRoot, dir); chain.add(function() { var handleFile = function() { if (type == 'file') action(dir); process.nextTick(function() { chain.next() }); } var handleDir = function(skip) { if (type == 'dir') action(dir); if (skip) chain.next(); else process.nextTick(function() { traverseAsync(dir, type, action, callback, chain)}); } var isSymbolicLink = is.symbolicLink(dir); if (is.directory(dir)) { handleDir(); } else if (isSymbolicLink) { fss.readlink(dir, function(origin) { if (origin) { if (is.existed(origin) && is.directory(origin)) { handleDir(isSymbolicLink) } else { handleFile() } } else { chain.next(); } }); } else { handleFile(); } }) }); chain.traverse(function() { c ? c.next() : callback(); }); }); } } /** * Traverse a directory recursively. * * @param {String} root * @param {String} type * @param {Function} action * @return {Array} the result * @api private */ var traverseSync = function(root, type, action) { if (!is.existed(root)) throw error.notExist(root); var originRoot = root; if (is.symbolicLink(root)) { root = fss.readlinkSync(root); } if (is.directory(root)) { fs.readdirSync(root).forEach(function(dir) { dir = path.join(originRoot, dir); var handleDir = function(skip) { if (type == 'dir') action(dir); if (skip) return; traverseSync(dir, type, action); } var handleFile = function() { if (type == 'file') action(dir); } var isSymbolicLink = is.symbolicLink(dir); if (is.directory(dir)) { handleDir(); } else if (isSymbolicLink) { var origin = fss.readlinkSync(dir); if (origin) { if (is.existed(origin) && is.directory(origin)) { handleDir(isSymbolicLink); } else { handleFile(); } } } else { handleFile(); } }); } }; ['file', 'dir'].forEach(function(type) { /** * `find.file` and `find.dir` * * Find files or sub-directories in a given directory and * passes the result in an array as a whole. This follows * the default callback style of nodejs, think about `fs.readdir`, * * @param {RegExp|String} pat * @param {String} root * @param {Function} fn * @api public */ find[type] = function(pat, root, fn) { var buffer = []; if (arguments.length == 2) { fn = root; root = pat; pat = ''; } process.nextTick(function() { traverseAsync( root , type , function(n) { buffer.push(n);} , function() { if (is.func(fn) && pat) { fn(buffer.filter(function(n) { return compare(pat, n); })); } else { fn(buffer); } } ); }); return { error: function(handler) { if (is.func(handler)) { find.__errorHandler = handler; } } } } /** * `find.eachfile` and `find.eachdir` * * Find files or sub-directories in a given directory and * apply with a given action to each result immediately * rather than pass them back as an array. * * @param {RegExp|String} pat * @param {String} root * @param {Function} action * @return {Object} for chain methods * @api public * */ find['each' + type] = function(pat, root, action) { var callback = function() {} if (arguments.length == 2) { action = root; root = pat; pat = ''; } process.nextTick(function() { traverseAsync( root , type , function(n) { if (!is.func(action)) return; if (!pat || compare(pat, n)) { action(n); } } , callback ); }); return { end: function(fn) { if (is.func(fn)) { callback = fn; } return this; }, error: function(handler) { if (is.func(handler)) { find.__errorHandler = handler; } return this; } }; } /** * `find.fileSync` and `find.dirSync` * * Find files or sub-directories in a given directory synchronously * and returns the result as an array. This follows the default 'Sync' * methods of nodejs, think about `fs.readdirSync`, * * @param {RegExp|String} pat * @param {String} root * @return {Array} the result * @api public */ find[type + 'Sync'] = function(pat, root) { var buffer = []; if (arguments.length == 1) { root = pat; pat = ''; } traverseSync(root, type, function(n) { buffer.push(n); }); return pat && buffer.filter(function(n) { return compare(pat, n); }) || buffer; } }); var fsMethods = [ 'existsSync', 'lstatSync', 'realpath', 'realpathSync', 'readdir', 'readdirSync' ]; /** * Configuations for internal usage * * @param {Object} options * @api public */ find.use = function(options) { if (options && options.fs) { if (fsMethods.every(n => !!options.fs[n])) { fs = options.fs; } else { throw new Error('The provided fs object is not compatiable with native fs.'); } } return find; }