@kadi.build/file-manager 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +268 -0
- package/package.json +48 -0
- package/src/ConfigManager.js +301 -0
- package/src/FileManager.js +526 -0
- package/src/index.js +48 -0
- package/src/providers/CompressionProvider.js +968 -0
- package/src/providers/LocalProvider.js +824 -0
- package/src/providers/RemoteProvider.js +514 -0
- package/src/providers/WatchProvider.js +611 -0
- package/src/utils/FileStreamingUtils.js +757 -0
- package/src/utils/PathUtils.js +144 -0
|
@@ -0,0 +1,611 @@
|
|
|
1
|
+
import chokidar from 'chokidar';
|
|
2
|
+
import { promises as fs } from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { EventEmitter } from 'events';
|
|
5
|
+
|
|
6
|
+
class WatchProvider extends EventEmitter {
|
|
7
|
+
constructor(config) {
|
|
8
|
+
super();
|
|
9
|
+
this.config = config || {};
|
|
10
|
+
this.localRoot = this.config.localRoot || process.cwd();
|
|
11
|
+
this.enabled = this.config.enabled !== false; // Default true
|
|
12
|
+
this.recursive = this.config.recursive !== false; // Default true
|
|
13
|
+
this.ignoreDotfiles = this.config.ignoreDotfiles !== false; // Default true
|
|
14
|
+
this.debounceMs = this.config.debounceMs || 100;
|
|
15
|
+
this.maxWatchers = this.config.maxWatchers || 50;
|
|
16
|
+
|
|
17
|
+
// Active watchers registry
|
|
18
|
+
this.watchers = new Map();
|
|
19
|
+
this.debounceTimers = new Map();
|
|
20
|
+
this.watchCount = 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ============================================================================
|
|
24
|
+
// CONNECTION AND VALIDATION
|
|
25
|
+
// ============================================================================
|
|
26
|
+
|
|
27
|
+
async testConnection() {
|
|
28
|
+
try {
|
|
29
|
+
const stats = await fs.stat(this.localRoot);
|
|
30
|
+
if (!stats.isDirectory()) {
|
|
31
|
+
throw new Error(`Local root '${this.localRoot}' is not a directory`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const testWatcher = chokidar.watch(this.localRoot, {
|
|
35
|
+
ignored: () => true,
|
|
36
|
+
persistent: false,
|
|
37
|
+
ignoreInitial: true
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
await new Promise((resolve, reject) => {
|
|
41
|
+
const timeout = setTimeout(() => {
|
|
42
|
+
reject(new Error('Chokidar initialization timeout'));
|
|
43
|
+
}, 5000);
|
|
44
|
+
|
|
45
|
+
testWatcher.on('ready', () => {
|
|
46
|
+
clearTimeout(timeout);
|
|
47
|
+
testWatcher.close();
|
|
48
|
+
resolve();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
testWatcher.on('error', (error) => {
|
|
52
|
+
clearTimeout(timeout);
|
|
53
|
+
reject(error);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
provider: 'watch',
|
|
59
|
+
localRoot: this.localRoot,
|
|
60
|
+
enabled: this.enabled,
|
|
61
|
+
recursive: this.recursive,
|
|
62
|
+
activeWatchers: this.watchers.size,
|
|
63
|
+
maxWatchers: this.maxWatchers,
|
|
64
|
+
debounceMs: this.debounceMs
|
|
65
|
+
};
|
|
66
|
+
} catch (error) {
|
|
67
|
+
throw new Error(`Watch provider connection test failed: ${error.message}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
validateConfig() {
|
|
72
|
+
const errors = [];
|
|
73
|
+
const warnings = [];
|
|
74
|
+
|
|
75
|
+
if (!this.localRoot) {
|
|
76
|
+
errors.push('Local root directory is required for watching');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (this.debounceMs < 0) {
|
|
80
|
+
errors.push('Debounce time must be non-negative');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (this.maxWatchers <= 0) {
|
|
84
|
+
errors.push('Max watchers must be positive');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (this.debounceMs > 10000) {
|
|
88
|
+
warnings.push('Very high debounce time (>10s) may delay notifications significantly');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (this.maxWatchers > 100) {
|
|
92
|
+
warnings.push('Very high max watchers (>100) may impact performance');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!this.enabled) {
|
|
96
|
+
warnings.push('File watching is disabled');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
isValid: errors.length === 0,
|
|
101
|
+
errors,
|
|
102
|
+
warnings
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ============================================================================
|
|
107
|
+
// PATH MANAGEMENT METHODS
|
|
108
|
+
// ============================================================================
|
|
109
|
+
|
|
110
|
+
normalizePath(inputPath) {
|
|
111
|
+
if (!inputPath || inputPath === '/') {
|
|
112
|
+
return this.localRoot;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (path.isAbsolute(inputPath)) {
|
|
116
|
+
return path.normalize(inputPath);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const resolvedLocalRoot = path.resolve(this.localRoot);
|
|
120
|
+
return path.resolve(resolvedLocalRoot, inputPath);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
validatePath(inputPath) {
|
|
124
|
+
if (!inputPath) {
|
|
125
|
+
throw new Error('Path cannot be empty');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (/[<>:"|?*\x00-\x1f]/.test(inputPath)) {
|
|
129
|
+
throw new Error(`Path contains invalid characters: ${inputPath}`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
generateWatchId(watchPath, options = {}) {
|
|
136
|
+
const normalizedPath = this.normalizePath(watchPath);
|
|
137
|
+
const optionsHash = JSON.stringify(options);
|
|
138
|
+
return `${normalizedPath}:${optionsHash}`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ============================================================================
|
|
142
|
+
// CORE WATCHING METHODS
|
|
143
|
+
// ============================================================================
|
|
144
|
+
|
|
145
|
+
async startWatching(watchPath, options = {}) {
|
|
146
|
+
this.validatePath(watchPath);
|
|
147
|
+
|
|
148
|
+
if (!this.enabled) {
|
|
149
|
+
throw new Error('File watching is disabled in configuration');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const normalizedPath = this.normalizePath(watchPath);
|
|
153
|
+
|
|
154
|
+
// Check if path exists
|
|
155
|
+
try {
|
|
156
|
+
const stats = await fs.stat(normalizedPath);
|
|
157
|
+
if (!stats.isDirectory() && !stats.isFile()) {
|
|
158
|
+
throw new Error(`Path '${watchPath}' is neither a file nor directory`);
|
|
159
|
+
}
|
|
160
|
+
} catch (error) {
|
|
161
|
+
if (error.code === 'ENOENT') {
|
|
162
|
+
throw new Error(`Path not found: ${watchPath}`);
|
|
163
|
+
}
|
|
164
|
+
throw error;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const watchId = this.generateWatchId(watchPath, options);
|
|
168
|
+
|
|
169
|
+
// Check if already watching
|
|
170
|
+
if (this.watchers.has(watchId)) {
|
|
171
|
+
return {
|
|
172
|
+
watchId,
|
|
173
|
+
path: watchPath,
|
|
174
|
+
alreadyWatching: true
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Check watcher limit
|
|
179
|
+
if (this.watchCount >= this.maxWatchers) {
|
|
180
|
+
throw new Error(`Maximum number of watchers (${this.maxWatchers}) reached`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const {
|
|
184
|
+
recursive = this.recursive,
|
|
185
|
+
ignoreDotfiles = this.ignoreDotfiles,
|
|
186
|
+
events = ['add', 'change', 'unlink', 'addDir', 'unlinkDir'],
|
|
187
|
+
callback = null,
|
|
188
|
+
persistent = true
|
|
189
|
+
} = options;
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
const ignorePattern = this.buildIgnorePattern(ignoreDotfiles, recursive, normalizedPath);
|
|
193
|
+
|
|
194
|
+
const watcher = chokidar.watch(normalizedPath, {
|
|
195
|
+
ignored: ignorePattern,
|
|
196
|
+
persistent: persistent,
|
|
197
|
+
ignoreInitial: true,
|
|
198
|
+
followSymlinks: false,
|
|
199
|
+
disableGlobbing: true,
|
|
200
|
+
usePolling: false,
|
|
201
|
+
interval: 100,
|
|
202
|
+
binaryInterval: 300,
|
|
203
|
+
ignorePermissionErrors: true
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// Set up event handlers
|
|
207
|
+
this.setupWatcherEvents(watcher, watchId, watchPath, events, callback);
|
|
208
|
+
|
|
209
|
+
// Wait for watcher to be ready
|
|
210
|
+
await new Promise((resolve, reject) => {
|
|
211
|
+
const timeout = setTimeout(() => {
|
|
212
|
+
reject(new Error('Watcher initialization timeout'));
|
|
213
|
+
}, 10000);
|
|
214
|
+
|
|
215
|
+
watcher.on('ready', () => {
|
|
216
|
+
clearTimeout(timeout);
|
|
217
|
+
resolve();
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
watcher.on('error', (error) => {
|
|
221
|
+
clearTimeout(timeout);
|
|
222
|
+
reject(error);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// Store watcher info
|
|
227
|
+
this.watchers.set(watchId, {
|
|
228
|
+
watcher,
|
|
229
|
+
path: watchPath,
|
|
230
|
+
normalizedPath,
|
|
231
|
+
options: {
|
|
232
|
+
recursive: recursive,
|
|
233
|
+
ignoreDotfiles: ignoreDotfiles,
|
|
234
|
+
events: events,
|
|
235
|
+
persistent: persistent
|
|
236
|
+
},
|
|
237
|
+
callback,
|
|
238
|
+
startedAt: new Date().toISOString(),
|
|
239
|
+
eventCount: 0
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
this.watchCount++;
|
|
243
|
+
|
|
244
|
+
return {
|
|
245
|
+
watchId,
|
|
246
|
+
path: watchPath,
|
|
247
|
+
recursive,
|
|
248
|
+
events,
|
|
249
|
+
startedAt: new Date().toISOString()
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
} catch (error) {
|
|
253
|
+
throw new Error(`Failed to start watching ${watchPath}: ${error.message}`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async stopWatching(watchIdOrPath) {
|
|
258
|
+
let watchId;
|
|
259
|
+
|
|
260
|
+
if (this.watchers.has(watchIdOrPath)) {
|
|
261
|
+
watchId = watchIdOrPath;
|
|
262
|
+
} else {
|
|
263
|
+
watchId = this.findWatcherByPath(watchIdOrPath);
|
|
264
|
+
if (!watchId) {
|
|
265
|
+
throw new Error(`No active watcher found for: ${watchIdOrPath}`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const watcherInfo = this.watchers.get(watchId);
|
|
270
|
+
if (!watcherInfo) {
|
|
271
|
+
throw new Error(`Watcher not found: ${watchId}`);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
try {
|
|
275
|
+
await watcherInfo.watcher.close();
|
|
276
|
+
|
|
277
|
+
this.cleanupDebounceTimers(watchId);
|
|
278
|
+
|
|
279
|
+
this.watchers.delete(watchId);
|
|
280
|
+
this.watchCount--;
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
watchId,
|
|
284
|
+
path: watcherInfo.path,
|
|
285
|
+
stopped: true,
|
|
286
|
+
eventCount: watcherInfo.eventCount,
|
|
287
|
+
duration: Date.now() - new Date(watcherInfo.startedAt).getTime()
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
} catch (error) {
|
|
291
|
+
throw new Error(`Failed to stop watching: ${error.message}`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async stopAllWatching() {
|
|
296
|
+
const results = [];
|
|
297
|
+
const watchIds = Array.from(this.watchers.keys());
|
|
298
|
+
|
|
299
|
+
for (const watchId of watchIds) {
|
|
300
|
+
try {
|
|
301
|
+
const result = await this.stopWatching(watchId);
|
|
302
|
+
results.push(result);
|
|
303
|
+
} catch (error) {
|
|
304
|
+
results.push({
|
|
305
|
+
watchId,
|
|
306
|
+
error: error.message
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
this.debounceTimers.clear();
|
|
312
|
+
|
|
313
|
+
return {
|
|
314
|
+
stopped: results.filter(r => r.stopped).length,
|
|
315
|
+
failed: results.filter(r => r.error).length,
|
|
316
|
+
results
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// ============================================================================
|
|
321
|
+
// WATCHER MANAGEMENT METHODS
|
|
322
|
+
// ============================================================================
|
|
323
|
+
|
|
324
|
+
setupWatcherEvents(watcher, watchId, watchPath, events, callback) {
|
|
325
|
+
const eventHandlers = {
|
|
326
|
+
add: (filePath) => this.handleFileEvent('add', filePath, watchId, watchPath, callback),
|
|
327
|
+
change: (filePath) => this.handleFileEvent('change', filePath, watchId, watchPath, callback),
|
|
328
|
+
unlink: (filePath) => this.handleFileEvent('unlink', filePath, watchId, watchPath, callback),
|
|
329
|
+
addDir: (dirPath) => this.handleFileEvent('addDir', dirPath, watchId, watchPath, callback),
|
|
330
|
+
unlinkDir: (dirPath) => this.handleFileEvent('unlinkDir', dirPath, watchId, watchPath, callback)
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
events.forEach(eventType => {
|
|
334
|
+
if (eventHandlers[eventType]) {
|
|
335
|
+
watcher.on(eventType, eventHandlers[eventType]);
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
watcher.on('error', (error) => {
|
|
340
|
+
this.emit('watcherError', {
|
|
341
|
+
watchId,
|
|
342
|
+
path: watchPath,
|
|
343
|
+
error: error.message
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
handleFileEvent(eventType, filePath, watchId, watchPath, callback) {
|
|
349
|
+
const watcherInfo = this.watchers.get(watchId);
|
|
350
|
+
if (!watcherInfo) {
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Non-recursive safety filter
|
|
355
|
+
const isNonRecursive = watcherInfo.options.recursive === false;
|
|
356
|
+
|
|
357
|
+
if (isNonRecursive) {
|
|
358
|
+
const watchedPath = path.resolve(watcherInfo.normalizedPath);
|
|
359
|
+
const eventPath = path.resolve(filePath);
|
|
360
|
+
const eventParentDir = path.dirname(eventPath);
|
|
361
|
+
|
|
362
|
+
if (eventParentDir !== watchedPath) {
|
|
363
|
+
return; // Block nested events
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Increment event counter
|
|
368
|
+
watcherInfo.eventCount = (watcherInfo.eventCount || 0) + 1;
|
|
369
|
+
|
|
370
|
+
// Create event data
|
|
371
|
+
const eventData = {
|
|
372
|
+
type: eventType,
|
|
373
|
+
path: filePath,
|
|
374
|
+
relativePath: path.relative(watcherInfo.normalizedPath, filePath),
|
|
375
|
+
watchId,
|
|
376
|
+
watchPath,
|
|
377
|
+
timestamp: new Date().toISOString()
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
// Apply debouncing
|
|
381
|
+
if (this.debounceMs > 0) {
|
|
382
|
+
this.debounceEvent(eventData, callback);
|
|
383
|
+
} else {
|
|
384
|
+
this.processEvent(eventData, callback);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
debounceEvent(eventData, callback) {
|
|
389
|
+
const debounceKey = `${eventData.watchId}:${eventData.path}:${eventData.type}`;
|
|
390
|
+
|
|
391
|
+
if (this.debounceTimers.has(debounceKey)) {
|
|
392
|
+
clearTimeout(this.debounceTimers.get(debounceKey));
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const timer = setTimeout(() => {
|
|
396
|
+
this.debounceTimers.delete(debounceKey);
|
|
397
|
+
this.processEvent(eventData, callback);
|
|
398
|
+
}, this.debounceMs);
|
|
399
|
+
|
|
400
|
+
this.debounceTimers.set(debounceKey, timer);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
processEvent(eventData, callback) {
|
|
404
|
+
this.emit('fileEvent', eventData);
|
|
405
|
+
|
|
406
|
+
if (callback && typeof callback === 'function') {
|
|
407
|
+
try {
|
|
408
|
+
callback(eventData);
|
|
409
|
+
} catch (error) {
|
|
410
|
+
this.emit('watcherError', {
|
|
411
|
+
watchId: eventData.watchId,
|
|
412
|
+
path: eventData.path,
|
|
413
|
+
error: `Callback error: ${error.message}`
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// ============================================================================
|
|
420
|
+
// UTILITY AND QUERY METHODS
|
|
421
|
+
// ============================================================================
|
|
422
|
+
|
|
423
|
+
buildIgnorePattern(ignoreDotfiles, isRecursive = true, watchedPath = null) {
|
|
424
|
+
const patterns = [];
|
|
425
|
+
|
|
426
|
+
if (ignoreDotfiles) {
|
|
427
|
+
patterns.push(/(^|[\/\\])\../);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Always ignore common system files
|
|
431
|
+
patterns.push(/node_modules/);
|
|
432
|
+
patterns.push(/\.git/);
|
|
433
|
+
patterns.push(/\.DS_Store/);
|
|
434
|
+
patterns.push(/Thumbs\.db/);
|
|
435
|
+
|
|
436
|
+
// For non-recursive, add a pattern that ignores anything in subdirectories
|
|
437
|
+
if (!isRecursive && watchedPath) {
|
|
438
|
+
const resolvedWatchedPath = path.resolve(watchedPath);
|
|
439
|
+
const nestedPattern = new RegExp(
|
|
440
|
+
resolvedWatchedPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') +
|
|
441
|
+
'[\\\\/][^\\\\\/]+[\\\\/]'
|
|
442
|
+
);
|
|
443
|
+
patterns.push(nestedPattern);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return patterns.length > 0 ? patterns : null;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
findWatcherByPath(searchPath) {
|
|
450
|
+
const normalizedSearch = this.normalizePath(searchPath);
|
|
451
|
+
|
|
452
|
+
for (const [watchId, watcherInfo] of this.watchers) {
|
|
453
|
+
if (watcherInfo.normalizedPath === normalizedSearch ||
|
|
454
|
+
watcherInfo.path === searchPath) {
|
|
455
|
+
return watchId;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
return null;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
cleanupDebounceTimers(watchId) {
|
|
463
|
+
const keysToDelete = [];
|
|
464
|
+
|
|
465
|
+
for (const [key, timer] of this.debounceTimers) {
|
|
466
|
+
if (key.startsWith(watchId + ':')) {
|
|
467
|
+
clearTimeout(timer);
|
|
468
|
+
keysToDelete.push(key);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
keysToDelete.forEach(key => this.debounceTimers.delete(key));
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
getEventIcon(eventType) {
|
|
476
|
+
const icons = {
|
|
477
|
+
add: '📄',
|
|
478
|
+
change: '✏️',
|
|
479
|
+
unlink: '🗑️',
|
|
480
|
+
addDir: '📁',
|
|
481
|
+
unlinkDir: '🗂️'
|
|
482
|
+
};
|
|
483
|
+
return icons[eventType] || '📋';
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// ============================================================================
|
|
487
|
+
// INFORMATION AND STATUS METHODS
|
|
488
|
+
// ============================================================================
|
|
489
|
+
|
|
490
|
+
listActiveWatchers() {
|
|
491
|
+
const watchers = [];
|
|
492
|
+
|
|
493
|
+
for (const [watchId, watcherInfo] of this.watchers) {
|
|
494
|
+
watchers.push({
|
|
495
|
+
watchId,
|
|
496
|
+
path: watcherInfo.path,
|
|
497
|
+
recursive: watcherInfo.options.recursive,
|
|
498
|
+
events: watcherInfo.options.events,
|
|
499
|
+
startedAt: watcherInfo.startedAt,
|
|
500
|
+
eventCount: watcherInfo.eventCount || 0,
|
|
501
|
+
duration: Date.now() - new Date(watcherInfo.startedAt).getTime()
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
return watchers;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
getWatcherInfo(watchIdOrPath) {
|
|
509
|
+
let watchId;
|
|
510
|
+
|
|
511
|
+
if (this.watchers.has(watchIdOrPath)) {
|
|
512
|
+
watchId = watchIdOrPath;
|
|
513
|
+
} else {
|
|
514
|
+
watchId = this.findWatcherByPath(watchIdOrPath);
|
|
515
|
+
if (!watchId) {
|
|
516
|
+
throw new Error(`Watcher not found: ${watchIdOrPath}`);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const watcherInfo = this.watchers.get(watchId);
|
|
521
|
+
if (!watcherInfo) {
|
|
522
|
+
throw new Error(`Watcher not found: ${watchId}`);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return {
|
|
526
|
+
watchId,
|
|
527
|
+
path: watcherInfo.path,
|
|
528
|
+
normalizedPath: watcherInfo.normalizedPath,
|
|
529
|
+
options: watcherInfo.options,
|
|
530
|
+
startedAt: watcherInfo.startedAt,
|
|
531
|
+
eventCount: watcherInfo.eventCount || 0,
|
|
532
|
+
duration: Date.now() - new Date(watcherInfo.startedAt).getTime(),
|
|
533
|
+
hasCallback: !!watcherInfo.callback
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
getWatchingStatus() {
|
|
538
|
+
return {
|
|
539
|
+
enabled: this.enabled,
|
|
540
|
+
activeWatchers: this.watchers.size,
|
|
541
|
+
maxWatchers: this.maxWatchers,
|
|
542
|
+
totalEvents: Array.from(this.watchers.values()).reduce((sum, w) => sum + (w.eventCount || 0), 0),
|
|
543
|
+
pendingDebounces: this.debounceTimers.size,
|
|
544
|
+
config: {
|
|
545
|
+
recursive: this.recursive,
|
|
546
|
+
ignoreDotfiles: this.ignoreDotfiles,
|
|
547
|
+
debounceMs: this.debounceMs
|
|
548
|
+
}
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// ============================================================================
|
|
553
|
+
// ERROR HANDLING HELPERS
|
|
554
|
+
// ============================================================================
|
|
555
|
+
|
|
556
|
+
isPathNotFoundError(error) {
|
|
557
|
+
return error.code === 'ENOENT' ||
|
|
558
|
+
error.message.includes('not found') ||
|
|
559
|
+
error.message.includes('no such file');
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
isPermissionError(error) {
|
|
563
|
+
return error.code === 'EACCES' ||
|
|
564
|
+
error.code === 'EPERM' ||
|
|
565
|
+
error.message.includes('permission denied');
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
isWatchingError(error) {
|
|
569
|
+
return error.message.includes('watch') ||
|
|
570
|
+
error.message.includes('EMFILE') ||
|
|
571
|
+
error.message.includes('ENOSPC');
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// ============================================================================
|
|
575
|
+
// CLEANUP AND SHUTDOWN
|
|
576
|
+
// ============================================================================
|
|
577
|
+
|
|
578
|
+
async shutdown() {
|
|
579
|
+
try {
|
|
580
|
+
const result = await this.stopAllWatching();
|
|
581
|
+
this.removeAllListeners();
|
|
582
|
+
return result;
|
|
583
|
+
} catch (error) {
|
|
584
|
+
throw error;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
formatBytes(bytes) {
|
|
589
|
+
if (bytes === 0) return '0 Bytes';
|
|
590
|
+
const k = 1024;
|
|
591
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
|
592
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
593
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
formatDuration(milliseconds) {
|
|
597
|
+
const seconds = Math.floor(milliseconds / 1000);
|
|
598
|
+
const minutes = Math.floor(seconds / 60);
|
|
599
|
+
const hours = Math.floor(minutes / 60);
|
|
600
|
+
|
|
601
|
+
if (hours > 0) {
|
|
602
|
+
return `${hours}h ${minutes % 60}m ${seconds % 60}s`;
|
|
603
|
+
} else if (minutes > 0) {
|
|
604
|
+
return `${minutes}m ${seconds % 60}s`;
|
|
605
|
+
} else {
|
|
606
|
+
return `${seconds}s`;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
export { WatchProvider };
|