215 lines
5.1 KiB
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...))
|
|
}
|