-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpublish.go
More file actions
120 lines (111 loc) · 4.84 KB
/
Copy pathpublish.go
File metadata and controls
120 lines (111 loc) · 4.84 KB
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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
package main
import (
"fmt"
"os"
"path/filepath"
"syscall"
)
// publish moves the result into place.
//
// A destination that does not exist is one atomic rename and nothing to clean
// up.
//
// Replacing one that DOES exist cannot be a single rename in the general case.
// rename(2) refuses a non-empty directory as its target, and refuses to replace
// a file with a directory (or the reverse) at all - so deleting the existing
// entry first would be the only alternative, and a crash in that window loses
// the destination outright while the replacement sits under a staging name
// nobody recognises.
//
// Moving the old entry aside first avoids the window whatever the two types
// are. Both renames are atomic, so the worst a crash leaves is the old data
// under .flux-old-<id>, which the startup sweep renames back when it finds the
// destination missing. Uniform rather than branching on type: the branch is
// where the file-replaced-by-directory case was originally missed.
//
// Renaming directly, rather than through mv, means a publish that would cross a
// filesystem boundary fails instead of silently becoming a copy. Staging and
// destination are both inside the volume by construction, so that cannot happen
// without something else already being wrong - and a non-atomic publish is
// exactly what the caller was promised would not occur.
func publish(staging, destination, root, id string) error {
// Which object is about to be published, read before anything moves. A
// sweep that finds a destination occupied cannot otherwise tell what is
// sitting there: the object this publish placed, or one the app owner put
// at that path themselves while the destination stood empty. Recording the
// answer costs one lstat and replaces a guess with a comparison.
//
// This also means a staging path that is not there fails before the
// destination is displaced rather than after, which leaves nothing to sweep.
staged, err := os.Lstat(staging)
if err != nil {
return err
}
stagedIdentity, err := identity(staged)
if err != nil {
return err
}
// Lstat, not Stat: a dangling symlink at the destination is an entry that
// has to be moved aside, and a check that followed it would treat the
// destination as empty and rename over the link.
if _, err := os.Lstat(destination); err != nil {
if !os.IsNotExist(err) {
return err
}
return os.Rename(staging, destination)
}
old := filepath.Join(root, swapPrefix+id)
marker := old + markerSuffix
// Where the data being moved aside belongs, written BEFORE it is moved. A
// crash between the two renames below leaves the caller's previous data
// under `old` with its own path empty, and without this the sweep has no way
// to know where to put it back - it would delete the only copy.
record := markerContents(destination, root) + "\n" + stagedIdentity + "\n"
if err := os.WriteFile(marker, []byte(record), 0o644); err != nil {
return fmt.Errorf("could not record where %s belongs: %w", destination, err)
}
if err := os.Rename(destination, old); err != nil {
return err
}
if err := os.Rename(staging, destination); err != nil {
return err
}
// Best effort from here. The publish has happened and the caller has what
// they asked for; a leftover swap directory is something the startup sweep
// reclaims, and failing the operation over it would report a success as a
// failure.
os.RemoveAll(old)
os.Remove(marker)
return nil
}
const (
// Prefix of the directory an interrupted publish leaves the previous data
// under, and the suffix of the file recording where it belongs. FluxOS
// matches both exactly, against a real identifier shape, because the sweep
// DELETES what it matches in a directory the app owner can also write to.
swapPrefix = ".flux-old-"
markerSuffix = ".dest"
)
// identity is what a sweep compares to decide whether a publish finished: the
// inode number of the object being placed, and its modification time in
// nanoseconds.
//
// Both survive rename, which is what makes them usable at all - ctime does not,
// since rename updates it, and a recorded ctime would mismatch the moment the
// publish succeeded.
//
// The inode number alone is not enough. Filesystems reuse them, so an entry the
// app owner creates at the destination after a publish can carry the number
// recorded here and be taken for the published object. An mtime to the
// nanosecond does not collide by accident.
//
// It does not have to resist being forged. The app owner can read this file
// through the file browser and can set an mtime, but a marker they match only
// makes the sweep delete the data it was holding for them - their own.
func identity(fi os.FileInfo) (string, error) {
stat, ok := fi.Sys().(*syscall.Stat_t)
if !ok {
return "", fmt.Errorf("cannot read the identity of %s on this platform", fi.Name())
}
return fmt.Sprintf("%d %d", stat.Ino, fi.ModTime().UnixNano()), nil
}