diff options
author | Dimitri Staessens <dimitri@ouroboros.rocks> | 2019-10-06 21:37:45 +0200 |
---|---|---|
committer | Dimitri Staessens <dimitri@ouroboros.rocks> | 2019-10-06 21:37:45 +0200 |
commit | 3c51c3be85bb0d1bdb87ea0d6632f1c256912f27 (patch) | |
tree | c7ccc8279b12c4f7bdbbb4270d617e48f51722e4 /node_modules/@mrmlnc/readdir-enhanced/lib | |
parent | 412c104bebc507bea9c94fd53b5bdc4b64cbfe31 (diff) | |
download | website-3c51c3be85bb0d1bdb87ea0d6632f1c256912f27.tar.gz website-3c51c3be85bb0d1bdb87ea0d6632f1c256912f27.zip |
build: Add some required modules for node
Diffstat (limited to 'node_modules/@mrmlnc/readdir-enhanced/lib')
11 files changed, 992 insertions, 0 deletions
diff --git a/node_modules/@mrmlnc/readdir-enhanced/lib/async/for-each.js b/node_modules/@mrmlnc/readdir-enhanced/lib/async/for-each.js new file mode 100644 index 0000000..1ac9b2f --- /dev/null +++ b/node_modules/@mrmlnc/readdir-enhanced/lib/async/for-each.js @@ -0,0 +1,29 @@ +'use strict'; + +module.exports = asyncForEach; + +/** + * Simultaneously processes all items in the given array. + * + * @param {array} array - The array to iterate over + * @param {function} iterator - The function to call for each item in the array + * @param {function} done - The function to call when all iterators have completed + */ +function asyncForEach (array, iterator, done) { + if (array.length === 0) { + // NOTE: Normally a bad idea to mix sync and async, but it's safe here because + // of the way that this method is currently used by DirectoryReader. + done(); + return; + } + + // Simultaneously process all items in the array. + let pending = array.length; + array.forEach(item => { + iterator(item, () => { + if (--pending === 0) { + done(); + } + }); + }); +} diff --git a/node_modules/@mrmlnc/readdir-enhanced/lib/async/index.js b/node_modules/@mrmlnc/readdir-enhanced/lib/async/index.js new file mode 100644 index 0000000..677e0b6 --- /dev/null +++ b/node_modules/@mrmlnc/readdir-enhanced/lib/async/index.js @@ -0,0 +1,48 @@ +'use strict'; + +module.exports = readdirAsync; + +const maybe = require('call-me-maybe'); +const DirectoryReader = require('../directory-reader'); + +let asyncFacade = { + fs: require('fs'), + forEach: require('./for-each'), + async: true +}; + +/** + * Returns the buffered output from an asynchronous {@link DirectoryReader}, + * via an error-first callback or a {@link Promise}. + * + * @param {string} dir + * @param {object} [options] + * @param {function} [callback] + * @param {object} internalOptions + */ +function readdirAsync (dir, options, callback, internalOptions) { + if (typeof options === 'function') { + callback = options; + options = undefined; + } + + return maybe(callback, new Promise(((resolve, reject) => { + let results = []; + + internalOptions.facade = asyncFacade; + + let reader = new DirectoryReader(dir, options, internalOptions); + let stream = reader.stream; + + stream.on('error', err => { + reject(err); + stream.pause(); + }); + stream.on('data', result => { + results.push(result); + }); + stream.on('end', () => { + resolve(results); + }); + }))); +} diff --git a/node_modules/@mrmlnc/readdir-enhanced/lib/call.js b/node_modules/@mrmlnc/readdir-enhanced/lib/call.js new file mode 100644 index 0000000..07e3d84 --- /dev/null +++ b/node_modules/@mrmlnc/readdir-enhanced/lib/call.js @@ -0,0 +1,54 @@ +'use strict'; + +let call = module.exports = { + safe: safeCall, + once: callOnce, +}; + +/** + * Calls a function with the given arguments, and ensures that the error-first callback is _always_ + * invoked exactly once, even if the function throws an error. + * + * @param {function} fn - The function to invoke + * @param {...*} args - The arguments to pass to the function. The final argument must be a callback function. + */ +function safeCall (fn, args) { + // Get the function arguments as an array + args = Array.prototype.slice.call(arguments, 1); + + // Replace the callback function with a wrapper that ensures it will only be called once + let callback = call.once(args.pop()); + args.push(callback); + + try { + fn.apply(null, args); + } + catch (err) { + callback(err); + } +} + +/** + * Returns a wrapper function that ensures the given callback function is only called once. + * Subsequent calls are ignored, unless the first argument is an Error, in which case the + * error is thrown. + * + * @param {function} fn - The function that should only be called once + * @returns {function} + */ +function callOnce (fn) { + let fulfilled = false; + + return function onceWrapper (err) { + if (!fulfilled) { + fulfilled = true; + return fn.apply(this, arguments); + } + else if (err) { + // The callback has already been called, but now an error has occurred + // (most likely inside the callback function). So re-throw the error, + // so it gets handled further up the call stack + throw err; + } + }; +} diff --git a/node_modules/@mrmlnc/readdir-enhanced/lib/directory-reader.js b/node_modules/@mrmlnc/readdir-enhanced/lib/directory-reader.js new file mode 100644 index 0000000..569d793 --- /dev/null +++ b/node_modules/@mrmlnc/readdir-enhanced/lib/directory-reader.js @@ -0,0 +1,380 @@ +'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; diff --git a/node_modules/@mrmlnc/readdir-enhanced/lib/index.js b/node_modules/@mrmlnc/readdir-enhanced/lib/index.js new file mode 100644 index 0000000..f77d2c6 --- /dev/null +++ b/node_modules/@mrmlnc/readdir-enhanced/lib/index.js @@ -0,0 +1,85 @@ +'use strict'; + +const readdirSync = require('./sync'); +const readdirAsync = require('./async'); +const readdirStream = require('./stream'); + +module.exports = exports = readdirAsyncPath; +exports.readdir = exports.readdirAsync = exports.async = readdirAsyncPath; +exports.readdirAsyncStat = exports.async.stat = readdirAsyncStat; +exports.readdirStream = exports.stream = readdirStreamPath; +exports.readdirStreamStat = exports.stream.stat = readdirStreamStat; +exports.readdirSync = exports.sync = readdirSyncPath; +exports.readdirSyncStat = exports.sync.stat = readdirSyncStat; + +/** + * Synchronous readdir that returns an array of string paths. + * + * @param {string} dir + * @param {object} [options] + * @returns {string[]} + */ +function readdirSyncPath (dir, options) { + return readdirSync(dir, options, {}); +} + +/** + * Synchronous readdir that returns results as an array of {@link fs.Stats} objects + * + * @param {string} dir + * @param {object} [options] + * @returns {fs.Stats[]} + */ +function readdirSyncStat (dir, options) { + return readdirSync(dir, options, { stats: true }); +} + +/** + * Aynchronous readdir (accepts an error-first callback or returns a {@link Promise}). + * Results are an array of path strings. + * + * @param {string} dir + * @param {object} [options] + * @param {function} [callback] + * @returns {Promise<string[]>} + */ +function readdirAsyncPath (dir, options, callback) { + return readdirAsync(dir, options, callback, {}); +} + +/** + * Aynchronous readdir (accepts an error-first callback or returns a {@link Promise}). + * Results are an array of {@link fs.Stats} objects. + * + * @param {string} dir + * @param {object} [options] + * @param {function} [callback] + * @returns {Promise<fs.Stats[]>} + */ +function readdirAsyncStat (dir, options, callback) { + return readdirAsync(dir, options, callback, { stats: true }); +} + +/** + * Aynchronous readdir that returns a {@link stream.Readable} (which is also an {@link EventEmitter}). + * All stream data events ("data", "file", "directory", "symlink") are passed a path string. + * + * @param {string} dir + * @param {object} [options] + * @returns {stream.Readable} + */ +function readdirStreamPath (dir, options) { + return readdirStream(dir, options, {}); +} + +/** + * Aynchronous readdir that returns a {@link stream.Readable} (which is also an {@link EventEmitter}) + * All stream data events ("data", "file", "directory", "symlink") are passed an {@link fs.Stats} object. + * + * @param {string} dir + * @param {object} [options] + * @returns {stream.Readable} + */ +function readdirStreamStat (dir, options) { + return readdirStream(dir, options, { stats: true }); +} diff --git a/node_modules/@mrmlnc/readdir-enhanced/lib/normalize-options.js b/node_modules/@mrmlnc/readdir-enhanced/lib/normalize-options.js new file mode 100644 index 0000000..66f1158 --- /dev/null +++ b/node_modules/@mrmlnc/readdir-enhanced/lib/normalize-options.js @@ -0,0 +1,177 @@ +'use strict'; + +const path = require('path'); +const globToRegExp = require('glob-to-regexp'); + +module.exports = normalizeOptions; + +let isWindows = /^win/.test(process.platform); + +/** + * @typedef {Object} FSFacade + * @property {fs.readdir} readdir + * @property {fs.stat} stat + * @property {fs.lstat} lstat + */ + +/** + * Validates and normalizes the options argument + * + * @param {object} [options] - User-specified options, if any + * @param {object} internalOptions - Internal options that aren't part of the public API + * + * @param {number|boolean|function} [options.deep] + * The number of directories to recursively traverse. Any falsy value or negative number will + * default to zero, so only the top-level contents will be returned. Set to `true` or `Infinity` + * to traverse all subdirectories. Or provide a function that accepts a {@link fs.Stats} object + * and returns a truthy value if the directory's contents should be crawled. + * + * @param {function|string|RegExp} [options.filter] + * A function that accepts a {@link fs.Stats} object and returns a truthy value if the data should + * be returned. Or a RegExp or glob string pattern, to filter by file name. + * + * @param {string} [options.sep] + * The path separator to use. By default, the OS-specific separator will be used, but this can be + * set to a specific value to ensure consistency across platforms. + * + * @param {string} [options.basePath] + * The base path to prepend to each result. If empty, then all results will be relative to `dir`. + * + * @param {FSFacade} [options.fs] + * Synchronous or asynchronous facades for Node.js File System module + * + * @param {object} [internalOptions.facade] + * Synchronous or asynchronous facades for various methods, including for the Node.js File System module + * + * @param {boolean} [internalOptions.emit] + * Indicates whether the reader should emit "file", "directory", and "symlink" events + * + * @param {boolean} [internalOptions.stats] + * Indicates whether the reader should emit {@link fs.Stats} objects instead of path strings + * + * @returns {object} + */ +function normalizeOptions (options, internalOptions) { + if (options === null || options === undefined) { + options = {}; + } + else if (typeof options !== 'object') { + throw new TypeError('options must be an object'); + } + + let recurseDepth, recurseFn, recurseRegExp, recurseGlob, deep = options.deep; + if (deep === null || deep === undefined) { + recurseDepth = 0; + } + else if (typeof deep === 'boolean') { + recurseDepth = deep ? Infinity : 0; + } + else if (typeof deep === 'number') { + if (deep < 0 || isNaN(deep)) { + throw new Error('options.deep must be a positive number'); + } + else if (Math.floor(deep) !== deep) { + throw new Error('options.deep must be an integer'); + } + else { + recurseDepth = deep; + } + } + else if (typeof deep === 'function') { + recurseDepth = Infinity; + recurseFn = deep; + } + else if (deep instanceof RegExp) { + recurseDepth = Infinity; + recurseRegExp = deep; + } + else if (typeof deep === 'string' && deep.length > 0) { + recurseDepth = Infinity; + recurseGlob = globToRegExp(deep, { extended: true, globstar: true }); + } + else { + throw new TypeError('options.deep must be a boolean, number, function, regular expression, or glob pattern'); + } + + let filterFn, filterRegExp, filterGlob, filter = options.filter; + if (filter !== null && filter !== undefined) { + if (typeof filter === 'function') { + filterFn = filter; + } + else if (filter instanceof RegExp) { + filterRegExp = filter; + } + else if (typeof filter === 'string' && filter.length > 0) { + filterGlob = globToRegExp(filter, { extended: true, globstar: true }); + } + else { + throw new TypeError('options.filter must be a function, regular expression, or glob pattern'); + } + } + + let sep = options.sep; + if (sep === null || sep === undefined) { + sep = path.sep; + } + else if (typeof sep !== 'string') { + throw new TypeError('options.sep must be a string'); + } + + let basePath = options.basePath; + if (basePath === null || basePath === undefined) { + basePath = ''; + } + else if (typeof basePath === 'string') { + // Append a path separator to the basePath, if necessary + if (basePath && basePath.substr(-1) !== sep) { + basePath += sep; + } + } + else { + throw new TypeError('options.basePath must be a string'); + } + + // Convert the basePath to POSIX (forward slashes) + // so that glob pattern matching works consistently, even on Windows + let posixBasePath = basePath; + if (posixBasePath && sep !== '/') { + posixBasePath = posixBasePath.replace(new RegExp('\\' + sep, 'g'), '/'); + + /* istanbul ignore if */ + if (isWindows) { + // Convert Windows root paths (C:\) and UNCs (\\) to POSIX root paths + posixBasePath = posixBasePath.replace(/^([a-zA-Z]\:\/|\/\/)/, '/'); + } + } + + // Determine which facade methods to use + let facade; + if (options.fs === null || options.fs === undefined) { + // The user didn't provide their own facades, so use our internal ones + facade = internalOptions.facade; + } + else if (typeof options.fs === 'object') { + // Merge the internal facade methods with the user-provided `fs` facades + facade = Object.assign({}, internalOptions.facade); + facade.fs = Object.assign({}, internalOptions.facade.fs, options.fs); + } + else { + throw new TypeError('options.fs must be an object'); + } + + return { + recurseDepth, + recurseFn, + recurseRegExp, + recurseGlob, + filterFn, + filterRegExp, + filterGlob, + sep, + basePath, + posixBasePath, + facade, + emit: !!internalOptions.emit, + stats: !!internalOptions.stats, + }; +} diff --git a/node_modules/@mrmlnc/readdir-enhanced/lib/stat.js b/node_modules/@mrmlnc/readdir-enhanced/lib/stat.js new file mode 100644 index 0000000..e338693 --- /dev/null +++ b/node_modules/@mrmlnc/readdir-enhanced/lib/stat.js @@ -0,0 +1,74 @@ +'use strict'; + +const call = require('./call'); + +module.exports = stat; + +/** + * Retrieves the {@link fs.Stats} for the given path. If the path is a symbolic link, + * then the Stats of the symlink's target are returned instead. If the symlink is broken, + * then the Stats of the symlink itself are returned. + * + * @param {object} fs - Synchronous or Asynchronouse facade for the "fs" module + * @param {string} path - The path to return stats for + * @param {function} callback + */ +function stat (fs, path, callback) { + let isSymLink = false; + + call.safe(fs.lstat, path, (err, lstats) => { + if (err) { + // fs.lstat threw an eror + return callback(err); + } + + try { + isSymLink = lstats.isSymbolicLink(); + } + catch (err2) { + // lstats.isSymbolicLink() threw an error + // (probably because fs.lstat returned an invalid result) + return callback(err2); + } + + if (isSymLink) { + // Try to resolve the symlink + symlinkStat(fs, path, lstats, callback); + } + else { + // It's not a symlink, so return the stats as-is + callback(null, lstats); + } + }); +} + +/** + * Retrieves the {@link fs.Stats} for the target of the given symlink. + * If the symlink is broken, then the Stats of the symlink itself are returned. + * + * @param {object} fs - Synchronous or Asynchronouse facade for the "fs" module + * @param {string} path - The path of the symlink to return stats for + * @param {object} lstats - The stats of the symlink + * @param {function} callback + */ +function symlinkStat (fs, path, lstats, callback) { + call.safe(fs.stat, path, (err, stats) => { + if (err) { + // The symlink is broken, so return the stats for the link itself + return callback(null, lstats); + } + + try { + // Return the stats for the resolved symlink target, + // and override the `isSymbolicLink` method to indicate that it's a symlink + stats.isSymbolicLink = () => true; + } + catch (err2) { + // Setting stats.isSymbolicLink threw an error + // (probably because fs.stat returned an invalid result) + return callback(err2); + } + + callback(null, stats); + }); +} diff --git a/node_modules/@mrmlnc/readdir-enhanced/lib/stream/index.js b/node_modules/@mrmlnc/readdir-enhanced/lib/stream/index.js new file mode 100644 index 0000000..22a9609 --- /dev/null +++ b/node_modules/@mrmlnc/readdir-enhanced/lib/stream/index.js @@ -0,0 +1,25 @@ +'use strict'; + +module.exports = readdirStream; + +const DirectoryReader = require('../directory-reader'); + +let streamFacade = { + fs: require('fs'), + forEach: require('../async/for-each'), + async: true +}; + +/** + * Returns the {@link stream.Readable} of an asynchronous {@link DirectoryReader}. + * + * @param {string} dir + * @param {object} [options] + * @param {object} internalOptions + */ +function readdirStream (dir, options, internalOptions) { + internalOptions.facade = streamFacade; + + let reader = new DirectoryReader(dir, options, internalOptions); + return reader.stream; +} diff --git a/node_modules/@mrmlnc/readdir-enhanced/lib/sync/for-each.js b/node_modules/@mrmlnc/readdir-enhanced/lib/sync/for-each.js new file mode 100644 index 0000000..c5ec088 --- /dev/null +++ b/node_modules/@mrmlnc/readdir-enhanced/lib/sync/for-each.js @@ -0,0 +1,22 @@ +'use strict'; + +module.exports = syncForEach; + +/** + * A facade that allows {@link Array.forEach} to be called as though it were asynchronous. + * + * @param {array} array - The array to iterate over + * @param {function} iterator - The function to call for each item in the array + * @param {function} done - The function to call when all iterators have completed + */ +function syncForEach (array, iterator, done) { + array.forEach(item => { + iterator(item, () => { + // Note: No error-handling here because this is currently only ever called + // by DirectoryReader, which never passes an `error` parameter to the callback. + // Instead, DirectoryReader emits an "error" event if an error occurs. + }); + }); + + done(); +} diff --git a/node_modules/@mrmlnc/readdir-enhanced/lib/sync/fs.js b/node_modules/@mrmlnc/readdir-enhanced/lib/sync/fs.js new file mode 100644 index 0000000..3aada77 --- /dev/null +++ b/node_modules/@mrmlnc/readdir-enhanced/lib/sync/fs.js @@ -0,0 +1,64 @@ +'use strict'; + +const fs = require('fs'); +const call = require('../call'); + +/** + * A facade around {@link fs.readdirSync} that allows it to be called + * the same way as {@link fs.readdir}. + * + * @param {string} dir + * @param {function} callback + */ +exports.readdir = function (dir, callback) { + // Make sure the callback is only called once + callback = call.once(callback); + + try { + let items = fs.readdirSync(dir); + callback(null, items); + } + catch (err) { + callback(err); + } +}; + +/** + * A facade around {@link fs.statSync} that allows it to be called + * the same way as {@link fs.stat}. + * + * @param {string} path + * @param {function} callback + */ +exports.stat = function (path, callback) { + // Make sure the callback is only called once + callback = call.once(callback); + + try { + let stats = fs.statSync(path); + callback(null, stats); + } + catch (err) { + callback(err); + } +}; + +/** + * A facade around {@link fs.lstatSync} that allows it to be called + * the same way as {@link fs.lstat}. + * + * @param {string} path + * @param {function} callback + */ +exports.lstat = function (path, callback) { + // Make sure the callback is only called once + callback = call.once(callback); + + try { + let stats = fs.lstatSync(path); + callback(null, stats); + } + catch (err) { + callback(err); + } +}; diff --git a/node_modules/@mrmlnc/readdir-enhanced/lib/sync/index.js b/node_modules/@mrmlnc/readdir-enhanced/lib/sync/index.js new file mode 100644 index 0000000..60243a1 --- /dev/null +++ b/node_modules/@mrmlnc/readdir-enhanced/lib/sync/index.js @@ -0,0 +1,34 @@ +'use strict'; + +module.exports = readdirSync; + +const DirectoryReader = require('../directory-reader'); + +let syncFacade = { + fs: require('./fs'), + forEach: require('./for-each'), + sync: true +}; + +/** + * Returns the buffered output from a synchronous {@link DirectoryReader}. + * + * @param {string} dir + * @param {object} [options] + * @param {object} internalOptions + */ +function readdirSync (dir, options, internalOptions) { + internalOptions.facade = syncFacade; + + let reader = new DirectoryReader(dir, options, internalOptions); + let stream = reader.stream; + + let results = []; + let data = stream.read(); + while (data !== null) { + results.push(data); + data = stream.read(); + } + + return results; +} |