Moonshark/core/watchers/dir.go
2025-03-26 11:51:22 -05:00

232 lines
5.4 KiB
Go

package watchers
import (
"fmt"
"os"
"path/filepath"
"sync"
"time"
"git.sharkk.net/Sky/Moonshark/core/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
log *logger.Logger
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 {
// Directory to watch
Dir string
// Callback function to call when changes are detected
Callback func() error
// Logger instance
Log *logger.Logger
// Debounce time (0 means use default)
DebounceTime time.Duration
// Recursive watching (watch subdirectories)
Recursive bool
}
// 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,
log: config.Log,
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()
return filepath.Walk(w.dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
w.logWarning("Error accessing path %s: %v", path, err)
return nil // Continue with other files
}
// Skip if not recursive and this is a subdirectory
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) {
// Get current state
currentFiles := make(map[string]FileInfo)
err := filepath.Walk(w.dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
// File might have been deleted between directory read and stat
return nil
}
// Skip if not recursive and this is a subdirectory
if !w.recursive && info.IsDir() && path != w.dir {
return filepath.SkipDir
}
currentFiles[path] = FileInfo{
ModTime: info.ModTime(),
Size: info.Size(),
IsDir: info.IsDir(),
}
return nil
})
if err != nil {
return false, err
}
// Compare with previous state
w.filesMu.RLock()
previousFiles := w.files
w.filesMu.RUnlock()
// Check for different file count (quick check)
if len(currentFiles) != len(previousFiles) {
w.logDebug("File count changed: %d -> %d", len(previousFiles), len(currentFiles))
w.updateFiles(currentFiles)
return true, nil
}
// Check for modified, added, or removed files
for path, currentInfo := range currentFiles {
prevInfo, exists := previousFiles[path]
if !exists {
// New file
w.logDebug("New file detected: %s", path)
w.updateFiles(currentFiles)
return true, nil
}
if currentInfo.ModTime != prevInfo.ModTime || currentInfo.Size != prevInfo.Size {
// File modified
w.logDebug("File modified: %s", path)
w.updateFiles(currentFiles)
return true, nil
}
}
// Check for deleted files
for path := range previousFiles {
if _, exists := currentFiles[path]; !exists {
// File deleted
w.logDebug("File deleted: %s", path)
w.updateFiles(currentFiles)
return true, nil
}
}
// No changes detected
return false, nil
}
// updateFiles updates the internal file list
func (w *DirectoryWatcher) updateFiles(newFiles map[string]FileInfo) {
w.filesMu.Lock()
w.files = newFiles
w.filesMu.Unlock()
}
// notifyChange triggers the callback with debouncing
func (w *DirectoryWatcher) notifyChange() {
w.debounceMu.Lock()
defer w.debounceMu.Unlock()
if w.debouncing {
// Reset timer if already 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) {
w.log.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) {
w.log.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) {
w.log.Error("[Watcher] [%s] %s", w.dir, fmt.Sprintf(format, args...))
}