diff --git a/internal/fileutil/fileutil.go b/internal/fileutil/fileutil.go index 39e0e330f..7b4abee0b 100644 --- a/internal/fileutil/fileutil.go +++ b/internal/fileutil/fileutil.go @@ -26,12 +26,17 @@ import ( // AtomicWriteFile atomically (as atomic as os.Rename allows) writes a file to a // disk. -func AtomicWriteFile(filename string, reader io.Reader, mode os.FileMode) error { +func AtomicWriteFile(filename string, reader io.Reader, mode os.FileMode) (err error) { tempFile, err := os.CreateTemp(filepath.Split(filename)) if err != nil { return err } tempName := tempFile.Name() + defer func() { + if err != nil { + _ = os.Remove(tempName) + } + }() if _, err := io.Copy(tempFile, reader); err != nil { tempFile.Close() // return value is ignored as we are already on error path diff --git a/internal/fileutil/fileutil_test.go b/internal/fileutil/fileutil_test.go index 71fcae177..0ad28e439 100644 --- a/internal/fileutil/fileutil_test.go +++ b/internal/fileutil/fileutil_test.go @@ -120,6 +120,30 @@ func TestAtomicWriteFile_LargeContent(t *testing.T) { } } +// TestAtomicWriteFile_CleansUpTempFileOnRenameError verifies that temporary +// files are removed when the final rename step fails. +func TestAtomicWriteFile_CleansUpTempFileOnRenameError(t *testing.T) { + dir := t.TempDir() + target := filepath.Join(dir, "existing-dir") + if err := os.Mkdir(target, 0755); err != nil { + t.Fatal(err) + } + + err := AtomicWriteFile(target, bytes.NewReader([]byte("content")), 0644) + if err == nil { + t.Fatal("expected rename error, got nil") + } + + entries, err := os.ReadDir(dir) + if err != nil { + t.Fatal(err) + } + + if len(entries) != 1 || entries[0].Name() != "existing-dir" { + t.Fatalf("expected only existing-dir to remain, found %v entries", len(entries)) + } +} + // TestPlatformAtomicWriteFile_OverwritesExisting verifies that the platform // helper replaces existing files instead of silently skipping them. func TestPlatformAtomicWriteFile_OverwritesExisting(t *testing.T) {