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) }