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
|
// SPDX-License-Identifier: GPL-3.0-or-later
package apply
import (
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"syscall"
)
// copyFile copies src to dst by streaming its content through a temporary
// file created in dst's directory, syncing it, then renaming it into place.
// dst must not already exist. Mode and modification time are preserved from
// src. On any failure the temporary file is removed and neither src nor a
// pre-existing dst is touched.
//
// This is not internal/config's replaceFile reused: that helper resolves a
// destination symlink and overwrites a file that is already there, and it
// takes the whole replacement as a []byte. copy's destination must not exist
// beforehand, and a []byte cannot stand in for a file that may be many
// gigabytes, so this is a separate, streaming equivalent kept local to
// internal/apply rather than shared with internal/config.
func copyFile(src, dst string) error {
in, err := os.Open(src)
if err != nil {
return err
}
defer in.Close()
fi, err := in.Stat()
if err != nil {
return err
}
dir := filepath.Dir(dst)
if err := os.MkdirAll(dir, 0o755); err != nil {
return err
}
tmp, err := os.CreateTemp(dir, ".krino-*")
if err != nil {
return err
}
tmpName := tmp.Name()
done := false
defer func() {
if !done {
os.Remove(tmpName)
}
}()
if _, err := io.Copy(tmp, in); err != nil {
tmp.Close()
return err
}
if err := tmp.Chmod(fi.Mode().Perm()); err != nil {
tmp.Close()
return err
}
if err := tmp.Sync(); err != nil {
tmp.Close()
return err
}
if err := tmp.Close(); err != nil {
return err
}
if err := os.Chtimes(tmpName, fi.ModTime(), fi.ModTime()); err != nil {
return err
}
// Deliberate, not redundant: the executor's own conflict re-check
// (runFileStep, apply.go) already found a name free of anything on disk
// before ever calling copyFile, so dst existing here means something
// else claimed it in the meantime. Refusing is the only safe response —
// silently overwriting it, via os.Rename below, would destroy whatever
// just raced us.
if _, err := os.Lstat(dst); err == nil {
return fmt.Errorf("copy: destination already exists: %s", dst)
} else if !os.IsNotExist(err) {
return err
}
if err := os.Rename(tmpName, dst); err != nil {
return err
}
done = true
return nil
}
// moveFile moves src to dst. It tries os.Rename first, which is atomic when
// src and dst are on the same filesystem. Only when that fails with EXDEV
// (a different filesystem) does it fall back to copying src to dst through
// copyFile — itself leaving no temporary file and touching neither src nor
// dst on failure — and, only once that copy has landed at dst, removing src.
// A failure at any point before the copy has landed leaves src exactly
// where it was; a failure to remove src afterward leaves both a full copy
// at dst and the original at src rather than risk deleting the only good
// copy.
//
// The filesystem check is done by unwrapping the error for syscall.EXDEV,
// never by comparing a Stat_t's device field: that field's type differs
// across the platforms `make ci` vets (freebsd, openbsd), while syscall.EXDEV
// itself is defined identically on all three.
func moveFile(src, dst string) error {
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
return err
}
err := os.Rename(src, dst)
if err == nil {
return nil
}
if !errors.Is(err, syscall.EXDEV) {
return err
}
if err := copyFile(src, dst); err != nil {
return err
}
return os.Remove(src)
}
// maxSuffixAttempts bounds nextFreeName. internal/plan/conflict.go and
// internal/trash/trash.go each have their own cap of the same size, for the
// same reason given below: nextFreeName solves yet another, independent
// collision problem and is not sharing code with either.
const maxSuffixAttempts = 10000
// nextFreeName finds the first stem_N.ext, N starting at 1, that does not
// currently exist on disk. It is the executor's own conflict re-check (spec
// §7.4, last paragraph): planning already resolved every conflict once
// against the filesystem as it was then, but something else can claim the
// planned name before the executor gets to it, so the executor looks again,
// right before acting, using only the real filesystem — it has no run-wide
// claim set to consult, unlike planning's.
//
// internal/plan's own suffixed() and splitExt are unexported, so they are
// not reachable from here; nextFreeName and splitExt below are a second,
// small implementation, the same shape as internal/plan's and
// internal/trash's for the same reason those two do not share code with
// each other either — each resolves a distinct, independently changing set
// of collisions (planned destinations; entries already in the Trash; names
// that appeared on disk since this plan was made).
func nextFreeName(dst string) (string, error) {
dir, base := filepath.Split(dst)
stem, ext := splitExt(base)
for n := 1; n <= maxSuffixAttempts; n++ {
candidate := filepath.Join(dir, fmt.Sprintf("%s_%d%s", stem, n, ext))
if _, err := os.Lstat(candidate); os.IsNotExist(err) {
return candidate, nil
}
}
return "", errors.New("too many conflicting names")
}
// splitExt splits name on its last dot, which does not count when it is the
// first character: "a.tar.gz" -> "a.tar", ".gz"; ".bashrc" -> ".bashrc", "".
// Duplicated from internal/plan/conflict.go and internal/trash/trash.go
// (unexported in both) rather than shared; see nextFreeName's comment.
func splitExt(name string) (stem, ext string) {
i := strings.LastIndexByte(name, '.')
if i <= 0 {
return name, ""
}
return name[:i], name[i:]
}
// mkdirAllTracked creates dir and any missing ancestors (mode 0755),
// returning every directory it actually created, outermost first, so undo
// can later remove the empty ones again. A directory that already existed
// is not included, and nothing is created or returned on error.
func mkdirAllTracked(dir string) ([]string, error) {
dir = filepath.Clean(dir)
if fi, err := os.Stat(dir); err == nil {
if !fi.IsDir() {
return nil, fmt.Errorf("%s exists and is not a directory", dir)
}
return nil, nil
} else if !os.IsNotExist(err) {
return nil, err
}
parent := filepath.Dir(dir)
var made []string
if parent != dir {
parentMade, err := mkdirAllTracked(parent)
if err != nil {
return nil, err
}
made = parentMade
}
if err := os.Mkdir(dir, 0o755); err != nil && !os.IsExist(err) {
return made, err
}
return append(made, dir), nil
}
|