| /* |
| 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) { |
| console.warn("Error from chokidar (" + this.path + "): " + 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; |
| var p = this.watcher.close(); |
| if(p && p.catch) p.catch(this.onWatcherError.bind(this)); |
| 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; |
| } |