139 lines
2.6 KiB
Go
139 lines
2.6 KiB
Go
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()
|
||
}
|