Added ability to stop the walk from descending into a given directory. You use this by calling in either the 'directory' or 'name' event handler. This will prevent the directory from being traversed.
303 lines
7.7 KiB
JavaScript
303 lines
7.7 KiB
JavaScript
// Adapted from work by jorge@jorgechamorro.com on 2010-11-25
|
|
(function () {
|
|
"use strict";
|
|
|
|
function noop() {}
|
|
|
|
var fs = require('fs')
|
|
, forEachAsync = require('foreachasync').forEachAsync
|
|
, EventEmitter = require('events').EventEmitter
|
|
, TypeEmitter = require('./node-type-emitter')
|
|
, util = require('util')
|
|
, path = require('path')
|
|
;
|
|
|
|
function appendToDirs(stat) {
|
|
/*jshint validthis:true*/
|
|
if(stat.flag && stat.flag === NO_DESCEND) { return; }
|
|
this.push(stat.name);
|
|
}
|
|
|
|
function wFilesHandlerWrapper(items) {
|
|
/*jshint validthis:true*/
|
|
this._wFilesHandler(noop, items);
|
|
}
|
|
|
|
function Walker(pathname, options, sync) {
|
|
EventEmitter.call(this);
|
|
|
|
var me = this
|
|
;
|
|
|
|
options = options || {};
|
|
me._wStat = options.followLinks && 'stat' || 'lstat';
|
|
me._wStatSync = me._wStat + 'Sync';
|
|
me._wsync = sync;
|
|
me._wq = [];
|
|
me._wqueue = [me._wq];
|
|
me._wcurpath = undefined;
|
|
me._wfilters = options.filters || [];
|
|
me._wfirstrun = true;
|
|
me._wcurpath = pathname;
|
|
|
|
if (me._wsync) {
|
|
//console.log('_walkSync');
|
|
me._wWalk = me._wWalkSync;
|
|
} else {
|
|
//console.log('_walkASync');
|
|
me._wWalk = me._wWalkAsync;
|
|
}
|
|
|
|
options.listeners = options.listeners || {};
|
|
Object.keys(options.listeners).forEach(function (event) {
|
|
var callbacks = options.listeners[event]
|
|
;
|
|
|
|
if ('function' === typeof callbacks) {
|
|
callbacks = [callbacks];
|
|
}
|
|
|
|
callbacks.forEach(function (callback) {
|
|
me.on(event, callback);
|
|
});
|
|
});
|
|
|
|
me._wWalk();
|
|
}
|
|
|
|
// Inherits must come before prototype additions
|
|
util.inherits(Walker, EventEmitter);
|
|
|
|
Walker.prototype._wLstatHandler = function (err, stat) {
|
|
var me = this
|
|
;
|
|
|
|
stat = stat || {};
|
|
stat.name = me._wcurfile;
|
|
|
|
if (err) {
|
|
stat.error = err;
|
|
//me.emit('error', curpath, stat);
|
|
// TODO v3.0 (don't noop the next if there are listeners)
|
|
me.emit('nodeError', me._wcurpath, stat, noop);
|
|
me._wfnodegroups.errors.push(stat);
|
|
me._wCurFileCallback();
|
|
} else {
|
|
TypeEmitter.sortFnodesByType(stat, me._wfnodegroups);
|
|
// NOTE: wCurFileCallback doesn't need thisness, so this is okay
|
|
TypeEmitter.emitNodeType(me, me._wcurpath, stat, me._wCurFileCallback, me);
|
|
}
|
|
};
|
|
Walker.prototype._wFilesHandler = function (cont, file) {
|
|
var statPath
|
|
, me = this
|
|
;
|
|
|
|
|
|
me._wcurfile = file;
|
|
me._wCurFileCallback = cont;
|
|
me.emit('name', me._wcurpath, file, noop);
|
|
|
|
statPath = me._wcurpath + path.sep + file;
|
|
|
|
if (!me._wsync) {
|
|
// TODO how to remove this anony?
|
|
fs[me._wStat](statPath, function (err, stat) {
|
|
me._wLstatHandler(err, stat);
|
|
});
|
|
return;
|
|
}
|
|
|
|
try {
|
|
me._wLstatHandler(null, fs[me._wStatSync](statPath));
|
|
} catch(e) {
|
|
me._wLstatHandler(e);
|
|
}
|
|
};
|
|
Walker.prototype._wOnEmitDone = function () {
|
|
var me = this
|
|
, dirs = []
|
|
;
|
|
|
|
me._wfnodegroups.directories.forEach(appendToDirs, dirs);
|
|
dirs.forEach(me._wJoinPath, me);
|
|
me._wqueue.push(me._wq = dirs);
|
|
me._wNext();
|
|
};
|
|
Walker.prototype._wPostFilesHandler = function () {
|
|
var me = this
|
|
;
|
|
|
|
if (me._wfnodegroups.errors.length) {
|
|
// TODO v3.0 (don't noop the next)
|
|
// .errors is an array of stats with { name: name, error: error }
|
|
me.emit('errors', me._wcurpath, me._wfnodegroups.errors, noop);
|
|
}
|
|
// XXX emitNodeTypes still needs refactor
|
|
TypeEmitter.emitNodeTypeGroups(me, me._wcurpath, me._wfnodegroups, me._wOnEmitDone, me);
|
|
};
|
|
Walker.prototype._wReadFiles = function () {
|
|
var me = this
|
|
;
|
|
|
|
if (!me._wcurfiles || 0 === me._wcurfiles.length) {
|
|
return me._wNext();
|
|
}
|
|
|
|
// TODO could allow user to selectively stat
|
|
// and don't stat if there are no stat listeners
|
|
me.emit('names', me._wcurpath, me._wcurfiles, noop);
|
|
|
|
if (me._wsync) {
|
|
me._wcurfiles.forEach(wFilesHandlerWrapper, me);
|
|
me._wPostFilesHandler();
|
|
} else {
|
|
forEachAsync(me._wcurfiles, me._wFilesHandler, me).then(me._wPostFilesHandler);
|
|
}
|
|
};
|
|
Walker.prototype._wReaddirHandler = function (err, files) {
|
|
var fnodeGroups = TypeEmitter.createNodeGroups()
|
|
, me = this
|
|
, parent
|
|
, child
|
|
;
|
|
|
|
me._wfnodegroups = fnodeGroups;
|
|
me._wcurfiles = files;
|
|
|
|
// no error, great
|
|
if (!err) {
|
|
me._wReadFiles();
|
|
return;
|
|
}
|
|
|
|
// TODO path.sep
|
|
me._wcurpath = me._wcurpath.replace(/\/$/, '');
|
|
|
|
// error? not first run? => directory error
|
|
if (!me._wfirstrun) {
|
|
// TODO v3.0 (don't noop the next if there are listeners)
|
|
me.emit('directoryError', me._wcurpath, { error: err }, noop);
|
|
// TODO v3.0
|
|
//me.emit('directoryError', me._wcurpath.replace(/^(.*)\/.*$/, '$1'), { name: me._wcurpath.replace(/^.*\/(.*)/, '$1'), error: err }, noop);
|
|
me._wReadFiles();
|
|
return;
|
|
}
|
|
|
|
// error? first run? => maybe a file, maybe a true error
|
|
me._wfirstrun = false;
|
|
|
|
// readdir failed (might be a file), try a stat on the parent
|
|
parent = me._wcurpath.replace(/^(.*)\/.*$/, '$1');
|
|
fs[me._wStat](parent, function (e, stat) {
|
|
|
|
if (stat) {
|
|
// success
|
|
// now try stat on this as a child of the parent directory
|
|
child = me._wcurpath.replace(/^.*\/(.*)$/, '$1');
|
|
me._wcurfiles = [child];
|
|
me._wcurpath = parent;
|
|
} else {
|
|
// TODO v3.0
|
|
//me.emit('directoryError', me._wcurpath.replace(/^(.*)\/.*$/, '$1'), { name: me._wcurpath.replace(/^.*\/(.*)/, '$1'), error: err }, noop);
|
|
// TODO v3.0 (don't noop the next)
|
|
// the original readdir error, not the parent stat error
|
|
me.emit('nodeError', me._wcurpath, { error: err }, noop);
|
|
}
|
|
|
|
me._wReadFiles();
|
|
});
|
|
};
|
|
Walker.prototype._wFilter = function () {
|
|
var me = this
|
|
, exclude
|
|
;
|
|
|
|
// Stop directories that contain filter keywords
|
|
// from continuing through the walk process
|
|
exclude = me._wfilters.some(function (filter) {
|
|
if (me._wcurpath.match(filter)) {
|
|
return true;
|
|
}
|
|
});
|
|
|
|
return exclude;
|
|
};
|
|
Walker.prototype._wWalkSync = function () {
|
|
//console.log('walkSync');
|
|
var err
|
|
, files
|
|
, me = this
|
|
;
|
|
|
|
try {
|
|
files = fs.readdirSync(me._wcurpath);
|
|
} catch(e) {
|
|
err = e;
|
|
}
|
|
|
|
me._wReaddirHandler(err, files);
|
|
};
|
|
Walker.prototype._wWalkAsync = function () {
|
|
//console.log('walkAsync');
|
|
var me = this
|
|
;
|
|
|
|
// TODO how to remove this anony?
|
|
fs.readdir(me._wcurpath, function (err, files) {
|
|
me._wReaddirHandler(err, files);
|
|
});
|
|
};
|
|
Walker.prototype._wNext = function () {
|
|
var me = this
|
|
;
|
|
|
|
if (me._paused) {
|
|
return;
|
|
}
|
|
if (me._wq.length) {
|
|
me._wcurpath = me._wq.pop();
|
|
while (me._wq.length && me._wFilter()) {
|
|
me._wcurpath = me._wq.pop();
|
|
}
|
|
if (me._wcurpath && !me._wFilter()) {
|
|
me._wWalk();
|
|
} else {
|
|
me._wNext();
|
|
}
|
|
return;
|
|
}
|
|
me._wqueue.length -= 1;
|
|
if (me._wqueue.length) {
|
|
me._wq = me._wqueue[me._wqueue.length - 1];
|
|
return me._wNext();
|
|
}
|
|
|
|
// To not break compatibility
|
|
//process.nextTick(function () {
|
|
me.emit('end');
|
|
//});
|
|
};
|
|
Walker.prototype._wJoinPath = function (v, i, o) {
|
|
var me = this
|
|
;
|
|
|
|
o[i] = [me._wcurpath, path.sep, v].join('');
|
|
};
|
|
Walker.prototype.pause = function () {
|
|
this._paused = true;
|
|
};
|
|
Walker.prototype.resume = function () {
|
|
this._paused = false;
|
|
this._wNext();
|
|
};
|
|
|
|
exports.walk = function (path, opts) {
|
|
return new Walker(path, opts, false);
|
|
};
|
|
|
|
exports.walkSync = function (path, opts) {
|
|
return new Walker(path, opts, true);
|
|
};
|
|
}());
|