Moonshark/core/watchers/Dir.go

215 lines
5.1 KiB
Go

package watchers
import (
"fmt"
"os"
"path/filepath"
"sync"
"time"
"Moonshark/core/utils/logger"
)
// Default debounce time between detected change and callback
const defaultDebounceTime = 300 * time.Millisecond
// FileInfo stores metadata about a file for change detection
type FileInfo struct {
ModTime time.Time
Size int64
IsDir bool
}
// 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
// Configuration
callback func() error
debounceTime time.Duration
recursive bool
// Debounce timer
debounceTimer *time.Timer
debouncing bool
debounceMu sync.Mutex
}
// 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)
}
// NewDirectoryWatcher creates a new directory watcher
func NewDirectoryWatcher(config DirectoryWatcherConfig) (*DirectoryWatcher, error) {
debounceTime := config.DebounceTime
if debounceTime == 0 {
debounceTime = defaultDebounceTime
}
w := &DirectoryWatcher{
dir: config.Dir,
files: make(map[string]FileInfo),
callback: config.Callback,
debounceTime: debounceTime,
recursive: config.Recursive,
}
// Perform initial scan
if err := w.scanDirectory(); err != nil {
return nil, err
}
return w, nil
}
// scanDirectory builds the initial file list
func (w *DirectoryWatcher) scanDirectory() error {
w.filesMu.Lock()
defer w.filesMu.Unlock()
// Clear existing files map
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
}
// Skip subdirectories if not recursive
if !w.recursive && info.IsDir() && path != w.dir {
return filepath.SkipDir
}
w.files[path] = FileInfo{
ModTime: info.ModTime(),
Size: info.Size(),
IsDir: info.IsDir(),
}
return nil
})
}
// checkForChanges detects if any files have been added, modified, or deleted
func (w *DirectoryWatcher) checkForChanges() (bool, error) {
// Lock for reading previous state
w.filesMu.RLock()
prevFileCount := len(w.files)
w.filesMu.RUnlock()
// Track new state and whether changes were detected
newFiles := make(map[string]FileInfo)
changed := false
// Walk the directory to check for changes
err := filepath.Walk(w.dir, func(path string, info os.FileInfo, err error) error {
// Skip errors (file might have been deleted)
if err != nil {
return nil
}
// Skip subdirectories if not recursive
if !w.recursive && info.IsDir() && path != w.dir {
return filepath.SkipDir
}
// Store current file info
currentInfo := FileInfo{
ModTime: info.ModTime(),
Size: info.Size(),
IsDir: info.IsDir(),
}
newFiles[path] = currentInfo
// Check if file is new or modified (only if we haven't already detected a change)
if !changed {
w.filesMu.RLock()
prevInfo, exists := w.files[path]
w.filesMu.RUnlock()
if !exists || currentInfo.ModTime != prevInfo.ModTime || currentInfo.Size != prevInfo.Size {
changed = true
w.logDebug("File changed: %s", path)
}
}
return nil
})
if err != nil {
return false, err
}
// Check for deleted files (only if we haven't already detected a change)
if !changed && len(newFiles) != prevFileCount {
w.filesMu.RLock()
for path := range w.files {
if _, exists := newFiles[path]; !exists {
changed = true
w.logDebug("File deleted: %s", path)
break
}
}
w.filesMu.RUnlock()
}
// Update files map if changed
if changed {
w.filesMu.Lock()
w.files = newFiles
w.filesMu.Unlock()
}
return changed, nil
}
// notifyChange triggers the callback with debouncing
func (w *DirectoryWatcher) notifyChange() {
w.debounceMu.Lock()
defer w.debounceMu.Unlock()
// Reset timer if already debouncing
if w.debouncing {
if w.debounceTimer != nil {
w.debounceTimer.Stop()
}
} else {
w.debouncing = true
}
w.debounceTimer = time.AfterFunc(w.debounceTime, func() {
if err := w.callback(); err != nil {
w.logError("Callback error: %v", err)
}
// Reset debouncing state
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.Debug("[Watcher] [%s] %s", w.dir, fmt.Sprintf(format, args...))
}
// logWarning logs a warning message with the watcher's directory prefix
func (w *DirectoryWatcher) logWarning(format string, args ...any) {
logger.Warning("[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.Error("[Watcher] [%s] %s", w.dir, fmt.Sprintf(format, args...))
}