als-watcher
Version:
Watch directory changes with events, file types filters and depth control.
112 lines (95 loc) • 3.78 kB
JavaScript
const { watch, statSync, stat } = require('fs');
const { join } = require('path');
const { fileListSync } = require('als-file-list');
const EventEmitter = require('events');
class DirectoryWatcher {
constructor(directoryPath, extensions = [], depth = Infinity) {
this.directoryPath = directoryPath;
this.extensions = extensions;
this.depth = depth;
this.emitter = new EventEmitter();
this.filesMap = new Map();
this.recentlyDeleted = null;
this.lastFilename = null;
}
init() {
const files = fileListSync(this.directoryPath);
files.forEach(file => {
const filePath = join(this.directoryPath, file);
try {this.filesMap.set(file, statSync(filePath).mtimeMs);}
catch (error) {this.emitError(`Failed to get stats for file: ${filePath}`, error)}
});
}
startWatching() {
try {
watch(this.directoryPath, { recursive: true }, (eventType, filename) => {
if (!this.isValidFile(filename)) return;
this.lastFilename = filename;
const filePath = join(this.directoryPath, filename);
stat(filePath, (err, stats) => {
if (err) {
if (err.code === 'ENOENT') this.handleUnlink(filename);
else this.emitError(`Error accessing file: ${filePath}`, err);
return;
}
if (!stats.isFile()) return;
if (!this.filesMap.has(filename)) this.handleAddOrRename(filename, stats);
else this.handleChange(filename, stats);
});
});
console.log(`Started watching directory: ${this.directoryPath}`);
} catch (error) {
this.emitError(`Failed to watch directory: ${this.directoryPath}`, error);
}
}
isValidFile(filename) {
return filename && (this.extensions.length === 0 || this.extensions.some(ext => filename.endsWith(`.${ext}`))) &&
((filename.match(/\/|\\/g) || []).length <= this.depth);
}
handleAddOrRename(filename, stats) {
if (this.recentlyDeleted) {
this.emitRename(this.recentlyDeleted, filename);
this.recentlyDeleted = null;
} else this.emitAdd(filename);
this.filesMap.set(filename, stats.mtimeMs);
}
handleChange(filename, stats) {
if (stats.mtimeMs > this.filesMap.get(filename)) {
this.emitChange(filename);
this.filesMap.set(filename, stats.mtimeMs);
}
}
handleUnlink(filename) {
if (!this.filesMap.has(filename)) return;
this.recentlyDeleted = filename;
this.filesMap.delete(filename);
this.emitUnlink(filename);
}
emitAdd(filename) {
this.emitter.emit('add', filename);
this.emitter.emit('operation', 'add', filename);
}
emitChange(filename) {
this.emitter.emit('change', filename);
this.emitter.emit('operation', 'change', filename);
}
emitUnlink(filename) {
this.emitter.emit('unlink', filename);
this.emitter.emit('operation', 'unlink', filename);
}
emitRename(oldFilename, newFilename) {
this.emitter.emit('rename', oldFilename, newFilename);
this.emitter.emit('operation', 'rename', oldFilename, newFilename);
}
emitError(message, error) {
error.details = { msg: message, lastFilename: this.lastFilename };
this.emitter.emit('error', error);
}
}
function watchDirectory(directoryPath, extensions = [], depth = Infinity) {
const watcher = new DirectoryWatcher(directoryPath, extensions, depth);
watcher.init();
watcher.startWatching();
return watcher;
}
module.exports = watchDirectory;