add AtomicWriteFile for replacing a file in one step

AtomicWriteFile writes to a temporary file in the same directory, flushes it,
sets the mode and renames it over the target, so a reader never sees a
half-written file and a failure leaves the old one untouched. Comes with a doc
comment and table-driven tests. (closes #17)

Model: opus-5
This commit is contained in:
2026-09-05 03:30:52 +00:00
parent 9302c14a6c
commit 9708301eba
2 changed files with 166 additions and 0 deletions

56
atomicwrite.go Normal file
View File

@@ -0,0 +1,56 @@
package util
import (
"os"
"path/filepath"
)
// AtomicWriteFile writes data to path without ever leaving a half-written file
// behind. It writes to a temporary file in the same directory, flushes it to
// disk, gives it the mode in perm and renames it over path, so anything reading
// path sees either the previous contents or the complete new contents and
// never something in between. If any step fails, the temporary file is removed
// and path is left as it was.
//
// The temporary file has to live in the same directory as path, because a
// rename across two filesystems is not possible and would not be instant if it
// were.
//
// Two details differ from os.WriteFile. The mode is applied after the file is
// created, so perm is what the finished file ends up with rather than perm with
// the process umask taken out of it. And the directory holding the file is not
// flushed, so a machine that loses power immediately after this returns may
// come back with the rename undone, even though the data itself was written.
func AtomicWriteFile(path string, data []byte, perm os.FileMode) error {
directory := filepath.Dir(path)
temporary, err := os.CreateTemp(directory, "."+filepath.Base(path)+".tmp")
if err != nil {
return err
}
temporaryPath := temporary.Name()
// Does nothing once the rename below has succeeded, because by then the
// temporary name no longer refers to anything.
defer os.Remove(temporaryPath)
if _, err := temporary.Write(data); err != nil {
temporary.Close()
return err
}
if err := temporary.Sync(); err != nil {
temporary.Close()
return err
}
if err := temporary.Close(); err != nil {
return err
}
if err := os.Chmod(temporaryPath, perm); err != nil {
return err
}
return os.Rename(temporaryPath, path)
}