aboutsummaryrefslogtreecommitdiff
path: root/internal/persistence/atomic.go
blob: 2b24391c507b43c575139e94789b58d966300e46 (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
54
55
56
57
58
59
60
61
// Package persistence provides crash-resistant single-file replacement.
// It does not serialize callers or make multi-file operations transactional.
package persistence

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

// ErrDurabilityUncertain reports that the atomic replacement was renamed into
// place but the directory could not be synced (or a post-rename chmod
// failed). The new content is visible to readers, but a crash may still lose
// it. Callers that must distinguish "nothing happened" from "the replacement
// landed but may not survive a crash" should test errors.Is against this.
var ErrDurabilityUncertain = errors.New("replacement completed; durability uncertain")

// 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("%w: open directory for sync: %v", ErrDurabilityUncertain, err)
	}
	defer d.Close()
	if err := d.Sync(); err != nil {
		return fmt.Errorf("%w: sync directory: %v", ErrDurabilityUncertain, err)
	}
	return nil
}