carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package hostfiles
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "io"
8 "os"
9 "path"
10 "sort"
11)
12
13type fileState int
14
15const (
16 fileOK fileState = iota
17 fileTorn
18 fileGone
19 fileNotRegular
20 fileTooLarge
21 fileUnreadable
22 fileRetry
23)
24
25// Test seams. betweenLookAndOpen runs after something has been inspected
26// and before it is opened; readAll is the one place file contents are read.
27var (
28 betweenLookAndOpen = func() {}
29 readAll = io.ReadAll
30)
31
32// SyncStats reports one pass. Retry lists regular files replaced between
33// the look and the open: nothing was read and nothing staged for them this
34// pass. Torn lists files staged from a read during which the file changed,
35// so their staged bytes may be an inconsistent image.
36type SyncStats struct {
37 Published, Deleted int
38 TooLarge, NotRegular, Unreadable []string
39 Retry, Torn []string
40}
41
42// skipped reports whether rel, or any directory above it, is excluded.
43func skipped(skip func(string, bool) bool, rel string, isDir bool) bool {
44 if skip == nil {
45 return false
46 }
47 if skip(rel, isDir) {
48 return true
49 }
50 for dir := path.Dir(rel); dir != "."; dir = path.Dir(dir) {
51 if skip(dir, true) {
52 return true
53 }
54 }
55 return false
56}
57
58// SyncDir stages every difference between dir and the manifest: changed
59// and new regular files, and a tombstone for each manifest path that is
60// no longer there. It does not commit. skip, when non-nil, excludes a
61// relative path and, for a directory, everything under it; an excluded
62// path is left exactly as the manifest has it.
63//
64// A tombstone needs proof of absence. A file that exists but could not be
65// read keeps its manifest entry and is reported in Unreadable. A root
66// that cannot be opened is an error and stages nothing: an unreadable
67// directory is not an empty one.
68func SyncDir(ctx context.Context, p *Publisher, dir string, skip func(rel string, isDir bool) bool) (SyncStats, error) {
69 root, err := openTree(dir)
70 if err != nil {
71 return SyncStats{}, fmt.Errorf("hostfiles: sync %s: %w", dir, err)
72 }
73 defer root.Close()
74 return SyncDirAt(ctx, p, root.f, skip)
75}
76
77// MaxPathBytes caps a path in a files tree, relative to its root: PATH_MAX
78// (4096) less its terminating NUL, as moves' archive contract has it. It is
79// the one cap every writer (Stage, StageSkipped, StageDelete, Commit, the
80// watcher's publishOne, a walk) and every reader (checkManifest: Restore,
81// CopyManifest, ValidateManifest) applies, so no manifest is written that
82// a reader refuses. Every path it admits can be opened by one openat2
83// beneath the restored root (Present). It also bounds depth (about 2048
84// components of one byte), and with it a walk's open directories and its
85// path strings.
86const MaxPathBytes = 4095
87
88// ErrTooDeep is a path past MaxPathBytes: a writer refuses to stage it,
89// and the walk or manifest that holds it is refused whole.
90var ErrTooDeep = errors.New("hostfiles: a path is past the tree's length cap")
91
92// withinTreeCap is the one cap the writers and readers share.
93func withinTreeCap(rel string) error {
94 if n := len(rel); n > MaxPathBytes {
95 return fmt.Errorf("%w: %d bytes (cap %d), starting %q", ErrTooDeep, n, MaxPathBytes, rel[:64])
96 }
97 return nil
98}
99
100// SyncDirAt is SyncDir over a directory the caller has already opened, and
101// does not close. SyncDir opens its root by path and follows a link there
102// (the path is the platform's own); a caller syncing ground someone else
103// can write opens the root itself without following anything
104// (instancefs.OpenDirBeneath) and syncs here, so the tree published is the
105// directory it vetted, whatever is renamed into the path afterwards.
106// Beneath the root nothing is followed either way.
107func SyncDirAt(ctx context.Context, p *Publisher, dir *os.File, skip func(rel string, isDir bool) bool) (SyncStats, error) {
108 root, err := handleOf(dir)
109 if err != nil {
110 return SyncStats{}, err
111 }
112 return syncRoot(ctx, p, root, skip)
113}
114
115func syncRoot(ctx context.Context, p *Publisher, root *dirHandle, skip func(rel string, isDir bool) bool) (SyncStats, error) {
116 var st SyncStats
117 seen := map[string]bool{}
118 err := syncTree(ctx, root, p, skip, func(d *dirHandle, name, rel string) error {
119 if err := ctx.Err(); err != nil {
120 return err
121 }
122 body, mode, state, err := readStable(d, name)
123 if err != nil {
124 return err
125 }
126 switch state {
127 case fileGone:
128 return nil
129 case fileNotRegular:
130 st.NotRegular = append(st.NotRegular, rel)
131 return nil
132 case fileUnreadable:
133 seen[rel] = true // it exists: keep what the manifest has
134 st.Unreadable = append(st.Unreadable, rel)
135 return nil
136 case fileRetry:
137 seen[rel] = true // replaced mid-look: the next scan reads it
138 st.Retry = append(st.Retry, rel)
139 return nil
140 case fileTooLarge:
141 seen[rel] = true
142 st.TooLarge = append(st.TooLarge, rel)
143 return p.StageSkipped(rel, "too-large")
144 }
145 seen[rel] = true
146 before := p.pending()
147 if err := p.Stage(ctx, rel, body, mode, state == fileTorn); err != nil {
148 return err
149 }
150 if p.pending() != before {
151 st.Published++
152 }
153 if state == fileTorn {
154 st.Torn = append(st.Torn, rel)
155 }
156 return nil
157 })
158 if err != nil {
159 return st, fmt.Errorf("hostfiles: sync %s: %w", root.Name(), err)
160 }
161 for _, rel := range p.Paths() {
162 if seen[rel] || skipped(skip, rel, false) {
163 continue
164 }
165 if err := p.StageDelete(rel); err != nil {
166 return st, err
167 }
168 st.Deleted++
169 }
170 sort.Strings(st.TooLarge)
171 sort.Strings(st.NotRegular)
172 sort.Strings(st.Unreadable)
173 sort.Strings(st.Retry)
174 sort.Strings(st.Torn)
175 return st, nil
176}
177