return nil
}
+
+// writeFileDurable replaces path only after syncing its private temporary file.
+// It syncs the directory after rename before it reports success. An error after
+// rename has an uncertain outcome, so callers must stop writes until recovery.
+func writeFileDurable(path string, data []byte) (err error) {
+ file, err := os.CreateTemp(filepath.Dir(path), "."+filepath.Base(path)+".*")
+ if err != nil {
+ return fmt.Errorf("create durable temporary file: %w", err)
+ }
+
+ defer func() {
+ removeErr := os.Remove(file.Name())
+ if removeErr != nil && !errors.Is(removeErr, os.ErrNotExist) {
+ err = errors.Join(err, fmt.Errorf("remove durable temporary file: %w", removeErr))
+ }
+ }()
+
+ _, err = file.Write(data)
+ if err == nil {
+ err = file.Sync()
+ }
+
+ err = errors.Join(err, file.Close())
+ if err != nil {
+ return fmt.Errorf("write durable temporary file: %w", err)
+ }
+
+ err = os.Rename(file.Name(), path)
+ if err != nil {
+ return fmt.Errorf("publish durable file: %w", err)
+ }
+
+ directory, err := os.Open(filepath.Dir(path))
+ if err != nil {
+ return fmt.Errorf("open durable directory: %w", err)
+ }
+
+ err = errors.Join(directory.Sync(), directory.Close())
+ if err != nil {
+ return fmt.Errorf("sync durable directory: %w", err)
+ }
+
+ return nil
+}
--- /dev/null
+package main
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestDurableReplacementKeepsPrivateCompleteContents(t *testing.T) {
+ path := filepath.Join(t.TempDir(), "state.json")
+ require.NoError(t, writeFileDurable(path, []byte("first\n")))
+ require.NoError(t, writeFileDurable(path, []byte("second\n")))
+
+ data, err := os.ReadFile(path)
+ require.NoError(t, err)
+ require.Equal(t, "second\n", string(data))
+
+ info, err := os.Stat(path)
+ require.NoError(t, err)
+ require.Equal(t, os.FileMode(0o600), info.Mode().Perm())
+
+ entries, err := os.ReadDir(filepath.Dir(path))
+ require.NoError(t, err)
+ require.Len(t, entries, 1)
+}
+
+func TestDurableReplacementReportsFailuresAndRemovesTemporaryFiles(t *testing.T) {
+ directory := t.TempDir()
+ path := filepath.Join(directory, "state.json")
+ require.NoError(t, os.Mkdir(path, 0o700))
+ require.ErrorContains(t, writeFileDurable(path, []byte("data")), "publish durable file")
+
+ entries, err := os.ReadDir(directory)
+ require.NoError(t, err)
+ require.Len(t, entries, 1)
+ require.True(t, entries[0].IsDir())
+ require.ErrorContains(t, writeFileDurable(filepath.Join(directory, "missing", "state"), nil), "create durable temporary file")
+}