Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion file_handling.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Comment on lines +107 to +108
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Register temp cleanup before preserving times

When os.Chtimes fails after the temp file has already been created (for example on a filesystem that rejects the timestamp or denies utime updates), this new early return happens before the defer os.Remove(tempName) is installed, so Write leaves the random temp file behind in the target directory. That regresses the cleanup guarantee for failures after temp-file creation; move the defer immediately after os.WriteFile succeeds so the new Chtimes failure path is also cleaned up.

Useful? React with 👍 / 👎.

}
// 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).
Expand Down
30 changes: 30 additions & 0 deletions file_handling_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package main

import (
"os"
"path/filepath"
"testing"
"time"
)

// TestNewFile exercises NewFile's path-resolution behavior.
Expand Down Expand Up @@ -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)
}
}