readdirp.js 8.3 KB

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