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 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 | }
|
42 | module.exports = DirectoryWatcher;
|
43 |
|
44 | DirectoryWatcher.prototype = Object.create(EventEmitter.prototype);
|
45 | DirectoryWatcher.prototype.constructor = DirectoryWatcher;
|
46 |
|
47 | DirectoryWatcher.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 |
|
83 | DirectoryWatcher.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 |
|
107 | DirectoryWatcher.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 |
|
120 | DirectoryWatcher.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 |
|
136 | DirectoryWatcher.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 |
|
176 | DirectoryWatcher.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 |
|
189 | DirectoryWatcher.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 |
|
195 | DirectoryWatcher.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 |
|
223 | DirectoryWatcher.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 |
|
232 | DirectoryWatcher.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 |
|
241 | DirectoryWatcher.prototype.onWatcherError = function onWatcherError(err) {
|
242 | };
|
243 |
|
244 | DirectoryWatcher.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 |
|
273 | DirectoryWatcher.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 |
|
289 | DirectoryWatcher.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 |
|
300 | function 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 |
|
308 | Watcher.prototype = Object.create(EventEmitter.prototype);
|
309 | Watcher.prototype.constructor = Watcher;
|
310 |
|
311 | Watcher.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 |
|
317 | Watcher.prototype.close = function close() {
|
318 | this.emit("closed");
|
319 | };
|