From 6cbdbd6e5a24190d06b9f5fcf92ed89bd94500ce Mon Sep 17 00:00:00 2001 From: Erik Kemperman Date: Wed, 26 Oct 2016 12:59:14 +0200 Subject: [PATCH] Allow custom fs adapter to be passed as option --- README.md | 8 +++++- common.js | 28 ++++++++++++++++++-- glob.js | 15 +++++------ sync.js | 14 +++++----- test/fs.js | 76 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 121 insertions(+), 20 deletions(-) create mode 100644 test/fs.js diff --git a/README.md b/README.md index baa1d1ba..a97e8eff 100644 --- a/README.md +++ b/README.md @@ -173,6 +173,8 @@ be immediately available on the `g.found` member. * `realpathCache` An optional object which is passed to `fs.realpath` to minimize unnecessary syscalls. It is stored on the instantiated Glob object, and may be re-used. +* `fs` By default `require('fs')` is used, but a custom filesystem + adapter may be provided as an option. ### Events @@ -272,10 +274,14 @@ the filesystem. * `realpath` Set to true to call `fs.realpath` on all of the results. In the case of a symlink that cannot be resolved, the full absolute path to the matched entry is returned (though it will usually be a - broken symlink) + broken symlink). This option is forced to be `false` when a custom + filesystem adapter is provided in the `fs` option. * `absolute` Set to true to always receive absolute paths for matched files. Unlike `realpath`, this also affects the values returned in the `match` event. +* `fs` Provide custom implementations of the filesystem calls `readdir`, + `stat` and optionally `lstat`. With `glob.sync()`, their synchronous + counterparts are expected. ## Comparisons to other fnmatch/glob implementations diff --git a/common.js b/common.js index 66651bb3..6daca48b 100644 --- a/common.js +++ b/common.js @@ -12,6 +12,8 @@ function ownProp (obj, field) { return Object.prototype.hasOwnProperty.call(obj, field) } +var fs = require("fs") +var rp = require("fs.realpath") var path = require("path") var minimatch = require("minimatch") var isAbsolute = require("path-is-absolute") @@ -25,6 +27,26 @@ function alphasort (a, b) { return a.localeCompare(b) } +function setupFs (self, options) { + if (!options.fs) { + self.fs = fs + self.rp = rp + } else { + // if custom filesystem adapter is provided, force realpath option to false + self.realpath = false + // wire up the adapter functions, deferring to stat when lstat is absent + self.fs = self.sync ? { + readdirSync: options.fs.readdirSync, + statSync: options.fs.statSync, + lstatSync: options.fs.lstatSync || options.fs.statSync + } : { + readdir: options.fs.readdir, + stat: options.fs.stat, + lstat: options.fs.lstat || options.fs.stat + } + } +} + function setupIgnores (self, options) { self.ignore = options.ignore || [] @@ -50,7 +72,7 @@ function ignoreMap (pattern) { } } -function setopts (self, pattern, options) { +function setopts (self, pattern, options, sync) { if (!options) options = {} @@ -73,7 +95,7 @@ function setopts (self, pattern, options) { self.nodir = !!options.nodir if (self.nodir) self.mark = true - self.sync = !!options.sync + self.sync = sync self.nounique = !!options.nounique self.nonull = !!options.nonull self.nosort = !!options.nosort @@ -87,6 +109,8 @@ function setopts (self, pattern, options) { self.statCache = options.statCache || Object.create(null) self.symlinks = options.symlinks || Object.create(null) + setupFs(self, options) + setupIgnores(self, options) self.changedCwd = false diff --git a/glob.js b/glob.js index bfdd7a11..3ac87604 100644 --- a/glob.js +++ b/glob.js @@ -40,8 +40,6 @@ module.exports = glob -var fs = require('fs') -var rp = require('fs.realpath') var minimatch = require('minimatch') var Minimatch = minimatch.Minimatch var inherits = require('inherits') @@ -132,7 +130,7 @@ function Glob (pattern, options, cb) { if (!(this instanceof Glob)) return new Glob(pattern, options, cb) - setopts(this, pattern, options) + setopts(this, pattern, options, false) this._didRealPath = false // process each pattern in the minimatch set @@ -237,7 +235,7 @@ Glob.prototype._realpathSet = function (index, cb) { // one or more of the links in the realpath couldn't be // resolved. just return the abs value in that case. p = self._makeAbs(p) - rp.realpath(p, self.realpathCache, function (er, real) { + self.rp.realpath(p, self.realpathCache, function (er, real) { if (!er) set[real] = true else if (er.syscall === 'stat') @@ -505,7 +503,7 @@ Glob.prototype._readdirInGlobStar = function (abs, cb) { var lstatcb = inflight(lstatkey, lstatcb_) if (lstatcb) - fs.lstat(abs, lstatcb) + self.fs.lstat(abs, lstatcb) function lstatcb_ (er, lstat) { if (er && er.code === 'ENOENT') @@ -545,8 +543,7 @@ Glob.prototype._readdir = function (abs, inGlobStar, cb) { return cb(null, c) } - var self = this - fs.readdir(abs, readdirCb(this, abs, cb)) + this.fs.readdir(abs, readdirCb(this, abs, cb)) } function readdirCb (self, abs, cb) { @@ -750,13 +747,13 @@ Glob.prototype._stat = function (f, cb) { var self = this var statcb = inflight('stat\0' + abs, lstatcb_) if (statcb) - fs.lstat(abs, statcb) + self.fs.lstat(abs, statcb) function lstatcb_ (er, lstat) { if (lstat && lstat.isSymbolicLink()) { // If it's a symlink, then treat it as the target, unless // the target does not exist, then treat it as a file. - return fs.stat(abs, function (er, stat) { + return self.fs.stat(abs, function (er, stat) { if (er) self._stat2(f, abs, null, lstat, cb) else diff --git a/sync.js b/sync.js index c952134b..85f3c82e 100644 --- a/sync.js +++ b/sync.js @@ -1,8 +1,6 @@ module.exports = globSync globSync.GlobSync = GlobSync -var fs = require('fs') -var rp = require('fs.realpath') var minimatch = require('minimatch') var Minimatch = minimatch.Minimatch var Glob = require('./glob.js').Glob @@ -37,7 +35,7 @@ function GlobSync (pattern, options) { if (!(this instanceof GlobSync)) return new GlobSync(pattern, options) - setopts(this, pattern, options) + setopts(this, pattern, options, true) if (this.noprocess) return this @@ -59,7 +57,7 @@ GlobSync.prototype._finish = function () { for (var p in matchset) { try { p = self._makeAbs(p) - var real = rp.realpathSync(p, self.realpathCache) + var real = self.rp.realpathSync(p, self.realpathCache) set[real] = true } catch (er) { if (er.syscall === 'stat') @@ -248,7 +246,7 @@ GlobSync.prototype._readdirInGlobStar = function (abs) { var lstat var stat try { - lstat = fs.lstatSync(abs) + lstat = this.fs.lstatSync(abs) } catch (er) { if (er.code === 'ENOENT') { // lstat failed, doesn't exist @@ -285,7 +283,7 @@ GlobSync.prototype._readdir = function (abs, inGlobStar) { } try { - return this._readdirEntries(abs, fs.readdirSync(abs)) + return this._readdirEntries(abs, this.fs.readdirSync(abs)) } catch (er) { this._readdirError(abs, er) return null @@ -444,7 +442,7 @@ GlobSync.prototype._stat = function (f) { if (!stat) { var lstat try { - lstat = fs.lstatSync(abs) + lstat = this.fs.lstatSync(abs) } catch (er) { if (er && (er.code === 'ENOENT' || er.code === 'ENOTDIR')) { this.statCache[abs] = false @@ -454,7 +452,7 @@ GlobSync.prototype._stat = function (f) { if (lstat && lstat.isSymbolicLink()) { try { - stat = fs.statSync(abs) + stat = this.fs.statSync(abs) } catch (er) { stat = lstat } diff --git a/test/fs.js b/test/fs.js new file mode 100644 index 00000000..80efe000 --- /dev/null +++ b/test/fs.js @@ -0,0 +1,76 @@ +require("./global-leakage.js") + +var fs = require('fs') +var test = require('tap').test +var glob = require('../') + +// pattern to (potentially) trigger all fs calls +var pattern = 'a/symlink/**/c' + +// on win32, the fixtures will not include symlink, so use a different pattern +// and adjust expectations of stat / statSync being called +var win32 = process.platform === 'win32' +if (win32) + pattern = 'a/bc/**/f' + + +var asyncCases = [ + // all adapter functions are called for our pattern, except stat on win32 + { readdir: true, stat: !win32, lstat: true }, + + // stat is called instead of lstat if adapter doesn't implement it + { readdir: true, stat: true } +] + +var syncCases = [ + // all adapter functions are called for our pattern, except statSync on win32 + { readdirSync: true, statSync: !win32, lstatSync: true }, + + // statSync is called instead of lstatSync if adapter doesn't implement it + { readdirSync: true, statSync: true } +] + + +process.chdir(__dirname + '/fixtures') + + +asyncCases.forEach(function(exp) { + test('fs adapter ' + JSON.stringify(exp), function(t) { + var fns = Object.keys(exp) + var opt = { fs: {} } + var spy = _spy(fns, opt) + glob(pattern, opt, function() { + fns.forEach(function(fn) { + t.ok(spy[fn] === exp[fn], 'expect ' + fn + ' called: ' + exp[fn]) + }) + t.end() + }) + }) +}) + + +syncCases.forEach(function(exp) { + test('fs adapter ' + JSON.stringify(exp), function(t) { + var fns = Object.keys(exp) + var opt = { fs: {} } + var spy = _spy(fns, opt) + glob.sync(pattern, opt) + fns.forEach(function(fn) { + t.ok(spy[fn] === exp[fn], 'expect ' + fn + ' called: ' + exp[fn]) + }) + t.end() + }) +}) + + +function _spy(fns, opt) { + var spy = {} + fns.forEach(function(fn) { + spy[fn] = false + opt.fs[fn] = function() { + spy[fn] = true + return fs[fn].apply(null, arguments) + } + }) + return spy +}