UNPKG

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