Files
u-desk/internal/filewatch/watcher.go
T
lxy e7a2c5148e 新增: 文件操作增强与预览扩展
- 文件剪贴板复制剪切粘贴,重名自动副本
- SFTP下载到本机与外部文件拖入,目录递归
- 传输进度浮动面板,transfer-progress事件链
- Drawio预览,Excel工作线程,冻结看门狗
- cmd与bat自写batch语法高亮,psm1等映射补齐
2026-09-15 22:26:12 +08:00

143 lines
2.7 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() {
// 文件已不存在则跳过(如被删除)
info, err := os.Stat(target)
if err != nil {
return
}
if w.emitEvent != nil {
w.emitEvent("file-changed", map[string]any{
"path": target,
"mtime": info.ModTime().UnixMilli(),
})
}
})
case _, ok := <-w.watcher.Errors:
if !ok {
return
}
}
}
}
// Close 释放资源
func (w *Watcher) Close() {
w.UnwatchFile()
}