UNPKG

taildir

Version:

A simple directory watcher that recursively monitors file changes using only native Node.js libraries

170 lines (141 loc) 5.02 kB
const fs = require('fs'); const path = require('path'); class DirectoryWatcher { constructor(directory, options = {}) { this.directory = path.resolve(directory); this.watchers = new Map(); this.debounceTimers = new Map(); this.fileSizes = new Map(); this.extensions = options.extensions || []; } watch() { console.log(`Starting to watch: ${this.directory}`); this.watchDirectory(this.directory); } watchDirectory(dir) { try { const files = fs.readdirSync(dir, { withFileTypes: true }); files.forEach(file => { const fullPath = path.join(dir, file.name); if (file.isDirectory()) { this.watchDirectory(fullPath); } else if (!this.shouldIgnoreFile(fullPath)) { const stats = fs.statSync(fullPath); this.fileSizes.set(fullPath, stats.size); } }); if (!this.watchers.has(dir)) { const watcher = fs.watch(dir, { recursive: false }, (eventType, filename) => { if (filename) { const filePath = path.join(dir, filename); this.handleFileChange(eventType, filePath); } }); watcher.on('error', (error) => { console.error(`Error watching ${dir}:`, error.message); this.watchers.delete(dir); }); this.watchers.set(dir, watcher); } } catch (error) { console.error(`Error accessing directory ${dir}:`, error.message); } } shouldIgnoreFile(filePath) { const basename = path.basename(filePath); // Ignore hidden files and common temporary files if (basename.startsWith('.')) return true; if (basename.endsWith('~')) return true; if (basename.endsWith('.tmp')) return true; // If extensions filter is set, only watch files with those extensions if (this.extensions.length > 0) { const ext = path.extname(filePath).toLowerCase().slice(1); // Remove the dot if (!this.extensions.includes(ext)) return true; } return false; } handleFileChange(eventType, filePath) { if (this.shouldIgnoreFile(filePath)) return; const key = `${eventType}-${filePath}`; if (this.debounceTimers.has(key)) { clearTimeout(this.debounceTimers.get(key)); } const timer = setTimeout(() => { this.debounceTimers.delete(key); fs.stat(filePath, (err, stats) => { const timestamp = new Date().toISOString(); if (err) { if (err.code === 'ENOENT') { this.fileSizes.delete(filePath); if (this.watchers.has(filePath)) { this.watchers.get(filePath).close(); this.watchers.delete(filePath); } } } else { if (stats.isDirectory()) { if (eventType === 'rename' && !this.watchers.has(filePath)) { this.watchDirectory(filePath); } } else { if (eventType === 'rename') { // New file created this.fileSizes.set(filePath, stats.size); if (stats.size > 0) { this.outputFileContent(filePath, 0, stats.size); } else { // Empty file created, just show the name console.log(`\n==> ${filePath} <==`); } } else { const previousSize = this.fileSizes.get(filePath); const currentSize = stats.size; if (previousSize === undefined) { // First time seeing this file, just track its size this.fileSizes.set(filePath, currentSize); } else if (currentSize > previousSize) { this.outputFileContent(filePath, previousSize, currentSize); this.fileSizes.set(filePath, currentSize); } else if (currentSize < previousSize) { this.fileSizes.set(filePath, currentSize); } } } } }); }, 100); this.debounceTimers.set(key, timer); } outputFileContent(filePath, startPosition, endPosition) { const stream = fs.createReadStream(filePath, { start: startPosition, end: endPosition - 1 }); let content = ''; stream.on('data', (chunk) => { content += chunk.toString(); }); stream.on('end', () => { const trimmedContent = content.trim(); if (trimmedContent) { console.log(`\n==> ${filePath} <==`); console.log(trimmedContent); } }); stream.on('error', (error) => { console.error(`Error reading file ${filePath}: ${error.message}`); }); } stop() { for (const [path, watcher] of this.watchers) { watcher.close(); } for (const timer of this.debounceTimers.values()) { clearTimeout(timer); } this.watchers.clear(); this.debounceTimers.clear(); this.fileSizes.clear(); } } module.exports = DirectoryWatcher;