Skip to content

Commit

Permalink
Allow custom fs adapter to be passed as option (#280)
Browse files Browse the repository at this point in the history
  • Loading branch information
erikkemperman committed Sep 7, 2016
1 parent 98327d8 commit fc1975e
Show file tree
Hide file tree
Showing 5 changed files with 121 additions and 20 deletions.
8 changes: 7 additions & 1 deletion README.md
Expand Up @@ -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

Expand Down Expand Up @@ -272,7 +274,11 @@ 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.
* `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

Expand Down
28 changes: 26 additions & 2 deletions common.js
Expand Up @@ -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")
Expand All @@ -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 || []

Expand All @@ -50,7 +72,7 @@ function ignoreMap (pattern) {
}
}

function setopts (self, pattern, options) {
function setopts (self, pattern, options, sync) {
if (!options)
options = {}

Expand All @@ -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
Expand All @@ -86,6 +108,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
Expand Down
15 changes: 6 additions & 9 deletions glob.js
Expand Up @@ -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')
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Expand Down
14 changes: 6 additions & 8 deletions 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
Expand Down Expand Up @@ -36,7 +34,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
Expand All @@ -58,7 +56,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')
Expand Down Expand Up @@ -238,7 +236,7 @@ GlobSync.prototype._readdirInGlobStar = function (abs) {
var lstat
var stat
try {
lstat = fs.lstatSync(abs)
lstat = this.fs.lstatSync(abs)
} catch (er) {
// lstat failed, doesn't exist
return null
Expand Down Expand Up @@ -273,7 +271,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
Expand Down Expand Up @@ -432,14 +430,14 @@ GlobSync.prototype._stat = function (f) {
if (!stat) {
var lstat
try {
lstat = fs.lstatSync(abs)
lstat = this.fs.lstatSync(abs)
} catch (er) {
return false
}

if (lstat.isSymbolicLink()) {
try {
stat = fs.statSync(abs)
stat = this.fs.statSync(abs)
} catch (er) {
stat = lstat
}
Expand Down
76 changes: 76 additions & 0 deletions 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, [].slice.call(arguments, 0))
}
})
return spy
}

0 comments on commit fc1975e

Please sign in to comment.