123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  1. 'use strict';
  2. var fs = require('graceful-fs')
  3. , path = require('path')
  4. , minimatch = require('minimatch')
  5. , toString = Object.prototype.toString
  6. , si = require('set-immediate-shim')
  7. ;
  8. // Standard helpers
  9. function isFunction (obj) {
  10. return toString.call(obj) === '[object Function]';
  11. }
  12. function isString (obj) {
  13. return toString.call(obj) === '[object String]';
  14. }
  15. function isRegExp (obj) {
  16. return toString.call(obj) === '[object RegExp]';
  17. }
  18. function isUndefined (obj) {
  19. return obj === void 0;
  20. }
  21. /**
  22. * Main function which ends up calling readdirRec and reads all files and directories in given root recursively.
  23. * @param { Object } opts Options to specify root (start directory), filters and recursion depth
  24. * @param { function } callback1 When callback2 is given calls back for each processed file - function (fileInfo) { ... },
  25. * when callback2 is not given, it behaves like explained in callback2
  26. * @param { function } callback2 Calls back once all files have been processed with an array of errors and file infos
  27. * function (err, fileInfos) { ... }
  28. */
  29. function readdir(opts, callback1, callback2) {
  30. var stream
  31. , handleError
  32. , handleFatalError
  33. , pending = 0
  34. , errors = []
  35. , readdirResult = {
  36. directories: []
  37. , files: []
  38. }
  39. , fileProcessed
  40. , allProcessed
  41. , realRoot
  42. , aborted = false
  43. , paused = false
  44. ;
  45. // If no callbacks were given we will use a streaming interface
  46. if (isUndefined(callback1)) {
  47. var api = require('./stream-api')();
  48. stream = api.stream;
  49. callback1 = api.processEntry;
  50. callback2 = api.done;
  51. handleError = api.handleError;
  52. handleFatalError = api.handleFatalError;
  53. stream.on('close', function () { aborted = true; });
  54. stream.on('pause', function () { paused = true; });
  55. stream.on('resume', function () { paused = false; });
  56. } else {
  57. handleError = function (err) { errors.push(err); };
  58. handleFatalError = function (err) {
  59. handleError(err);
  60. allProcessed(errors, null);
  61. };
  62. }
  63. if (isUndefined(opts)){
  64. handleFatalError(new Error (
  65. 'Need to pass at least one argument: opts! \n' +
  66. 'https://github.com/thlorenz/readdirp#options'
  67. )
  68. );
  69. return stream;
  70. }
  71. opts.root = opts.root || '.';
  72. opts.fileFilter = opts.fileFilter || function() { return true; };
  73. opts.directoryFilter = opts.directoryFilter || function() { return true; };
  74. opts.depth = typeof opts.depth === 'undefined' ? 999999999 : opts.depth;
  75. opts.entryType = opts.entryType || 'files';
  76. var statfn = opts.lstat === true ? fs.lstat.bind(fs) : fs.stat.bind(fs);
  77. if (isUndefined(callback2)) {
  78. fileProcessed = function() { };
  79. allProcessed = callback1;
  80. } else {
  81. fileProcessed = callback1;
  82. allProcessed = callback2;
  83. }
  84. function normalizeFilter (filter) {
  85. if (isUndefined(filter)) return undefined;
  86. function isNegated (filters) {
  87. function negated(f) {
  88. return f.indexOf('!') === 0;
  89. }
  90. var some = filters.some(negated);
  91. if (!some) {
  92. return false;
  93. } else {
  94. if (filters.every(negated)) {
  95. return true;
  96. } else {
  97. // if we detect illegal filters, bail out immediately
  98. throw new Error(
  99. 'Cannot mix negated with non negated glob filters: ' + filters + '\n' +
  100. 'https://github.com/thlorenz/readdirp#filters'
  101. );
  102. }
  103. }
  104. }
  105. // Turn all filters into a function
  106. if (isFunction(filter)) {
  107. return filter;
  108. } else if (isString(filter)) {
  109. return function (entryInfo) {
  110. return minimatch(entryInfo.name, filter.trim());
  111. };
  112. } else if (filter && Array.isArray(filter)) {
  113. if (filter) filter = filter.map(function (f) {
  114. return f.trim();
  115. });
  116. return isNegated(filter) ?
  117. // use AND to concat multiple negated filters
  118. function (entryInfo) {
  119. return filter.every(function (f) {
  120. return minimatch(entryInfo.name, f);
  121. });
  122. }
  123. :
  124. // use OR to concat multiple inclusive filters
  125. function (entryInfo) {
  126. return filter.some(function (f) {
  127. return minimatch(entryInfo.name, f);
  128. });
  129. };
  130. }
  131. }
  132. function processDir(currentDir, entries, callProcessed) {
  133. if (aborted) return;
  134. var total = entries.length
  135. , processed = 0
  136. , entryInfos = []
  137. ;
  138. fs.realpath(currentDir, function(err, realCurrentDir) {
  139. if (aborted) return;
  140. if (err) {
  141. handleError(err);
  142. callProcessed(entryInfos);
  143. return;
  144. }
  145. var relDir = path.relative(realRoot, realCurrentDir);
  146. if (entries.length === 0) {
  147. callProcessed([]);
  148. } else {
  149. entries.forEach(function (entry) {
  150. var fullPath = path.join(realCurrentDir, entry)
  151. , relPath = path.join(relDir, entry);
  152. statfn(fullPath, function (err, stat) {
  153. if (err) {
  154. handleError(err);
  155. } else {
  156. entryInfos.push({
  157. name : entry
  158. , path : relPath // relative to root
  159. , fullPath : fullPath
  160. , parentDir : relDir // relative to root
  161. , fullParentDir : realCurrentDir
  162. , stat : stat
  163. });
  164. }
  165. processed++;
  166. if (processed === total) callProcessed(entryInfos);
  167. });
  168. });
  169. }
  170. });
  171. }
  172. function readdirRec(currentDir, depth, callCurrentDirProcessed) {
  173. var args = arguments;
  174. if (aborted) return;
  175. if (paused) {
  176. si(function () {
  177. readdirRec.apply(null, args);
  178. })
  179. return;
  180. }
  181. fs.readdir(currentDir, function (err, entries) {
  182. if (err) {
  183. handleError(err);
  184. callCurrentDirProcessed();
  185. return;
  186. }
  187. processDir(currentDir, entries, function(entryInfos) {
  188. var subdirs = entryInfos
  189. .filter(function (ei) { return ei.stat.isDirectory() && opts.directoryFilter(ei); });
  190. subdirs.forEach(function (di) {
  191. if(opts.entryType === 'directories' || opts.entryType === 'both' || opts.entryType === 'all') {
  192. fileProcessed(di);
  193. }
  194. readdirResult.directories.push(di);
  195. });
  196. entryInfos
  197. .filter(function(ei) {
  198. var isCorrectType = opts.entryType === 'all' ?
  199. !ei.stat.isDirectory() : ei.stat.isFile() || ei.stat.isSymbolicLink();
  200. return isCorrectType && opts.fileFilter(ei);
  201. })
  202. .forEach(function (fi) {
  203. if(opts.entryType === 'files' || opts.entryType === 'both' || opts.entryType === 'all') {
  204. fileProcessed(fi);
  205. }
  206. readdirResult.files.push(fi);
  207. });
  208. var pendingSubdirs = subdirs.length;
  209. // Be done if no more subfolders exist or we reached the maximum desired depth
  210. if(pendingSubdirs === 0 || depth === opts.depth) {
  211. callCurrentDirProcessed();
  212. } else {
  213. // recurse into subdirs, keeping track of which ones are done
  214. // and call back once all are processed
  215. subdirs.forEach(function (subdir) {
  216. readdirRec(subdir.fullPath, depth + 1, function () {
  217. pendingSubdirs = pendingSubdirs - 1;
  218. if(pendingSubdirs === 0) {
  219. callCurrentDirProcessed();
  220. }
  221. });
  222. });
  223. }
  224. });
  225. });
  226. }
  227. // Validate and normalize filters
  228. try {
  229. opts.fileFilter = normalizeFilter(opts.fileFilter);
  230. opts.directoryFilter = normalizeFilter(opts.directoryFilter);
  231. } catch (err) {
  232. // if we detect illegal filters, bail out immediately
  233. handleFatalError(err);
  234. return stream;
  235. }
  236. // If filters were valid get on with the show
  237. fs.realpath(opts.root, function(err, res) {
  238. if (err) {
  239. handleFatalError(err);
  240. return stream;
  241. }
  242. realRoot = res;
  243. readdirRec(opts.root, 0, function () {
  244. // All errors are collected into the errors array
  245. if (errors.length > 0) {
  246. allProcessed(errors, readdirResult);
  247. } else {
  248. allProcessed(null, readdirResult);
  249. }
  250. });
  251. });
  252. return stream;
  253. }
  254. module.exports = readdir;