| 'use strict'; |
| |
| var fs = require('graceful-fs') |
| , path = require('path') |
| , micromatch = require('micromatch').isMatch |
| , toString = Object.prototype.toString |
| ; |
| |
| |
| // Standard helpers |
| function isFunction (obj) { |
| return toString.call(obj) === '[object Function]'; |
| } |
| |
| function isString (obj) { |
| return toString.call(obj) === '[object String]'; |
| } |
| |
| function isUndefined (obj) { |
| return obj === void 0; |
| } |
| |
| /** |
| * Main function which ends up calling readdirRec and reads all files and directories in given root recursively. |
| * @param { Object } opts Options to specify root (start directory), filters and recursion depth |
| * @param { function } callback1 When callback2 is given calls back for each processed file - function (fileInfo) { ... }, |
| * when callback2 is not given, it behaves like explained in callback2 |
| * @param { function } callback2 Calls back once all files have been processed with an array of errors and file infos |
| * function (err, fileInfos) { ... } |
| */ |
| function readdir(opts, callback1, callback2) { |
| var stream |
| , handleError |
| , handleFatalError |
| , errors = [] |
| , readdirResult = { |
| directories: [] |
| , files: [] |
| } |
| , fileProcessed |
| , allProcessed |
| , realRoot |
| , aborted = false |
| , paused = false |
| ; |
| |
| // If no callbacks were given we will use a streaming interface |
| if (isUndefined(callback1)) { |
| var api = require('./stream-api')(); |
| stream = api.stream; |
| callback1 = api.processEntry; |
| callback2 = api.done; |
| handleError = api.handleError; |
| handleFatalError = api.handleFatalError; |
| |
| stream.on('close', function () { aborted = true; }); |
| stream.on('pause', function () { paused = true; }); |
| stream.on('resume', function () { paused = false; }); |
| } else { |
| handleError = function (err) { errors.push(err); }; |
| handleFatalError = function (err) { |
| handleError(err); |
| allProcessed(errors, null); |
| }; |
| } |
| |
| if (isUndefined(opts)){ |
| handleFatalError(new Error ( |
| 'Need to pass at least one argument: opts! \n' + |
| 'https://github.com/paulmillr/readdirp#options' |
| ) |
| ); |
| return stream; |
| } |
| |
| opts.root = opts.root || '.'; |
| opts.fileFilter = opts.fileFilter || function() { return true; }; |
| opts.directoryFilter = opts.directoryFilter || function() { return true; }; |
| opts.depth = typeof opts.depth === 'undefined' ? 999999999 : opts.depth; |
| opts.entryType = opts.entryType || 'files'; |
| |
| var statfn = opts.lstat === true ? fs.lstat.bind(fs) : fs.stat.bind(fs); |
| |
| if (isUndefined(callback2)) { |
| fileProcessed = function() { }; |
| allProcessed = callback1; |
| } else { |
| fileProcessed = callback1; |
| allProcessed = callback2; |
| } |
| |
| function normalizeFilter (filter) { |
| |
| if (isUndefined(filter)) return undefined; |
| |
| function isNegated (filters) { |
| |
| function negated(f) { |
| return f.indexOf('!') === 0; |
| } |
| |
| var some = filters.some(negated); |
| if (!some) { |
| return false; |
| } else { |
| if (filters.every(negated)) { |
| return true; |
| } else { |
| // if we detect illegal filters, bail out immediately |
| throw new Error( |
| 'Cannot mix negated with non negated glob filters: ' + filters + '\n' + |
| 'https://github.com/paulmillr/readdirp#filters' |
| ); |
| } |
| } |
| } |
| |
| // Turn all filters into a function |
| if (isFunction(filter)) { |
| |
| return filter; |
| |
| } else if (isString(filter)) { |
| |
| return function (entryInfo) { |
| return micromatch(entryInfo.name, filter.trim()); |
| }; |
| |
| } else if (filter && Array.isArray(filter)) { |
| |
| if (filter) filter = filter.map(function (f) { |
| return f.trim(); |
| }); |
| |
| return isNegated(filter) ? |
| // use AND to concat multiple negated filters |
| function (entryInfo) { |
| return filter.every(function (f) { |
| return micromatch(entryInfo.name, f); |
| }); |
| } |
| : |
| // use OR to concat multiple inclusive filters |
| function (entryInfo) { |
| return filter.some(function (f) { |
| return micromatch(entryInfo.name, f); |
| }); |
| }; |
| } |
| } |
| |
| function processDir(currentDir, entries, callProcessed) { |
| if (aborted) return; |
| var total = entries.length |
| , processed = 0 |
| , entryInfos = [] |
| ; |
| |
| fs.realpath(currentDir, function(err, realCurrentDir) { |
| if (aborted) return; |
| if (err) { |
| handleError(err); |
| callProcessed(entryInfos); |
| return; |
| } |
| |
| var relDir = path.relative(realRoot, realCurrentDir); |
| |
| if (entries.length === 0) { |
| callProcessed([]); |
| } else { |
| entries.forEach(function (entry) { |
| |
| var fullPath = path.join(realCurrentDir, entry) |
| , relPath = path.join(relDir, entry); |
| |
| statfn(fullPath, function (err, stat) { |
| if (err) { |
| handleError(err); |
| } else { |
| entryInfos.push({ |
| name : entry |
| , path : relPath // relative to root |
| , fullPath : fullPath |
| |
| , parentDir : relDir // relative to root |
| , fullParentDir : realCurrentDir |
| |
| , stat : stat |
| }); |
| } |
| processed++; |
| if (processed === total) callProcessed(entryInfos); |
| }); |
| }); |
| } |
| }); |
| } |
| |
| function readdirRec(currentDir, depth, callCurrentDirProcessed) { |
| var args = arguments; |
| if (aborted) return; |
| if (paused) { |
| setImmediate(function () { |
| readdirRec.apply(null, args); |
| }) |
| return; |
| } |
| |
| fs.readdir(currentDir, function (err, entries) { |
| if (err) { |
| handleError(err); |
| callCurrentDirProcessed(); |
| return; |
| } |
| |
| processDir(currentDir, entries, function(entryInfos) { |
| |
| var subdirs = entryInfos |
| .filter(function (ei) { return ei.stat.isDirectory() && opts.directoryFilter(ei); }); |
| |
| subdirs.forEach(function (di) { |
| if(opts.entryType === 'directories' || opts.entryType === 'both' || opts.entryType === 'all') { |
| fileProcessed(di); |
| } |
| readdirResult.directories.push(di); |
| }); |
| |
| entryInfos |
| .filter(function(ei) { |
| var isCorrectType = opts.entryType === 'all' ? |
| !ei.stat.isDirectory() : ei.stat.isFile() || ei.stat.isSymbolicLink(); |
| return isCorrectType && opts.fileFilter(ei); |
| }) |
| .forEach(function (fi) { |
| if(opts.entryType === 'files' || opts.entryType === 'both' || opts.entryType === 'all') { |
| fileProcessed(fi); |
| } |
| readdirResult.files.push(fi); |
| }); |
| |
| var pendingSubdirs = subdirs.length; |
| |
| // Be done if no more subfolders exist or we reached the maximum desired depth |
| if(pendingSubdirs === 0 || depth === opts.depth) { |
| callCurrentDirProcessed(); |
| } else { |
| // recurse into subdirs, keeping track of which ones are done |
| // and call back once all are processed |
| subdirs.forEach(function (subdir) { |
| readdirRec(subdir.fullPath, depth + 1, function () { |
| pendingSubdirs = pendingSubdirs - 1; |
| if(pendingSubdirs === 0) { |
| callCurrentDirProcessed(); |
| } |
| }); |
| }); |
| } |
| }); |
| }); |
| } |
| |
| // Validate and normalize filters |
| try { |
| opts.fileFilter = normalizeFilter(opts.fileFilter); |
| opts.directoryFilter = normalizeFilter(opts.directoryFilter); |
| } catch (err) { |
| // if we detect illegal filters, bail out immediately |
| handleFatalError(err); |
| return stream; |
| } |
| |
| // If filters were valid get on with the show |
| fs.realpath(opts.root, function(err, res) { |
| if (err) { |
| handleFatalError(err); |
| return stream; |
| } |
| |
| realRoot = res; |
| readdirRec(opts.root, 0, function () { |
| // All errors are collected into the errors array |
| if (errors.length > 0) { |
| allProcessed(errors, readdirResult); |
| } else { |
| allProcessed(null, readdirResult); |
| } |
| }); |
| }); |
| |
| return stream; |
| } |
| |
| module.exports = readdir; |