package watchers import ( "fmt" "os" "path/filepath" "sync" "time" "Moonshark/logger" ) // Default debounce time between detected change and callback const defaultDebounceTime = 300 * time.Millisecond // FileChange represents a detected file change type FileChange struct { Path string IsNew bool IsDeleted bool } // FileInfo stores minimal metadata about a file for change detection type FileInfo struct { ModTime time.Time } // DirectoryWatcher watches a specific directory for changes type DirectoryWatcher struct { // Directory to watch dir string // Map of file paths to their metadata files map[string]FileInfo filesMu sync.RWMutex // Track changed files during a check cycle changedFiles []FileChange // Enhanced callback that receives changes (optional) enhancedCallback func([]FileChange) error // Configuration callback func() error debounceTime time.Duration recursive bool // Debounce timer debounceTimer *time.Timer debouncing bool debounceMu sync.Mutex // Error tracking consecutiveErrors int lastError error } // DirectoryWatcherConfig contains configuration for a directory watcher type DirectoryWatcherConfig struct { Dir string // Directory to watch Callback func() error // Callback function to call when changes are detected DebounceTime time.Duration // Debounce time (0 means use default) Recursive bool // Recursive watching (watch subdirectories) EnhancedCallback func([]FileChange) error // Enhanced callback that receives file changes } // scanDirectory builds the initial file list func (w *DirectoryWatcher) scanDirectory() error { w.filesMu.Lock() defer w.filesMu.Unlock() w.files = make(map[string]FileInfo) return filepath.Walk(w.dir, func(path string, info os.FileInfo, err error) error { if err != nil { return nil // Skip files with errors } if !w.recursive && info.IsDir() && path != w.dir { return filepath.SkipDir } w.files[path] = FileInfo{ ModTime: info.ModTime(), } return nil }) } // checkForChanges detects if any files have been added, modified, or deleted func (w *DirectoryWatcher) checkForChanges() (bool, error) { w.filesMu.RLock() prevFileCount := len(w.files) w.filesMu.RUnlock() newFiles := make(map[string]FileInfo) changed := false w.changedFiles = nil // Reset changed files list err := filepath.Walk(w.dir, func(path string, info os.FileInfo, err error) error { // Skip errors if err != nil { return nil } if !w.recursive && info.IsDir() && path != w.dir { return filepath.SkipDir } currentInfo := FileInfo{ ModTime: info.ModTime(), } newFiles[path] = currentInfo w.filesMu.RLock() prevInfo, exists := w.files[path] w.filesMu.RUnlock() if !exists { changed = true w.changedFiles = append(w.changedFiles, FileChange{ Path: path, IsNew: true, }) w.logDebug("File added: %s", path) } else if currentInfo.ModTime != prevInfo.ModTime { changed = true w.changedFiles = append(w.changedFiles, FileChange{ Path: path, }) w.logDebug("File changed: %s", path) } return nil }) // Only check for deleted files if needed if err == nil && (!changed && len(newFiles) != prevFileCount) { w.filesMu.RLock() for path := range w.files { if _, exists := newFiles[path]; !exists { changed = true w.changedFiles = append(w.changedFiles, FileChange{ Path: path, IsDeleted: true, }) w.logDebug("File deleted: %s", path) break // We already know changes happened } } w.filesMu.RUnlock() } if changed { w.filesMu.Lock() w.files = newFiles w.filesMu.Unlock() } return changed, err } // notifyChange triggers the callback with debouncing func (w *DirectoryWatcher) notifyChange() { w.debounceMu.Lock() defer w.debounceMu.Unlock() if w.debouncing { if w.debounceTimer != nil { w.debounceTimer.Stop() } } else { w.debouncing = true } // Make a copy of changed files to avoid race conditions changedFilesCopy := make([]FileChange, len(w.changedFiles)) copy(changedFilesCopy, w.changedFiles) w.debounceTimer = time.AfterFunc(w.debounceTime, func() { var err error if w.enhancedCallback != nil { err = w.enhancedCallback(changedFilesCopy) } else if w.callback != nil { err = w.callback() } if err != nil { w.logError("Callback error: %v", err) } w.debounceMu.Lock() w.debouncing = false w.debounceMu.Unlock() }) } // logDebug logs a debug message with the watcher's directory prefix func (w *DirectoryWatcher) logDebug(format string, args ...any) { logger.Debugf("[Watcher] [%s] %s", w.dir, fmt.Sprintf(format, args...)) } // logError logs an error message with the watcher's directory prefix func (w *DirectoryWatcher) logError(format string, args ...any) { logger.Errorf("[Watcher] [%s] %s", w.dir, fmt.Sprintf(format, args...)) } // GetDir gets the DirectoryWatcher's current directory func (w *DirectoryWatcher) GetDir() string { return w.dir }