237 lines
4.7 KiB
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...))
|
|
}
|