-
Notifications
You must be signed in to change notification settings - Fork 469
/
system.go
309 lines (276 loc) · 9.4 KB
/
system.go
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
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
package chezmoi
import (
"context"
"errors"
"io/fs"
"os/exec"
"sort"
"strings"
"time"
vfs "github.com/twpayne/go-vfs/v4"
"golang.org/x/sync/errgroup"
)
type RunScriptOptions struct {
Interpreter *Interpreter
Condition ScriptCondition
}
// A System reads from and writes to a filesystem, runs scripts, and persists
// state.
type System interface { //nolint:interfacebloat
Chmod(name AbsPath, mode fs.FileMode) error
Chtimes(name AbsPath, atime, mtime time.Time) error
Glob(pattern string) ([]string, error)
Link(oldname, newname AbsPath) error
Lstat(filename AbsPath) (fs.FileInfo, error)
Mkdir(name AbsPath, perm fs.FileMode) error
RawPath(absPath AbsPath) (AbsPath, error)
ReadDir(name AbsPath) ([]fs.DirEntry, error)
ReadFile(name AbsPath) ([]byte, error)
Readlink(name AbsPath) (string, error)
Remove(name AbsPath) error
RemoveAll(name AbsPath) error
Rename(oldpath, newpath AbsPath) error
RunCmd(cmd *exec.Cmd) error
RunScript(scriptname RelPath, dir AbsPath, data []byte, options RunScriptOptions) error
Stat(name AbsPath) (fs.FileInfo, error)
UnderlyingFS() vfs.FS
WriteFile(filename AbsPath, data []byte, perm fs.FileMode) error
WriteSymlink(oldname string, newname AbsPath) error
}
// A emptySystemMixin simulates an empty system.
type emptySystemMixin struct{}
func (emptySystemMixin) Glob(pattern string) ([]string, error) { return nil, nil }
func (emptySystemMixin) Lstat(name AbsPath) (fs.FileInfo, error) { return nil, fs.ErrNotExist }
func (emptySystemMixin) RawPath(path AbsPath) (AbsPath, error) { return path, nil }
func (emptySystemMixin) ReadDir(name AbsPath) ([]fs.DirEntry, error) { return nil, fs.ErrNotExist }
func (emptySystemMixin) ReadFile(name AbsPath) ([]byte, error) { return nil, fs.ErrNotExist }
func (emptySystemMixin) Readlink(name AbsPath) (string, error) { return "", fs.ErrNotExist }
func (emptySystemMixin) Stat(name AbsPath) (fs.FileInfo, error) { return nil, fs.ErrNotExist }
func (emptySystemMixin) UnderlyingFS() vfs.FS { return nil }
// A noUpdateSystemMixin panics on any update.
type noUpdateSystemMixin struct{}
func (noUpdateSystemMixin) Chmod(name AbsPath, perm fs.FileMode) error {
panic("update to no update system")
}
func (noUpdateSystemMixin) Chtimes(name AbsPath, atime, mtime time.Time) error {
panic("update to no update system")
}
func (noUpdateSystemMixin) Link(oldname, newname AbsPath) error {
panic("update to no update system")
}
func (noUpdateSystemMixin) Mkdir(name AbsPath, perm fs.FileMode) error {
panic("update to no update system")
}
func (noUpdateSystemMixin) Remove(name AbsPath) error {
panic("update to no update system")
}
func (noUpdateSystemMixin) RemoveAll(name AbsPath) error {
panic("update to no update system")
}
func (noUpdateSystemMixin) Rename(oldpath, newpath AbsPath) error {
panic("update to no update system")
}
func (noUpdateSystemMixin) RunCmd(cmd *exec.Cmd) error {
panic("update to no update system")
}
func (noUpdateSystemMixin) RunScript(
scriptname RelPath,
dir AbsPath,
data []byte,
options RunScriptOptions,
) error {
panic("update to no update system")
}
func (noUpdateSystemMixin) WriteFile(filename AbsPath, data []byte, perm fs.FileMode) error {
panic("update to no update system")
}
func (noUpdateSystemMixin) WriteSymlink(oldname string, newname AbsPath) error {
panic("update to no update system")
}
// MkdirAll is the equivalent of os.MkdirAll but operates on system.
func MkdirAll(system System, absPath AbsPath, perm fs.FileMode) error {
switch err := system.Mkdir(absPath, perm); {
case err == nil:
// Mkdir was successful.
return nil
case errors.Is(err, fs.ErrExist):
// path already exists, but we don't know whether it's a directory or
// something else. We get this error if we try to create a subdirectory
// of a non-directory, for example if the parent directory of path is a
// file. There's a race condition here between the call to Mkdir and the
// call to Stat but we can't avoid it because there's not enough
// information in the returned error from Mkdir. We need to distinguish
// between "path already exists and is already a directory" and "path
// already exists and is not a directory". Between the call to Mkdir and
// the call to Stat path might have changed.
fileInfo, statErr := system.Stat(absPath)
if statErr != nil {
return statErr
}
if !fileInfo.IsDir() {
return err
}
return nil
case errors.Is(err, fs.ErrNotExist):
// Parent directory does not exist. Create the parent directory
// recursively, then try again.
parentDir := absPath.Dir()
if parentDir == RootAbsPath || parentDir == DotAbsPath {
// We cannot create the root directory or the current directory, so
// return the original error.
return err
}
if err := MkdirAll(system, parentDir, perm); err != nil {
return err
}
return system.Mkdir(absPath, perm)
default:
// Some other error.
return err
}
}
// A WalkFunc is called for every entry in a directory.
type WalkFunc func(absPath AbsPath, fileInfo fs.FileInfo, err error) error
// Walk walks rootAbsPath in system, calling walkFunc for each file or directory
// in the tree, including rootAbsPath.
//
// Walk does not follow symlinks.
func Walk(system System, rootAbsPath AbsPath, walkFunc WalkFunc) error {
outerWalkFunc := func(absPath string, fileInfo fs.FileInfo, err error) error {
return walkFunc(NewAbsPath(absPath).ToSlash(), fileInfo, err)
}
return vfs.Walk(system.UnderlyingFS(), rootAbsPath.String(), outerWalkFunc)
}
// A concurrentWalkSourceDirFunc is a function called concurrently for every
// entry in a source directory.
type concurrentWalkSourceDirFunc func(ctx context.Context, absPath AbsPath, fileInfo fs.FileInfo, err error) error
// WalkSourceDir walks the source directory rooted at sourceDirAbsPath in
// system, calling walkFunc for each file or directory in the tree, including
// sourceDirAbsPath.
//
// WalkSourceDir does not follow symbolic links found in directories, but if
// sourceDirAbsPath itself is a symbolic link, its target will be walked.
//
// Directory entries .chezmoidata.<format> and .chezmoitemplates are visited
// before all other entries. All other entries are visited in alphabetical
// order.
func WalkSourceDir(system System, sourceDirAbsPath AbsPath, walkFunc WalkFunc) error {
fileInfo, err := system.Stat(sourceDirAbsPath)
if err != nil {
err = walkFunc(sourceDirAbsPath, nil, err)
} else {
err = walkSourceDir(system, sourceDirAbsPath, fileInfo, walkFunc)
if errors.Is(err, fs.SkipDir) {
err = nil
}
}
return err
}
// sourceDirEntryOrder defines the order in which entries are visited in the
// source directory. More negative values are visited first. Entries with the
// same order are visited alphabetically. The default order is zero.
var sourceDirEntryOrder = map[string]int{
VersionName: -3,
dataName + ".json": -2,
dataName + ".toml": -2,
dataName + ".yaml": -2,
TemplatesDirName: -1,
}
// walkSourceDir is a helper function for WalkSourceDir.
func walkSourceDir(system System, name AbsPath, fileInfo fs.FileInfo, walkFunc WalkFunc) error {
switch err := walkFunc(name, fileInfo, nil); {
case fileInfo.IsDir() && errors.Is(err, fs.SkipDir):
return nil
case err != nil:
return err
case !fileInfo.IsDir():
return nil
}
dirEntries, err := system.ReadDir(name)
if err != nil {
err = walkFunc(name, fileInfo, err)
if err != nil {
return err
}
}
sortSourceDirEntries(dirEntries)
for _, dirEntry := range dirEntries {
fileInfo, err := dirEntry.Info()
if err != nil {
err = walkFunc(name, nil, err)
if err != nil {
return err
}
}
if err := walkSourceDir(system, name.JoinString(dirEntry.Name()), fileInfo, walkFunc); err != nil {
if !errors.Is(err, fs.SkipDir) {
return err
}
}
}
return nil
}
func concurrentWalkSourceDir(
ctx context.Context, system System, dirAbsPath AbsPath, walkFunc concurrentWalkSourceDirFunc,
) error {
dirEntries, err := system.ReadDir(dirAbsPath)
if err != nil {
return walkFunc(ctx, dirAbsPath, nil, err)
}
sortSourceDirEntries(dirEntries)
// Walk all control plane entries in order.
visitDirEntry := func(dirEntry fs.DirEntry) error {
absPath := dirAbsPath.Join(NewRelPath(dirEntry.Name()))
fileInfo, err := dirEntry.Info()
if err != nil {
return walkFunc(ctx, absPath, nil, err)
}
switch err := walkFunc(ctx, absPath, fileInfo, nil); {
case fileInfo.IsDir() && errors.Is(err, fs.SkipDir):
return nil
case err != nil:
return err
case fileInfo.IsDir():
return concurrentWalkSourceDir(ctx, system, absPath, walkFunc)
default:
return nil
}
}
i := 0
for ; i < len(dirEntries); i++ {
dirEntry := dirEntries[i]
if !strings.HasPrefix(dirEntry.Name(), ".") {
break
}
if err := visitDirEntry(dirEntry); err != nil {
return err
}
}
// Walk all remaining entries concurrently.
visitDirEntryFunc := func(dirEntry fs.DirEntry) func() error {
return func() error {
return visitDirEntry(dirEntry)
}
}
group, ctx := errgroup.WithContext(ctx)
for _, dirEntry := range dirEntries[i:] {
group.Go(visitDirEntryFunc(dirEntry))
}
return group.Wait()
}
func sortSourceDirEntries(dirEntries []fs.DirEntry) {
sort.Slice(dirEntries, func(i, j int) bool {
nameI := dirEntries[i].Name()
nameJ := dirEntries[j].Name()
orderI := sourceDirEntryOrder[nameI]
orderJ := sourceDirEntryOrder[nameJ]
switch {
case orderI < orderJ:
return true
case orderI == orderJ:
return nameI < nameJ
default:
return false
}
})
}