package filewatch import ( "fmt" "os" "path/filepath" "strings" "sync" "time" "github.com/fsnotify/fsnotify" ) type Watcher struct { watcher *fsnotify.Watcher emitEvent func(name string, data ...any) mu sync.Mutex watched string // 当前监听的文件绝对路径 } func NewWatcher(emitEvent func(name string, data ...any)) *Watcher { return &Watcher{emitEvent: emitEvent} } // WatchFile 开始监听指定文件的变化。切换文件时自动取消旧监听。 func (w *Watcher) WatchFile(path string) error { w.mu.Lock() defer w.mu.Unlock() absPath, err := filepath.Abs(path) if err != nil { return fmt.Errorf("解析路径失败: %w", err) } // 同一文件不重复监听 if absPath == w.watched { return nil } // 停止旧监听 w.stopLocked() // 检查文件是否存在 if _, err := os.Stat(absPath); err != nil { return fmt.Errorf("文件不存在: %s", absPath) } // 创建 fsnotify watcher fw, err := fsnotify.NewWatcher() if err != nil { return fmt.Errorf("创建文件监听器失败: %w", err) } w.watcher = fw // fsnotify 在某些系统上不支持直接监听文件,监听其所在目录 dir := filepath.Dir(absPath) if err := fw.Add(dir); err != nil { fw.Close() return fmt.Errorf("监听目录失败: %w", err) } w.watched = absPath // 后台消费事件 go w.consumeEvents() return nil } // UnwatchFile 停止监听 func (w *Watcher) UnwatchFile() { w.mu.Lock() defer w.mu.Unlock() w.stopLocked() } func (w *Watcher) stopLocked() { if w.watcher != nil { w.watcher.Close() w.watcher = nil } w.watched = "" } func (w *Watcher) consumeEvents() { debounceDelay := 300 * time.Millisecond var debounceTimer *time.Timer for { select { case event, ok := <-w.watcher.Events: if !ok { return } // 只处理目标文件的 Write/Create/Rename 事件 if event.Op&(fsnotify.Write|fsnotify.Create|fsnotify.Rename) == 0 { continue } w.mu.Lock() target := w.watched w.mu.Unlock() if target == "" { return // 已停止监听 } // 路径比较(忽略大小写,Windows) if !strings.EqualFold(event.Name, target) { continue } // 防抖 if debounceTimer != nil { debounceTimer.Stop() } debounceTimer = time.AfterFunc(debounceDelay, func() { // 文件已不存在则跳过(如被删除) if _, err := os.Stat(target); err != nil { return } if w.emitEvent != nil { w.emitEvent("file-changed", target) } }) case _, ok := <-w.watcher.Errors: if !ok { return } } } } // Close 释放资源 func (w *Watcher) Close() { w.UnwatchFile() }