| /* | 
|     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; | 
| } |