]> git.giacomolli.uk - onechat/blobdiff - file.go
Describe queued event delivery boundaries
[onechat] / file.go
diff --git a/file.go b/file.go
index a7ba5bbdd78b6ec54d1ff6ab599dec2eb78212e7..00c8a0cb9509881e5a4f632d8c9f16c943269ea5 100644 (file)
--- a/file.go
+++ b/file.go
@@ -28,3 +28,47 @@ func writeFileAtomic(path string, data []byte) error {
 
        return nil
 }
 
        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
+}