From 71bfe1f11cd51384363166db4eae510465a1fd6b Mon Sep 17 00:00:00 2001 From: Erik Kemperman Date: Mon, 5 Sep 2016 23:23:57 +0200 Subject: [PATCH] Allow custom fs adapter to be passed as option (#280) --- README.md | 7 ++++++- common.js | 17 ++++++++++++++++ glob.js | 13 +++++-------- test/fs.js | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 85 insertions(+), 9 deletions(-) create mode 100644 test/fs.js diff --git a/README.md b/README.md index 9dd9384f..1e8e984f 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,7 +274,10 @@ 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 given in the `fs` option doesn't implement `realpath`. +* `fs` Provide custom implementations of the filesystem calls `readdir`, + `stat` and optionally `lstat` and/or `realpath`. ## Comparisons to other fnmatch/glob implementations diff --git a/common.js b/common.js index 58dc41e6..2b14f72c 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") @@ -86,6 +88,21 @@ function setopts (self, pattern, options) { self.statCache = options.statCache || Object.create(null) self.symlinks = options.symlinks || Object.create(null) + if (!options.fs) { + self.fs = fs + self.rp = rp.realpath + } else { + self.fs = options.fs + // if fs adapter doesn't implement lstat, defer to stat + if (!self.fs.lstat) + self.fs.lstat = self.fs.stat + // if fs adapter doesn't implement realpath, force the option to false + if (!self.fs.realpath) + self.realpath = false + else + self.rp = self.fs.realpath + } + setupIgnores(self, options) self.changedCwd = false diff --git a/glob.js b/glob.js index 9eca910b..ba4d59ef 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') @@ -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(p, self.realpathCache, function (er, real) { if (!er) set[real] = true else if (er.syscall === 'stat') @@ -502,7 +500,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) @@ -542,8 +540,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) { @@ -747,13 +744,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/test/fs.js b/test/fs.js new file mode 100644 index 00000000..92108837 --- /dev/null +++ b/test/fs.js @@ -0,0 +1,57 @@ +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 being called +var win32 = process.platform === 'win32' +if (win32) + pattern = 'a/bc/**/f' + + +// [ expected calls ] +var cases = + [ + // all adapter functions are called for our pattern, excepting stat on win32 + { readdir: true, stat: !win32, lstat: true, realpath: true }, + + // realpath isn't called if adapter doesn't have it + { readdir: true, stat: !win32, lstat: true }, + + // stat is called instead of lstat if adapter doesn't have lstat + { readdir: true, stat: true } + ] + + +process.chdir(__dirname + '/fixtures') + + +cases.forEach(function(exp, i) { + test('custom fs ' + i + ' ' + JSON.stringify(exp), function(t) { + var fns = Object.keys(exp) + var opt = { realpath: true } + var spy = {} + + opt.fs = {} + fns.forEach(function(fn) { + opt.fs[fn] = function() { + spy[fn] = true + return fs[fn].apply(null, [].slice.call(arguments, 0)) + } + spy[fn] = false + }) + + glob(pattern, opt, function(err) { + t.ok(!err, 'expect no error') + fns.forEach(function(fn) { + t.ok(spy[fn] === exp[fn], 'expect ' + fn + ' called: ' + exp[fn]) + }) + t.end() + }) + }) +})