aboutsummaryrefslogtreecommitdiff
path: root/internal/persistence/atomic.go
blob: a3cc06aebceb9778fb3d4f71ae39c894e70170fb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// Package persistence provides crash-resistant single-file replacement.
// It does not serialize callers or make multi-file operations transactional.
package persistence

import (
	"fmt"
	"os"
	"path/filepath"
)

// WriteFile writes and syncs a temporary file in the destination directory,
// atomically renames it over path, then syncs the directory. Existing file
// permissions are preserved; perm is used for new files. Callers must own the
// data being serialized and coordinate competing read-modify-write operations.
// A directory-sync error means replacement occurred but durability is uncertain.
func WriteFile(path string, data []byte, perm os.FileMode) error {
	if info, err := os.Stat(path); err == nil {
		perm = info.Mode().Perm()
	} else if !os.IsNotExist(err) {
		return err
	}
	dir := filepath.Dir(path)
	f, err := os.CreateTemp(dir, ".save-*")
	if err != nil {
		return err
	}
	defer os.Remove(f.Name())
	defer f.Close()
	if _, err := f.Write(data); err != nil {
		return err
	}
	if err := f.Chmod(perm); err != nil {
		return err
	}
	if err := f.Sync(); err != nil {
		return err
	}
	if err := f.Close(); err != nil {
		return err
	}
	if err := os.Rename(f.Name(), path); err != nil {
		return err
	}
	d, err := os.Open(dir)
	if err != nil {
		return fmt.Errorf("replacement completed; open directory for sync: %w", err)
	}
	defer d.Close()
	if err := d.Sync(); err != nil {
		return fmt.Errorf("replacement completed; sync directory: %w", err)
	}
	return nil
}