1 |
|
2 |
|
3 |
|
4 |
|
5 | var EventEmitter = require("events").EventEmitter;
|
6 | var async = require("async");
|
7 | var chokidar = require("chokidar");
|
8 | var fs = require("graceful-fs");
|
9 | var path = require("path");
|
10 |
|
11 | var watcherManager = require("./watcherManager");
|
12 |
|
13 | var FS_ACCURENCY = 10000;
|
14 |
|
15 |
|
16 | function withoutCase(str) {
|
17 | return str.toLowerCase();
|
18 | }
|
19 |
|
20 |
|
21 | function 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 |
|
29 | Watcher.prototype = Object.create(EventEmitter.prototype);
|
30 | Watcher.prototype.constructor = Watcher;
|
31 |
|
32 | Watcher.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 |
|
38 | Watcher.prototype.close = function close() {
|
39 | this.emit("closed");
|
40 | };
|
41 |
|
42 |
|
43 | function 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 | }
|
72 | module.exports = DirectoryWatcher;
|
73 |
|
74 | DirectoryWatcher.prototype = Object.create(EventEmitter.prototype);
|
75 | DirectoryWatcher.prototype.constructor = DirectoryWatcher;
|
76 |
|
77 | DirectoryWatcher.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 |
|
113 | DirectoryWatcher.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 |
|
137 | DirectoryWatcher.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 |
|
150 | DirectoryWatcher.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 |
|
166 | DirectoryWatcher.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 |
|
207 | DirectoryWatcher.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 |
|
214 | DirectoryWatcher.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 |
|
220 | DirectoryWatcher.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 |
|
237 | DirectoryWatcher.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 |
|
246 | DirectoryWatcher.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 |
|
255 | DirectoryWatcher.prototype.onWatcherError = function onWatcherError(/* err */) {
|
256 | };
|
257 |
|
258 | DirectoryWatcher.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 |
|
288 | DirectoryWatcher.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 |
|
316 | DirectoryWatcher.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 | };
|