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