Moonshark/watchers/watcher.go

237 lines
4.7 KiB
Go

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...))
}