You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							379 lines
						
					
					
						
							12 KiB
						
					
					
				
			
		
		
	
	
							379 lines
						
					
					
						
							12 KiB
						
					
					
				/* | 
						|
	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; | 
						|
}
 | 
						|
 |