From 801582f27ab6e90adf8572716f47f17d743b4877 Mon Sep 17 00:00:00 2001 From: Amir Husayn Panahifar Date: Thu, 15 Oct 2026 14:29:19 +0330 Subject: [PATCH] feat(fsx): add atomic and durable file writes --- internal/fsx/fsx.go | 75 ++++++++++++++++++++++++++++++++++++++++ internal/fsx/fsx_test.go | 55 +++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+) create mode 100644 internal/fsx/fsx.go create mode 100644 internal/fsx/fsx_test.go diff --git a/internal/fsx/fsx.go b/internal/fsx/fsx.go new file mode 100644 index 0000000..1727f4f --- /dev/null +++ b/internal/fsx/fsx.go @@ -0,0 +1,75 @@ +// 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() +} diff --git a/internal/fsx/fsx_test.go b/internal/fsx/fsx_test.go new file mode 100644 index 0000000..8cbea37 --- /dev/null +++ b/internal/fsx/fsx_test.go @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MIT + +package fsx + +import ( + "os" + "path/filepath" + "testing" +) + +func TestWriteFileAtomicCreatesAndReplaces(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "nested", "file.json") + + if err := WriteFileAtomic(path, []byte("first"), 0o600); err != nil { + t.Fatalf("WriteFileAtomic: %v", err) + } + got, err := os.ReadFile(path) + if err != nil { + t.Fatalf("ReadFile: %v", err) + } + if string(got) != "first" { + t.Fatalf("content = %q, want %q", got, "first") + } + info, err := os.Stat(path) + if err != nil { + t.Fatalf("Stat: %v", err) + } + if info.Mode().Perm() != 0o600 { + t.Fatalf("perm = %o, want 600", info.Mode().Perm()) + } + + if err := WriteFileAtomic(path, []byte("second"), 0o600); err != nil { + t.Fatalf("second WriteFileAtomic: %v", err) + } + got, _ = os.ReadFile(path) + if string(got) != "second" { + t.Fatalf("content = %q, want %q", got, "second") + } +} + +func TestWriteFileAtomicLeavesNoTempFiles(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "state.json") + if err := WriteFileAtomic(path, []byte("x"), 0o644); err != nil { + t.Fatalf("WriteFileAtomic: %v", err) + } + entries, err := os.ReadDir(dir) + if err != nil { + t.Fatalf("ReadDir: %v", err) + } + if len(entries) != 1 || entries[0].Name() != "state.json" { + t.Fatalf("unexpected directory contents: %v", entries) + } +}