diff --git a/internal/watch/snapshot_unix.go b/internal/watch/snapshot_unix.go new file mode 100644 index 0000000..322c2ab --- /dev/null +++ b/internal/watch/snapshot_unix.go @@ -0,0 +1,7 @@ +//go:build !windows + +package watch + +import "os" + +func snapshotInfo(path string) (os.FileInfo, error) { return os.Stat(path) } diff --git a/internal/watch/snapshot_windows.go b/internal/watch/snapshot_windows.go new file mode 100644 index 0000000..9a0a03d --- /dev/null +++ b/internal/watch/snapshot_windows.go @@ -0,0 +1,16 @@ +package watch + +import ( + "errors" + "os" +) + +func snapshotInfo(path string) (os.FileInfo, error) { + file, err := os.Open(path) + if err != nil { + return nil, err + } + // File.Stat captures the file ID from its open handle, before external renames. + info, err := file.Stat() + return info, errors.Join(err, file.Close()) +} diff --git a/internal/watch/watch.go b/internal/watch/watch.go index 9bf954f..8501a34 100644 --- a/internal/watch/watch.go +++ b/internal/watch/watch.go @@ -10,6 +10,7 @@ import ( "os" "path/filepath" "rawmark/internal/i18n" + "runtime" "sort" "strings" "time" @@ -135,8 +136,18 @@ func (w *Watcher) scan() (map[string]fileState, error) { next := make(map[string]fileState, len(entries)) dirs := map[string]bool{w.v.Root(): true} for _, e := range entries { - st := fileState{dir: e.Dir, size: e.Size, mtime: e.Info.ModTime().UnixNano(), info: e.Info} abs := filepath.Join(w.v.Root(), filepath.FromSlash(e.Path)) + info := e.Info + if runtime.GOOS == "windows" { + info, err = snapshotInfo(abs) + if errors.Is(err, fs.ErrNotExist) { + continue + } + if err != nil { + return nil, err + } + } + st := fileState{dir: e.Dir, size: info.Size(), mtime: info.ModTime().UnixNano(), info: info} if e.Dir { dirs[abs] = true } else if vault.IsNote(e.Path) { diff --git a/internal/watch/watch_test.go b/internal/watch/watch_test.go index 49f8dff..977b74d 100644 --- a/internal/watch/watch_test.go +++ b/internal/watch/watch_test.go @@ -42,6 +42,9 @@ func TestUnreadableNotePreservesSnapshot(t *testing.T) { t.Fatal(err) } allowRead := testutil.DenyRead(t, p) + if _, err := os.ReadFile(p); err == nil { + t.Fatal("filesystem failure fixture did not block reading") + } w.rescan() if w.snap["note.md"].rev != old || scans != 0 { t.Fatal("读取失败不能提交不完整快照或更新索引") @@ -69,7 +72,7 @@ func TestDiff(t *testing.T) { } } state := func(p, rev string) fileState { - info, err := os.Stat(filepath.Join(root, p)) + info, err := snapshotInfo(filepath.Join(root, p)) if err != nil { t.Fatal(err) } @@ -125,7 +128,7 @@ func TestDirectoryMovesCoverChildrenAndEmptyDirectories(t *testing.T) { snapshot := func(dir string) map[string]fileState { out := map[string]fileState{} for _, rel := range []string{dir, dir + "/empty", dir + "/a.md"} { - info, err := os.Stat(filepath.Join(root, rel)) + info, err := snapshotInfo(filepath.Join(root, rel)) if err != nil { t.Fatal(err) } @@ -170,7 +173,7 @@ func TestImageReplacementRefreshesTree(t *testing.T) { if err := os.WriteFile(p, []byte("image"), 0o644); err != nil { t.Fatal(err) } - info, err := os.Stat(p) + info, err := snapshotInfo(p) if err != nil { t.Fatal(err) }