| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380 | 
							- 'use strict';
 
- const Readable = require('stream').Readable;
 
- const EventEmitter = require('events').EventEmitter;
 
- const path = require('path');
 
- const normalizeOptions = require('./normalize-options');
 
- const stat = require('./stat');
 
- const call = require('./call');
 
- /**
 
-  * Asynchronously reads the contents of a directory and streams the results
 
-  * via a {@link stream.Readable}.
 
-  */
 
- class DirectoryReader {
 
-   /**
 
-    * @param {string} dir - The absolute or relative directory path to read
 
-    * @param {object} [options] - User-specified options, if any (see {@link normalizeOptions})
 
-    * @param {object} internalOptions - Internal options that aren't part of the public API
 
-    * @class
 
-    */
 
-   constructor (dir, options, internalOptions) {
 
-     this.options = options = normalizeOptions(options, internalOptions);
 
-     // Indicates whether we should keep reading
 
-     // This is set false if stream.Readable.push() returns false.
 
-     this.shouldRead = true;
 
-     // The directories to read
 
-     // (initialized with the top-level directory)
 
-     this.queue = [{
 
-       path: dir,
 
-       basePath: options.basePath,
 
-       posixBasePath: options.posixBasePath,
 
-       depth: 0
 
-     }];
 
-     // The number of directories that are currently being processed
 
-     this.pending = 0;
 
-     // The data that has been read, but not yet emitted
 
-     this.buffer = [];
 
-     this.stream = new Readable({ objectMode: true });
 
-     this.stream._read = () => {
 
-       // Start (or resume) reading
 
-       this.shouldRead = true;
 
-       // If we have data in the buffer, then send the next chunk
 
-       if (this.buffer.length > 0) {
 
-         this.pushFromBuffer();
 
-       }
 
-       // If we have directories queued, then start processing the next one
 
-       if (this.queue.length > 0) {
 
-         if (this.options.facade.sync) {
 
-           while (this.queue.length > 0) {
 
-             this.readNextDirectory();
 
-           }
 
-         }
 
-         else {
 
-           this.readNextDirectory();
 
-         }
 
-       }
 
-       this.checkForEOF();
 
-     };
 
-   }
 
-   /**
 
-    * Reads the next directory in the queue
 
-    */
 
-   readNextDirectory () {
 
-     let facade = this.options.facade;
 
-     let dir = this.queue.shift();
 
-     this.pending++;
 
-     // Read the directory listing
 
-     call.safe(facade.fs.readdir, dir.path, (err, items) => {
 
-       if (err) {
 
-         // fs.readdir threw an error
 
-         this.emit('error', err);
 
-         return this.finishedReadingDirectory();
 
-       }
 
-       try {
 
-         // Process each item in the directory (simultaneously, if async)
 
-         facade.forEach(
 
-           items,
 
-           this.processItem.bind(this, dir),
 
-           this.finishedReadingDirectory.bind(this, dir)
 
-         );
 
-       }
 
-       catch (err2) {
 
-         // facade.forEach threw an error
 
-         // (probably because fs.readdir returned an invalid result)
 
-         this.emit('error', err2);
 
-         this.finishedReadingDirectory();
 
-       }
 
-     });
 
-   }
 
-   /**
 
-    * This method is called after all items in a directory have been processed.
 
-    *
 
-    * NOTE: This does not necessarily mean that the reader is finished, since there may still
 
-    * be other directories queued or pending.
 
-    */
 
-   finishedReadingDirectory () {
 
-     this.pending--;
 
-     if (this.shouldRead) {
 
-       // If we have directories queued, then start processing the next one
 
-       if (this.queue.length > 0 && this.options.facade.async) {
 
-         this.readNextDirectory();
 
-       }
 
-       this.checkForEOF();
 
-     }
 
-   }
 
-   /**
 
-    * Determines whether the reader has finished processing all items in all directories.
 
-    * If so, then the "end" event is fired (via {@Readable#push})
 
-    */
 
-   checkForEOF () {
 
-     if (this.buffer.length === 0 &&   // The stuff we've already read
 
-     this.pending === 0 &&             // The stuff we're currently reading
 
-     this.queue.length === 0) {        // The stuff we haven't read yet
 
-       // There's no more stuff!
 
-       this.stream.push(null);
 
-     }
 
-   }
 
-   /**
 
-    * Processes a single item in a directory.
 
-    *
 
-    * If the item is a directory, and `option.deep` is enabled, then the item will be added
 
-    * to the directory queue.
 
-    *
 
-    * If the item meets the filter criteria, then it will be emitted to the reader's stream.
 
-    *
 
-    * @param {object} dir - A directory object from the queue
 
-    * @param {string} item - The name of the item (name only, no path)
 
-    * @param {function} done - A callback function that is called after the item has been processed
 
-    */
 
-   processItem (dir, item, done) {
 
-     let stream = this.stream;
 
-     let options = this.options;
 
-     let itemPath = dir.basePath + item;
 
-     let posixPath = dir.posixBasePath + item;
 
-     let fullPath = path.join(dir.path, item);
 
-     // If `options.deep` is a number, and we've already recursed to the max depth,
 
-     // then there's no need to check fs.Stats to know if it's a directory.
 
-     // If `options.deep` is a function, then we'll need fs.Stats
 
-     let maxDepthReached = dir.depth >= options.recurseDepth;
 
-     // Do we need to call `fs.stat`?
 
-     let needStats =
 
-       !maxDepthReached ||                                 // we need the fs.Stats to know if it's a directory
 
-       options.stats ||                                    // the user wants fs.Stats objects returned
 
-       options.recurseFn ||                                // we need fs.Stats for the recurse function
 
-       options.filterFn ||                                 // we need fs.Stats for the filter function
 
-       EventEmitter.listenerCount(stream, 'file') ||       // we need the fs.Stats to know if it's a file
 
-       EventEmitter.listenerCount(stream, 'directory') ||  // we need the fs.Stats to know if it's a directory
 
-       EventEmitter.listenerCount(stream, 'symlink');      // we need the fs.Stats to know if it's a symlink
 
-     // If we don't need stats, then exit early
 
-     if (!needStats) {
 
-       if (this.filter(itemPath, posixPath)) {
 
-         this.pushOrBuffer({ data: itemPath });
 
-       }
 
-       return done();
 
-     }
 
-     // Get the fs.Stats object for this path
 
-     stat(options.facade.fs, fullPath, (err, stats) => {
 
-       if (err) {
 
-         // fs.stat threw an error
 
-         this.emit('error', err);
 
-         return done();
 
-       }
 
-       try {
 
-         // Add the item's path to the fs.Stats object
 
-         // The base of this path, and its separators are determined by the options
 
-         // (i.e. options.basePath and options.sep)
 
-         stats.path = itemPath;
 
-         // Add depth of the path to the fs.Stats object for use this in the filter function
 
-         stats.depth = dir.depth;
 
-         if (this.shouldRecurse(stats, posixPath, maxDepthReached)) {
 
-           // Add this subdirectory to the queue
 
-           this.queue.push({
 
-             path: fullPath,
 
-             basePath: itemPath + options.sep,
 
-             posixBasePath: posixPath + '/',
 
-             depth: dir.depth + 1,
 
-           });
 
-         }
 
-         // Determine whether this item matches the filter criteria
 
-         if (this.filter(stats, posixPath)) {
 
-           this.pushOrBuffer({
 
-             data: options.stats ? stats : itemPath,
 
-             file: stats.isFile(),
 
-             directory: stats.isDirectory(),
 
-             symlink: stats.isSymbolicLink(),
 
-           });
 
-         }
 
-         done();
 
-       }
 
-       catch (err2) {
 
-         // An error occurred while processing the item
 
-         // (probably during a user-specified function, such as options.deep, options.filter, etc.)
 
-         this.emit('error', err2);
 
-         done();
 
-       }
 
-     });
 
-   }
 
-   /**
 
-    * Pushes the given chunk of data to the stream, or adds it to the buffer,
 
-    * depending on the state of the stream.
 
-    *
 
-    * @param {object} chunk
 
-    */
 
-   pushOrBuffer (chunk) {
 
-     // Add the chunk to the buffer
 
-     this.buffer.push(chunk);
 
-     // If we're still reading, then immediately emit the next chunk in the buffer
 
-     // (which may or may not be the chunk that we just added)
 
-     if (this.shouldRead) {
 
-       this.pushFromBuffer();
 
-     }
 
-   }
 
-   /**
 
-    * Immediately pushes the next chunk in the buffer to the reader's stream.
 
-    * The "data" event will always be fired (via {@link Readable#push}).
 
-    * In addition, the "file", "directory", and/or "symlink" events may be fired,
 
-    * depending on the type of properties of the chunk.
 
-    */
 
-   pushFromBuffer () {
 
-     let stream = this.stream;
 
-     let chunk = this.buffer.shift();
 
-     // Stream the data
 
-     try {
 
-       this.shouldRead = stream.push(chunk.data);
 
-     }
 
-     catch (err) {
 
-       this.emit('error', err);
 
-     }
 
-     // Also emit specific events, based on the type of chunk
 
-     chunk.file && this.emit('file', chunk.data);
 
-     chunk.symlink && this.emit('symlink', chunk.data);
 
-     chunk.directory && this.emit('directory', chunk.data);
 
-   }
 
-   /**
 
-    * Determines whether the given directory meets the user-specified recursion criteria.
 
-    * If the user didn't specify recursion criteria, then this function will default to true.
 
-    *
 
-    * @param {fs.Stats} stats - The directory's {@link fs.Stats} object
 
-    * @param {string} posixPath - The item's POSIX path (used for glob matching)
 
-    * @param {boolean} maxDepthReached - Whether we've already crawled the user-specified depth
 
-    * @returns {boolean}
 
-    */
 
-   shouldRecurse (stats, posixPath, maxDepthReached) {
 
-     let options = this.options;
 
-     if (maxDepthReached) {
 
-       // We've already crawled to the maximum depth. So no more recursion.
 
-       return false;
 
-     }
 
-     else if (!stats.isDirectory()) {
 
-       // It's not a directory. So don't try to crawl it.
 
-       return false;
 
-     }
 
-     else if (options.recurseGlob) {
 
-       // Glob patterns are always tested against the POSIX path, even on Windows
 
-       // https://github.com/isaacs/node-glob#windows
 
-       return options.recurseGlob.test(posixPath);
 
-     }
 
-     else if (options.recurseRegExp) {
 
-       // Regular expressions are tested against the normal path
 
-       // (based on the OS or options.sep)
 
-       return options.recurseRegExp.test(stats.path);
 
-     }
 
-     else if (options.recurseFn) {
 
-       try {
 
-         // Run the user-specified recursion criteria
 
-         return options.recurseFn.call(null, stats);
 
-       }
 
-       catch (err) {
 
-         // An error occurred in the user's code.
 
-         // In Sync and Async modes, this will return an error.
 
-         // In Streaming mode, we emit an "error" event, but continue processing
 
-         this.emit('error', err);
 
-       }
 
-     }
 
-     else {
 
-       // No recursion function was specified, and we're within the maximum depth.
 
-       // So crawl this directory.
 
-       return true;
 
-     }
 
-   }
 
-   /**
 
-    * Determines whether the given item meets the user-specified filter criteria.
 
-    * If the user didn't specify a filter, then this function will always return true.
 
-    *
 
-    * @param {string|fs.Stats} value - Either the item's path, or the item's {@link fs.Stats} object
 
-    * @param {string} posixPath - The item's POSIX path (used for glob matching)
 
-    * @returns {boolean}
 
-    */
 
-   filter (value, posixPath) {
 
-     let options = this.options;
 
-     if (options.filterGlob) {
 
-       // Glob patterns are always tested against the POSIX path, even on Windows
 
-       // https://github.com/isaacs/node-glob#windows
 
-       return options.filterGlob.test(posixPath);
 
-     }
 
-     else if (options.filterRegExp) {
 
-       // Regular expressions are tested against the normal path
 
-       // (based on the OS or options.sep)
 
-       return options.filterRegExp.test(value.path || value);
 
-     }
 
-     else if (options.filterFn) {
 
-       try {
 
-         // Run the user-specified filter function
 
-         return options.filterFn.call(null, value);
 
-       }
 
-       catch (err) {
 
-         // An error occurred in the user's code.
 
-         // In Sync and Async modes, this will return an error.
 
-         // In Streaming mode, we emit an "error" event, but continue processing
 
-         this.emit('error', err);
 
-       }
 
-     }
 
-     else {
 
-       // No filter was specified, so match everything
 
-       return true;
 
-     }
 
-   }
 
-   /**
 
-    * Emits an event.  If one of the event listeners throws an error,
 
-    * then an "error" event is emitted.
 
-    *
 
-    * @param {string} eventName
 
-    * @param {*} data
 
-    */
 
-   emit (eventName, data) {
 
-     let stream = this.stream;
 
-     try {
 
-       stream.emit(eventName, data);
 
-     }
 
-     catch (err) {
 
-       if (eventName === 'error') {
 
-         // Don't recursively emit "error" events.
 
-         // If the first one fails, then just throw
 
-         throw err;
 
-       }
 
-       else {
 
-         stream.emit('error', err);
 
-       }
 
-     }
 
-   }
 
- }
 
- module.exports = DirectoryReader;
 
 
  |