From 4ea06ee70be3ed76e302b899e41dfb72a43770de Mon Sep 17 00:00:00 2001 From: wuyangfan <1102042793@qq.com> Date: Sun, 31 May 2026 15:38:49 +0800 Subject: [PATCH] fix: preserve file mtime after atomic rewrite Call os.Chtimes on the temp file before rename so Make-style dependency tracking and file watchers see a stable modification time. Fixes #23 --- file_handling.go | 7 ++++++- file_handling_test.go | 30 ++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/file_handling.go b/file_handling.go index 7c0b1a7..9a28818 100644 --- a/file_handling.go +++ b/file_handling.go @@ -93,15 +93,20 @@ func (f *File) Read() (string, error) { // step after its creation fails (including the rename); on success the remove // is a no-op because the file has already been renamed away. func (f *File) Write(content string) error { - mode, err := f.Mode() + info, err := f.Info() if err != nil { return err } + mode := info.Mode() + modTime := info.ModTime() tempName := filepath.Join(f.Dir(), RandomString(20)) if err := os.WriteFile(tempName, []byte(content), mode); err != nil { return fmt.Errorf("create tempfile in %v: %w", f.Dir(), err) } + if err := os.Chtimes(tempName, modTime, modTime); err != nil { + return fmt.Errorf("preserve mtime on temp file %v: %w", tempName, err) + } // Make sure the temp file is removed if the rename below fails. On // success, the rename has already moved the file to f.Path so this is // a no-op (we deliberately ignore the not-exist error). diff --git a/file_handling_test.go b/file_handling_test.go index 91ee538..d956d74 100644 --- a/file_handling_test.go +++ b/file_handling_test.go @@ -1,8 +1,10 @@ package main import ( + "os" "path/filepath" "testing" + "time" ) // TestNewFile exercises NewFile's path-resolution behavior. @@ -77,3 +79,31 @@ func TestNewFile(t *testing.T) { }) } } + +func TestFileWritePreservesModTime(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "mtime.txt") + if err := os.WriteFile(path, []byte("before"), 0o644); err != nil { + t.Fatal(err) + } + want := time.Date(2019, 3, 14, 15, 9, 26, 0, time.UTC) + if err := os.Chtimes(path, want, want); err != nil { + t.Fatal(err) + } + + f, err := NewFile(path) + if err != nil { + t.Fatal(err) + } + if err := f.Write("after"); err != nil { + t.Fatal(err) + } + + got, err := os.Stat(path) + if err != nil { + t.Fatal(err) + } + if !got.ModTime().Equal(want) { + t.Fatalf("ModTime = %v; want %v", got.ModTime(), want) + } +}