/* | |
MIT License http://www.opensource.org/licenses/mit-license.php | |
Author Tobias Koppers @sokra | |
*/ | |
"use strict"; | |
var EventEmitter = require("events").EventEmitter; | |
var async = require("neo-async"); | |
var chokidar = require("chokidar"); | |
var fs = require("graceful-fs"); | |
var path = require("path"); | |
var watcherManager = require("./watcherManager"); | |
var FS_ACCURACY = 1000; | |
function withoutCase(str) { | |
return str.toLowerCase(); | |
} | |
function Watcher(directoryWatcher, filePath, startTime) { | |
EventEmitter.call(this); | |
this.directoryWatcher = directoryWatcher; | |
this.path = filePath; | |
this.startTime = startTime && +startTime; | |
// TODO this.data seem to be only read, weird | |
this.data = 0; | |
} | |
Watcher.prototype = Object.create(EventEmitter.prototype); | |
Watcher.prototype.constructor = Watcher; | |
Watcher.prototype.checkStartTime = function checkStartTime(mtime, initial) { | |
if(typeof this.startTime !== "number") return !initial; | |
var startTime = this.startTime; | |
return startTime <= mtime; | |
}; | |
Watcher.prototype.close = function close() { | |
this.emit("closed"); | |
}; | |
function DirectoryWatcher(directoryPath, options) { | |
EventEmitter.call(this); | |
this.options = options; | |
this.path = directoryPath; | |
this.files = Object.create(null); | |
this.directories = Object.create(null); | |
var interval = typeof options.poll === "number" ? options.poll : undefined; | |
this.watcher = chokidar.watch(directoryPath, { | |
ignoreInitial: true, | |
persistent: true, | |
followSymlinks: false, | |
depth: 0, | |
atomic: false, | |
alwaysStat: true, | |
ignorePermissionErrors: true, | |
ignored: options.ignored, | |
usePolling: options.poll ? true : undefined, | |
interval: interval, | |
binaryInterval: interval, | |
disableGlobbing: true | |
}); | |
this.watcher.on("add", this.onFileAdded.bind(this)); | |
this.watcher.on("addDir", this.onDirectoryAdded.bind(this)); | |
this.watcher.on("change", this.onChange.bind(this)); | |
this.watcher.on("unlink", this.onFileUnlinked.bind(this)); | |
this.watcher.on("unlinkDir", this.onDirectoryUnlinked.bind(this)); | |
this.watcher.on("error", this.onWatcherError.bind(this)); | |
this.initialScan = true; | |
this.nestedWatching = false; | |
this.initialScanRemoved = []; | |
this.doInitialScan(); | |
this.watchers = Object.create(null); | |
this.parentWatcher = null; | |
this.refs = 0; | |
} | |
module.exports = DirectoryWatcher; | |
DirectoryWatcher.prototype = Object.create(EventEmitter.prototype); | |
DirectoryWatcher.prototype.constructor = DirectoryWatcher; | |
DirectoryWatcher.prototype.setFileTime = function setFileTime(filePath, mtime, initial, type) { | |
var now = Date.now(); | |
var old = this.files[filePath]; | |
this.files[filePath] = [initial ? Math.min(now, mtime) : now, mtime]; | |
// we add the fs accuracy to reach the maximum possible mtime | |
if(mtime) | |
mtime = mtime + FS_ACCURACY; | |
if(!old) { | |
if(mtime) { | |
if(this.watchers[withoutCase(filePath)]) { | |
this.watchers[withoutCase(filePath)].forEach(function(w) { | |
if(!initial || w.checkStartTime(mtime, initial)) { | |
w.emit("change", mtime, initial ? "initial" : type); | |
} | |
}); | |
} | |
} | |
} else if(!initial && mtime) { | |
if(this.watchers[withoutCase(filePath)]) { | |
this.watchers[withoutCase(filePath)].forEach(function(w) { | |
w.emit("change", mtime, type); | |
}); | |
} | |
} else if(!initial && !mtime) { | |
if(this.watchers[withoutCase(filePath)]) { | |
this.watchers[withoutCase(filePath)].forEach(function(w) { | |
w.emit("remove", type); | |
}); | |
} | |
} | |
if(this.watchers[withoutCase(this.path)]) { | |
this.watchers[withoutCase(this.path)].forEach(function(w) { | |
if(!initial || w.checkStartTime(mtime, initial)) { | |
w.emit("change", filePath, mtime, initial ? "initial" : type); | |
} | |
}); | |
} | |
}; | |
DirectoryWatcher.prototype.setDirectory = function setDirectory(directoryPath, exist, initial, type) { | |
if(directoryPath === this.path) { | |
if(!initial && this.watchers[withoutCase(this.path)]) { | |
this.watchers[withoutCase(this.path)].forEach(function(w) { | |
w.emit("change", directoryPath, w.data, initial ? "initial" : type); | |
}); | |
} | |
} else { | |
var old = this.directories[directoryPath]; | |
if(!old) { | |
if(exist) { | |
if(this.nestedWatching) { | |
this.createNestedWatcher(directoryPath); | |
} else { | |
this.directories[directoryPath] = true; | |
} | |
if(!initial && this.watchers[withoutCase(this.path)]) { | |
this.watchers[withoutCase(this.path)].forEach(function(w) { | |
w.emit("change", directoryPath, w.data, initial ? "initial" : type); | |
}); | |
} | |
if(this.watchers[withoutCase(directoryPath) + "#directory"]) { | |
this.watchers[withoutCase(directoryPath) + "#directory"].forEach(function(w) { | |
w.emit("change", w.data, initial ? "initial" : type); | |
}); | |
} | |
} | |
} else { | |
if(!exist) { | |
if(this.nestedWatching) | |
this.directories[directoryPath].close(); | |
delete this.directories[directoryPath]; | |
if(!initial && this.watchers[withoutCase(this.path)]) { | |
this.watchers[withoutCase(this.path)].forEach(function(w) { | |
w.emit("change", directoryPath, w.data, initial ? "initial" : type); | |
}); | |
} | |
if(this.watchers[withoutCase(directoryPath) + "#directory"]) { | |
this.watchers[withoutCase(directoryPath) + "#directory"].forEach(function(w) { | |
w.emit("change", directoryPath, w.data, initial ? "initial" : type); | |
}); | |
} | |
} | |
} | |
} | |
}; | |
DirectoryWatcher.prototype.createNestedWatcher = function(directoryPath) { | |
this.directories[directoryPath] = watcherManager.watchDirectory(directoryPath, this.options, 1); | |
this.directories[directoryPath].on("change", function(filePath, mtime, type) { | |
if(this.watchers[withoutCase(this.path)]) { | |
this.watchers[withoutCase(this.path)].forEach(function(w) { | |
if(w.checkStartTime(mtime, false)) { | |
w.emit("change", filePath, mtime, type); | |
} | |
}); | |
} | |
}.bind(this)); | |
}; | |
DirectoryWatcher.prototype.setNestedWatching = function(flag) { | |
if(this.nestedWatching !== !!flag) { | |
this.nestedWatching = !!flag; | |
if(this.nestedWatching) { | |
Object.keys(this.directories).forEach(function(directory) { | |
this.createNestedWatcher(directory); | |
}, this); | |
} else { | |
Object.keys(this.directories).forEach(function(directory) { | |
this.directories[directory].close(); | |
this.directories[directory] = true; | |
}, this); | |
} | |
} | |
}; | |
DirectoryWatcher.prototype.watch = function watch(filePath, startTime) { | |
this.watchers[withoutCase(filePath)] = this.watchers[withoutCase(filePath)] || []; | |
this.refs++; | |
var watcher = new Watcher(this, filePath, startTime); | |
watcher.on("closed", function() { | |
var idx = this.watchers[withoutCase(filePath)].indexOf(watcher); | |
this.watchers[withoutCase(filePath)].splice(idx, 1); | |
if(this.watchers[withoutCase(filePath)].length === 0) { | |
delete this.watchers[withoutCase(filePath)]; | |
if(this.path === filePath) | |
this.setNestedWatching(false); | |
} | |
if(--this.refs <= 0) | |
this.close(); | |
}.bind(this)); | |
this.watchers[withoutCase(filePath)].push(watcher); | |
var data; | |
if(filePath === this.path) { | |
this.setNestedWatching(true); | |
data = false; | |
Object.keys(this.files).forEach(function(file) { | |
var d = this.files[file]; | |
if(!data) | |
data = d; | |
else | |
data = [Math.max(data[0], d[0]), Math.max(data[1], d[1])]; | |
}, this); | |
} else { | |
data = this.files[filePath]; | |
} | |
process.nextTick(function() { | |
if(data) { | |
var ts = data[0] === data[1] ? data[0] + FS_ACCURACY : data[0]; | |
if(ts >= startTime) | |
watcher.emit("change", data[1]); | |
} else if(this.initialScan && this.initialScanRemoved.indexOf(filePath) >= 0) { | |
watcher.emit("remove"); | |
} | |
}.bind(this)); | |
return watcher; | |
}; | |
DirectoryWatcher.prototype.onFileAdded = function onFileAdded(filePath, stat) { | |
if(filePath.indexOf(this.path) !== 0) return; | |
if(/[\\\/]/.test(filePath.substr(this.path.length + 1))) return; | |
this.setFileTime(filePath, +stat.mtime || +stat.ctime || 1, false, "add"); | |
}; | |
DirectoryWatcher.prototype.onDirectoryAdded = function onDirectoryAdded(directoryPath /*, stat */) { | |
if(directoryPath.indexOf(this.path) !== 0) return; | |
if(/[\\\/]/.test(directoryPath.substr(this.path.length + 1))) return; | |
this.setDirectory(directoryPath, true, false, "add"); | |
}; | |
DirectoryWatcher.prototype.onChange = function onChange(filePath, stat) { | |
if(filePath.indexOf(this.path) !== 0) return; | |
if(/[\\\/]/.test(filePath.substr(this.path.length + 1))) return; | |
var mtime = +stat.mtime || +stat.ctime || 1; | |
ensureFsAccuracy(mtime); | |
this.setFileTime(filePath, mtime, false, "change"); | |
}; | |
DirectoryWatcher.prototype.onFileUnlinked = function onFileUnlinked(filePath) { | |
if(filePath.indexOf(this.path) !== 0) return; | |
if(/[\\\/]/.test(filePath.substr(this.path.length + 1))) return; | |
this.setFileTime(filePath, null, false, "unlink"); | |
if(this.initialScan) { | |
this.initialScanRemoved.push(filePath); | |
} | |
}; | |
DirectoryWatcher.prototype.onDirectoryUnlinked = function onDirectoryUnlinked(directoryPath) { | |
if(directoryPath.indexOf(this.path) !== 0) return; | |
if(/[\\\/]/.test(directoryPath.substr(this.path.length + 1))) return; | |
this.setDirectory(directoryPath, false, false, "unlink"); | |
if(this.initialScan) { | |
this.initialScanRemoved.push(directoryPath); | |
} | |
}; | |
DirectoryWatcher.prototype.onWatcherError = function onWatcherError(/* err */) { | |
}; | |
DirectoryWatcher.prototype.doInitialScan = function doInitialScan() { | |
fs.readdir(this.path, function(err, items) { | |
if(err) { | |
this.parentWatcher = watcherManager.watchFile(this.path + "#directory", this.options, 1); | |
this.parentWatcher.on("change", function(mtime, type) { | |
if(this.watchers[withoutCase(this.path)]) { | |
this.watchers[withoutCase(this.path)].forEach(function(w) { | |
w.emit("change", this.path, mtime, type); | |
}, this); | |
} | |
}.bind(this)); | |
this.initialScan = false; | |
return; | |
} | |
async.forEach(items, function(item, callback) { | |
var itemPath = path.join(this.path, item); | |
fs.stat(itemPath, function(err2, stat) { | |
if(!this.initialScan) return; | |
if(err2) { | |
callback(); | |
return; | |
} | |
if(stat.isFile()) { | |
if(!this.files[itemPath]) | |
this.setFileTime(itemPath, +stat.mtime || +stat.ctime || 1, true); | |
} else if(stat.isDirectory()) { | |
if(!this.directories[itemPath]) | |
this.setDirectory(itemPath, true, true); | |
} | |
callback(); | |
}.bind(this)); | |
}.bind(this), function() { | |
this.initialScan = false; | |
this.initialScanRemoved = null; | |
}.bind(this)); | |
}.bind(this)); | |
}; | |
DirectoryWatcher.prototype.getTimes = function() { | |
var obj = Object.create(null); | |
var selfTime = 0; | |
Object.keys(this.files).forEach(function(file) { | |
var data = this.files[file]; | |
var time; | |
if(data[1]) { | |
time = Math.max(data[0], data[1] + FS_ACCURACY); | |
} else { | |
time = data[0]; | |
} | |
obj[file] = time; | |
if(time > selfTime) | |
selfTime = time; | |
}, this); | |
if(this.nestedWatching) { | |
Object.keys(this.directories).forEach(function(dir) { | |
var w = this.directories[dir]; | |
var times = w.directoryWatcher.getTimes(); | |
Object.keys(times).forEach(function(file) { | |
var time = times[file]; | |
obj[file] = time; | |
if(time > selfTime) | |
selfTime = time; | |
}); | |
}, this); | |
obj[this.path] = selfTime; | |
} | |
return obj; | |
}; | |
DirectoryWatcher.prototype.close = function() { | |
this.initialScan = false; | |
this.watcher.close(); | |
if(this.nestedWatching) { | |
Object.keys(this.directories).forEach(function(dir) { | |
this.directories[dir].close(); | |
}, this); | |
} | |
if(this.parentWatcher) this.parentWatcher.close(); | |
this.emit("closed"); | |
}; | |
function ensureFsAccuracy(mtime) { | |
if(!mtime) return; | |
if(FS_ACCURACY > 1 && mtime % 1 !== 0) | |
FS_ACCURACY = 1; | |
else if(FS_ACCURACY > 10 && mtime % 10 !== 0) | |
FS_ACCURACY = 10; | |
else if(FS_ACCURACY > 100 && mtime % 100 !== 0) | |
FS_ACCURACY = 100; | |
} |