Files
prx/internal/fsx/fsx.go
T

76 lines
2.1 KiB
Go
Raw Permalink Normal View History

// SPDX-License-Identifier: MIT
// Package fsx provides small filesystem helpers with atomic, durable writes.
//
// prx persists configuration and runtime state that must never be observed in a
// partially written form, so every write goes through WriteFileAtomic.
package fsx
import (
"fmt"
"os"
"path/filepath"
)
// WriteFileAtomic writes data to path atomically and durably.
//
// It creates any missing parent directories, writes to a temporary file in the
// destination directory, flushes it to stable storage, renames it over path and
// finally flushes the directory. Concurrent readers therefore either see the
// previous contents or the complete new contents, never a partial file.
func WriteFileAtomic(path string, data []byte, perm os.FileMode) error {
dir := filepath.Dir(path)
if err := os.MkdirAll(dir, 0o755); err != nil {
return fmt.Errorf("create %s: %w", dir, err)
}
tmp, err := os.CreateTemp(dir, ".prx-*.tmp")
if err != nil {
return fmt.Errorf("create temporary file in %s: %w", dir, err)
}
tmpName := tmp.Name()
// Remove the temporary file unless it has been renamed into place.
committed := false
defer func() {
if !committed {
_ = os.Remove(tmpName)
}
}()
if _, err := tmp.Write(data); err != nil {
_ = tmp.Close()
return fmt.Errorf("write %s: %w", tmpName, err)
}
if err := tmp.Chmod(perm); err != nil {
_ = tmp.Close()
return fmt.Errorf("chmod %s: %w", tmpName, err)
}
if err := tmp.Sync(); err != nil {
_ = tmp.Close()
return fmt.Errorf("sync %s: %w", tmpName, err)
}
if err := tmp.Close(); err != nil {
return fmt.Errorf("close %s: %w", tmpName, err)
}
if err := os.Rename(tmpName, path); err != nil {
return fmt.Errorf("replace %s: %w", path, err)
}
committed = true
// Flush the directory so the rename itself survives a crash. Failure to
// sync a directory is tolerated because some filesystems do not support it.
syncDir(dir)
return nil
}
// syncDir flushes directory metadata to stable storage, ignoring filesystems
// that do not support directory fsync.
func syncDir(dir string) {
f, err := os.Open(dir)
if err != nil {
return
}
defer f.Close()
_ = f.Sync()
}