forked from sneak/util
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
57 lines
1.7 KiB
Go
57 lines
1.7 KiB
Go
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)
|
|
}
|