UNPKG

9.25 kBJavaScriptView Raw
1/*
2 MIT License http://www.opensource.org/licenses/mit-license.php
3 Author Tobias Koppers @sokra
4*/
5var EventEmitter = require("events").EventEmitter;
6var async = require("async");
7var chokidar = require("chokidar");
8var fs = require("graceful-fs");
9var path = require("path");
10
11var watcherManager = require("./watcherManager");
12
13var FS_ACCURENCY = 10000;
14
15
16function DirectoryWatcher(path, options) {
17 EventEmitter.call(this);
18 this.path = path;
19 this.files = {};
20 this.directories = {};
21 this.watcher = chokidar.watch(path, {
22 ignoreInitial: true,
23 persistent: true,
24 followSymlinks: false,
25 depth: 0,
26 atomic: false,
27 ignorePermissionErrors: true
28 });
29 this.watcher.on("add", this.onFileAdded.bind(this));
30 this.watcher.on("addDir", this.onDirectoryAdded.bind(this));
31 this.watcher.on("change", this.onChange.bind(this));
32 this.watcher.on("unlink", this.onFileUnlinked.bind(this));
33 this.watcher.on("unlinkDir", this.onDirectoryUnlinked.bind(this));
34 this.watcher.on("error", this.onWatcherError.bind(this));
35 this.initialScan = true;
36 this.nestedWatching = false;
37 this.initialScanRemoved = [];
38 this.doInitialScan();
39 this.watchers = {};
40 this.refs = 0;
41}
42module.exports = DirectoryWatcher;
43
44DirectoryWatcher.prototype = Object.create(EventEmitter.prototype);
45DirectoryWatcher.prototype.constructor = DirectoryWatcher;
46
47DirectoryWatcher.prototype.setFileTime = function setFileTime(path, mtime, initial, type) {
48 var now = Date.now();
49 var old = this.files[path];
50 this.files[path] = [initial ? Math.min(now, mtime) : now, mtime];
51 if(!old) {
52 if(mtime) {
53 if(this.watchers[path]) {
54 this.watchers[path].forEach(function(w) {
55 if(!initial || w.checkStartTime(mtime, initial)) {
56 w.emit("change", mtime);
57 }
58 });
59 }
60 }
61 } else if(!initial && mtime && type !== "add") {
62 if(this.watchers[path]) {
63 this.watchers[path].forEach(function(w) {
64 w.emit("change", mtime);
65 });
66 }
67 } else if(!initial && !mtime) {
68 if(this.watchers[path]) {
69 this.watchers[path].forEach(function(w) {
70 w.emit("remove");
71 });
72 }
73 }
74 if(this.watchers[this.path]) {
75 this.watchers[this.path].forEach(function(w) {
76 if(!initial || w.checkStartTime(mtime, initial)) {
77 w.emit("change", path, mtime);
78 }
79 });
80 }
81};
82
83DirectoryWatcher.prototype.setDirectory = function setDirectory(path, exist, initial) {
84 var old = this.directories[path];
85 if(!old) {
86 if(exist) {
87 if(this.nestedWatching) {
88 this.createNestedWatcher(path);
89 } else {
90 this.directories[path] = true;
91 }
92 }
93 } else {
94 if(!exist) {
95 if(this.nestedWatching)
96 this.directories[path].close();
97 delete this.directories[path];
98 if(!initial && this.watchers[this.path]) {
99 this.watchers[this.path].forEach(function(w) {
100 w.emit("change", path, w.data);
101 });
102 }
103 }
104 }
105};
106
107DirectoryWatcher.prototype.createNestedWatcher = function(path) {
108 this.directories[path] = watcherManager.watchDirectory(path, 1);
109 this.directories[path].on("change", function(path, mtime) {
110 if(this.watchers[this.path]) {
111 this.watchers[this.path].forEach(function(w) {
112 if(w.checkStartTime(mtime, false)) {
113 w.emit("change", path, mtime);
114 }
115 });
116 }
117 }.bind(this));
118};
119
120DirectoryWatcher.prototype.setNestedWatching = function(flag) {
121 if(this.nestedWatching !== !!flag) {
122 this.nestedWatching = !!flag;
123 if(this.nestedWatching) {
124 Object.keys(this.directories).forEach(function(path) {
125 this.createNestedWatcher(path);
126 }, this);
127 } else {
128 Object.keys(this.directories).forEach(function(path) {
129 this.directories[path].close();
130 this.directories[path] = true;
131 }, this);
132 }
133 }
134};
135
136DirectoryWatcher.prototype.watch = function watch(path, startTime) {
137 this.watchers[path] = this.watchers[path] || [];
138 this.refs++;
139 var watcher = new Watcher(this, path, startTime);
140 watcher.on("closed", function() {
141 var idx = this.watchers[path].indexOf(watcher);
142 this.watchers[path].splice(idx, 1);
143 if(this.watchers[path].length === 0) {
144 delete this.watchers[path];
145 if(this.path === path)
146 this.setNestedWatching(false);
147 }
148 if(--this.refs <= 0)
149 this.close();
150 }.bind(this));
151 this.watchers[path].push(watcher);
152 if(path === this.path) {
153 this.setNestedWatching(true);
154 var data = false;
155 Object.keys(this.files).forEach(function(file) {
156 var d = this.files[file];
157 if(!data)
158 data = d;
159 else
160 data = [Math.max(data[0], d[0]), Math.max(data[1], d[1])];
161 }, this);
162 } else {
163 var data = this.files[path];
164 }
165 process.nextTick(function() {
166 if(data) {
167 if(data[0] > startTime)
168 watcher.emit("change", data[1]);
169 } else if(this.initialScan && this.initialScanRemoved.indexOf(path) >= 0) {
170 watcher.emit("remove");
171 }
172 }.bind(this));
173 return watcher;
174};
175
176DirectoryWatcher.prototype.onFileAdded = function onFileAdded(path, stat) {
177 if(path.indexOf(this.path) !== 0) return;
178 if(/[\\\/]/.test(path.substr(this.path.length + 1))) return;
179
180 if(!stat) {
181 return fs.stat(path, function(err, stat) {
182 if(err) return this.onWatcherError(err);
183 this.onFileAdded(path, stat);
184 }.bind(this));
185 }
186 this.setFileTime(path, +stat.mtime, false, "add");
187};
188
189DirectoryWatcher.prototype.onDirectoryAdded = function onDirectoryAdded(path, stat) {
190 if(path.indexOf(this.path) !== 0) return;
191 if(/[\\\/]/.test(path.substr(this.path.length + 1))) return;
192 this.setDirectory(path, true, false);
193};
194
195DirectoryWatcher.prototype.onChange = function onChange(path, stat) {
196 if(path.indexOf(this.path) !== 0) return;
197 if(/[\\\/]/.test(path.substr(this.path.length + 1))) return;
198 if(!stat) {
199 return fs.stat(path, function(err, stat) {
200 if(err) {
201 if(err.code === "ENOENT")
202 return this.setFileTime(path, null, false, "change");
203 else
204 return this.onWatcherError(err);
205 }
206 this.onChange(path, stat);
207 }.bind(this));
208 }
209 var mtime = +stat.mtime;
210 if(FS_ACCURENCY > 1 && mtime % 1 !== 0)
211 FS_ACCURENCY = 1;
212 else if(FS_ACCURENCY > 10 && mtime % 10 !== 0)
213 FS_ACCURENCY = 10;
214 else if(FS_ACCURENCY > 100 && mtime % 100 !== 0)
215 FS_ACCURENCY = 100;
216 else if(FS_ACCURENCY > 1000 && mtime % 1000 !== 0)
217 FS_ACCURENCY = 1000;
218 else if(FS_ACCURENCY > 2000 && mtime % 2000 !== 0)
219 FS_ACCURENCY = 2000;
220 this.setFileTime(path, mtime, false, "change");
221};
222
223DirectoryWatcher.prototype.onFileUnlinked = function onFileUnlinked(path) {
224 if(path.indexOf(this.path) !== 0) return;
225 if(/[\\\/]/.test(path.substr(this.path.length + 1))) return;
226 this.setFileTime(path, null, false, "unlink");
227 if(this.initialScan) {
228 this.initialScanRemoved.push(path);
229 }
230};
231
232DirectoryWatcher.prototype.onDirectoryUnlinked = function onDirectoryUnlinked(path) {
233 if(path.indexOf(this.path) !== 0) return;
234 if(/[\\\/]/.test(path.substr(this.path.length + 1))) return;
235 this.setDirectory(path, false, false);
236 if(this.initialScan) {
237 this.initialScanRemoved.push(path);
238 }
239};
240
241DirectoryWatcher.prototype.onWatcherError = function onWatcherError(err) {
242};
243
244DirectoryWatcher.prototype.doInitialScan = function doInitialScan() {
245 fs.readdir(this.path, function(err, items) {
246 if(err) {
247 this.initialScan = false;
248 return;
249 }
250 async.forEach(items, function(item, callback) {
251 var itemPath = path.join(this.path, item);
252 fs.stat(itemPath, function(err, stat) {
253 if(!this.initialScan) return;
254 if(err) {
255 return callback();
256 }
257 if(stat.isFile()) {
258 if(!this.files[itemPath])
259 this.setFileTime(itemPath, +stat.mtime, true);
260 } else if(stat.isDirectory()) {
261 if(!this.directories[itemPath])
262 this.setDirectory(itemPath, true, true);
263 }
264 return callback();
265 }.bind(this));
266 }.bind(this), function(err) {
267 this.initialScan = false;
268 this.initialScanRemoved = null;
269 }.bind(this));
270 }.bind(this));
271};
272
273DirectoryWatcher.prototype.getTimes = function() {
274 var obj = {};
275 var selfTime = 0;
276 Object.keys(this.files).forEach(function(file) {
277 var data = this.files[file];
278 if(data[1]) {
279 var time = Math.max(data[0], data[1]);
280 obj[file] = time;
281 if(time > selfTime)
282 selfTime = time;
283 }
284 }, this);
285 obj[this.path] = selfTime;
286 return obj;
287};
288
289DirectoryWatcher.prototype.close = function() {
290 this.initialScan = false;
291 this.watcher.close();
292 if(this.nestedWatching) {
293 Object.keys(this.directories).forEach(function(dir) {
294 this.directories[dir].close();
295 }, this);
296 }
297 this.emit("closed");
298};
299
300function Watcher(directoryWatcher, path, startTime) {
301 EventEmitter.call(this);
302 this.directoryWatcher = directoryWatcher;
303 this.path = path;
304 this.startTime = startTime && +startTime;
305 this.data = 0;
306}
307
308Watcher.prototype = Object.create(EventEmitter.prototype);
309Watcher.prototype.constructor = Watcher;
310
311Watcher.prototype.checkStartTime = function checkStartTime(mtime, initial) {
312 if(typeof this.startTime !== "number") return !initial;
313 var startTime = this.startTime && Math.floor(this.startTime / FS_ACCURENCY) * FS_ACCURENCY;
314 return startTime <= mtime;
315}
316
317Watcher.prototype.close = function close() {
318 this.emit("closed");
319};