package watchers import ( "fmt" "os" "path/filepath" "sync" "time" "Moonshark/logger" ) const ( defaultDebounceTime = 300 * time.Millisecond defaultPollInterval = 1 * time.Second ) type FileChange struct { Path string IsNew bool IsDeleted bool } type FileInfo struct { ModTime time.Time } // Watcher is now self-contained and manages its own polling type Watcher struct { dir string files map[string]FileInfo filesMu sync.RWMutex callback func([]FileChange) error debounceTime time.Duration pollInterval time.Duration recursive bool // Self-management done chan struct{} debounceTimer *time.Timer debouncing bool debounceMu sync.Mutex wg sync.WaitGroup } type WatcherConfig struct { Dir string Callback func([]FileChange) error DebounceTime time.Duration PollInterval time.Duration Recursive bool } func NewWatcher(config WatcherConfig) (*Watcher, error) { if config.DebounceTime == 0 { config.DebounceTime = defaultDebounceTime } if config.PollInterval == 0 { config.PollInterval = defaultPollInterval } w := &Watcher{ dir: config.Dir, files: make(map[string]FileInfo), callback: config.Callback, debounceTime: config.DebounceTime, pollInterval: config.PollInterval, recursive: config.Recursive, done: make(chan struct{}), } if err := w.scanDirectory(); err != nil { return nil, err } w.wg.Add(1) go w.watchLoop() return w, nil } func (w *Watcher) Close() { close(w.done) w.wg.Wait() w.debounceMu.Lock() if w.debounceTimer != nil { w.debounceTimer.Stop() } w.debounceMu.Unlock() } func (w *Watcher) GetDir() string { return w.dir } // watchLoop is the main polling loop for this watcher func (w *Watcher) watchLoop() { defer w.wg.Done() ticker := time.NewTicker(w.pollInterval) defer ticker.Stop() for { select { case <-ticker.C: w.checkAndNotify() case <-w.done: return } } } // checkAndNotify combines change detection and notification func (w *Watcher) checkAndNotify() { changed, changedFiles := w.detectChanges() if changed { w.notifyChange(changedFiles) } } // detectChanges scans directory and returns changes func (w *Watcher) detectChanges() (bool, []FileChange) { newFiles := make(map[string]FileInfo) var changedFiles []FileChange changed := false err := filepath.Walk(w.dir, func(path string, info os.FileInfo, err error) error { 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 changedFiles = append(changedFiles, FileChange{Path: path, IsNew: true}) w.logDebug("File added: %s", path) } else if currentInfo.ModTime != prevInfo.ModTime { changed = true changedFiles = append(changedFiles, FileChange{Path: path}) w.logDebug("File changed: %s", path) } return nil }) if err != nil { w.logError("Error scanning directory: %v", err) return false, nil } // Check for deletions w.filesMu.RLock() for path := range w.files { if _, exists := newFiles[path]; !exists { changed = true changedFiles = append(changedFiles, FileChange{Path: path, IsDeleted: true}) w.logDebug("File deleted: %s", path) } } w.filesMu.RUnlock() if changed { w.filesMu.Lock() w.files = newFiles w.filesMu.Unlock() } return changed, changedFiles } func (w *Watcher) 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 } if !w.recursive && info.IsDir() && path != w.dir { return filepath.SkipDir } w.files[path] = FileInfo{ModTime: info.ModTime()} return nil }) } func (w *Watcher) notifyChange(changedFiles []FileChange) { w.debounceMu.Lock() defer w.debounceMu.Unlock() if w.debouncing && w.debounceTimer != nil { w.debounceTimer.Stop() } w.debouncing = true // Copy to avoid race conditions filesCopy := make([]FileChange, len(changedFiles)) copy(filesCopy, changedFiles) w.debounceTimer = time.AfterFunc(w.debounceTime, func() { var err error if w.callback != nil { err = w.callback(filesCopy) } if err != nil { w.logError("Callback error: %v", err) } w.debounceMu.Lock() w.debouncing = false w.debounceMu.Unlock() }) } func (w *Watcher) logDebug(format string, args ...any) { logger.Debugf("[Watcher] [%s] %s", w.dir, fmt.Sprintf(format, args...)) } func (w *Watcher) logError(format string, args ...any) { logger.Errorf("[Watcher] [%s] %s", w.dir, fmt.Sprintf(format, args...)) }