-
Notifications
You must be signed in to change notification settings - Fork 1
/
io.go
326 lines (284 loc) · 9.54 KB
/
io.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
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
// -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2014-2020 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package osutil
import (
"bytes"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"syscall"
"github.com/snapcore/snapd/osutil/sys"
"github.com/snapcore/snapd/randutil"
)
// AtomicWriteFlags are a bitfield of flags for AtomicWriteFile
type AtomicWriteFlags uint
const (
// AtomicWriteFollow makes AtomicWriteFile follow symlinks
AtomicWriteFollow AtomicWriteFlags = 1 << iota
)
// Allow disabling sync for testing. This brings massive improvements on
// certain filesystems (like btrfs) and very much noticeable improvements in
// all unit tests in genreal.
var snapdUnsafeIO bool = IsTestBinary() && GetenvBool("SNAPD_UNSAFE_IO", true)
// An AtomicFile is similar to an os.File but it has an additional
// Commit() method that does whatever needs to be done so the
// modification is "atomic": an AtomicFile will do its best to leave
// either the previous content or the new content in permanent
// storage. It also has a Cancel() method to abort and clean up.
type AtomicFile struct {
*os.File
target string
tmpname string
uid sys.UserID
gid sys.GroupID
closed bool
renamed bool
}
// NewAtomicFile builds an AtomicFile backed by an *os.File that will have
// the given filename, permissions and uid/gid when Committed.
//
// It _might_ be implemented using O_TMPFILE (see open(2)).
//
// Note that it won't follow symlinks and will replace existing symlinks with
// the real file, unless the AtomicWriteFollow flag is specified.
//
// It is the caller's responsibility to clean up on error, by calling Cancel().
//
// It is also the caller's responsibility to coordinate access to this, if it
// is used from different goroutines.
//
// Also note that there are a number of scenarios where Commit fails and then
// Cancel also fails. In all these scenarios your filesystem was probably in a
// rather poor state. Good luck.
func NewAtomicFile(filename string, perm os.FileMode, flags AtomicWriteFlags, uid sys.UserID, gid sys.GroupID) (aw *AtomicFile, err error) {
if flags&AtomicWriteFollow != 0 {
if fn, err := os.Readlink(filename); err == nil || (fn != "" && os.IsNotExist(err)) {
if filepath.IsAbs(fn) {
filename = fn
} else {
filename = filepath.Join(filepath.Dir(filename), fn)
}
}
}
// The tilde is appended so that programs that inspect all files in some
// directory are more likely to ignore this file as an editor backup file.
//
// This fixes an issue in apparmor-utils package, specifically in
// aa-enforce. Tools from this package enumerate all profiles by loading
// parsing any file found in /etc/apparmor.d/, skipping only very specific
// suffixes, such as the one we selected below.
tmp := filename + "." + randutil.RandomString(12) + "~"
fd, err := os.OpenFile(tmp, os.O_WRONLY|os.O_CREATE|os.O_TRUNC|os.O_EXCL, perm)
if err != nil {
return nil, err
}
return &AtomicFile{
File: fd,
target: filename,
tmpname: tmp,
uid: uid,
gid: gid,
}, nil
}
// ErrCannotCancel means the Commit operation failed at the last step, and
// your luck has run out.
var ErrCannotCancel = errors.New("cannot cancel: file has already been renamed")
func (aw *AtomicFile) Close() error {
aw.closed = true
return aw.File.Close()
}
// Cancel closes the AtomicWriter, and cleans up any artifacts. Cancel
// can fail if Commit() was (even partially) successful, but calling
// Cancel after a successful Commit does nothing beyond returning
// error--so it's always safe to defer a Cancel().
func (aw *AtomicFile) Cancel() error {
if aw.renamed {
return ErrCannotCancel
}
var e1, e2 error
if aw.tmpname != "" {
e1 = os.Remove(aw.tmpname)
}
if !aw.closed {
e2 = aw.Close()
}
if e1 != nil {
return e1
}
return e2
}
var chown = sys.Chown
const NoChown = sys.FlagID
func (aw *AtomicFile) commit() error {
if aw.uid != NoChown || aw.gid != NoChown {
if err := chown(aw.File, aw.uid, aw.gid); err != nil {
return err
}
}
var dir *os.File
if !snapdUnsafeIO {
// XXX: if go switches to use aio_fsync, we need to open the dir for writing
d, err := os.Open(filepath.Dir(aw.target))
if err != nil {
return err
}
dir = d
defer dir.Close()
if err := aw.Sync(); err != nil {
return err
}
}
if err := aw.Close(); err != nil {
return err
}
if err := os.Rename(aw.tmpname, aw.target); err != nil {
return err
}
aw.renamed = true // it is now too late to Cancel()
if !snapdUnsafeIO {
return dir.Sync()
}
return nil
}
// Commit the modification; make it permanent.
//
// If Commit succeeds, the writer is closed and further attempts to
// write will fail. If Commit fails, the writer _might_ be closed;
// Cancel() needs to be called to clean up.
func (aw *AtomicFile) Commit() error {
return aw.commit()
}
// CommitAs commits the file under a new target name, following the same rules
// as Commit. The new target name must be located in the same directory as the
// original filename provided when creating AtomicFile.
//
// The call is useful when the target name is not known until the end (eg. it
// may depend on data being written to the file), in which case one can create
// AtomicFile using a temporary name and later override the actual name by
// calling CommitAs.
func (aw *AtomicFile) CommitAs(filename string) error {
if dir := filepath.Dir(filename); dir != filepath.Dir(aw.target) {
return fmt.Errorf("cannot commit as %q to a different directory %q", filepath.Base(filename), dir)
}
aw.target = filename
return aw.commit()
}
// The AtomicWrite* family of functions work like ioutil.WriteFile(), but the
// file created is an AtomicWriter, which is Committed before returning.
//
// AtomicWriteChown and AtomicWriteFileChown take an uid and a gid that can be
// used to specify the ownership of the created file. A special value of
// 0xffffffff (math.MaxUint32, or NoChown for convenience) can be used to
// request no change to that attribute.
//
// AtomicWriteFile and AtomicWriteFileChown take the content to be written as a
// []byte, and so work exactly like io.WriteFile(); AtomicWrite and
// AtomicWriteChown take an io.Reader which is copied into the file instead,
// and so are more amenable to streaming.
func AtomicWrite(filename string, reader io.Reader, perm os.FileMode, flags AtomicWriteFlags) (err error) {
return AtomicWriteChown(filename, reader, perm, flags, NoChown, NoChown)
}
func AtomicWriteFile(filename string, data []byte, perm os.FileMode, flags AtomicWriteFlags) (err error) {
return AtomicWriteChown(filename, bytes.NewReader(data), perm, flags, NoChown, NoChown)
}
func AtomicWriteFileChown(filename string, data []byte, perm os.FileMode, flags AtomicWriteFlags, uid sys.UserID, gid sys.GroupID) (err error) {
return AtomicWriteChown(filename, bytes.NewReader(data), perm, flags, uid, gid)
}
func AtomicWriteChown(filename string, reader io.Reader, perm os.FileMode, flags AtomicWriteFlags, uid sys.UserID, gid sys.GroupID) (err error) {
aw, err := NewAtomicFile(filename, perm, flags, uid, gid)
if err != nil {
return err
}
// Cancel once Committed is a NOP :-)
defer aw.Cancel()
if _, err := io.Copy(aw, reader); err != nil {
return err
}
return aw.Commit()
}
// AtomicRename attempts to rename a path from oldName to newName atomically.
func AtomicRename(oldName, newName string) error {
var oldDir, newDir *os.File
// snapdUnsafeIO controls the ability to ignore expensive disk
// synchronization. It is only used inside tests.
if !snapdUnsafeIO {
oldDirPath := filepath.Dir(oldName)
newDirPath := filepath.Dir(newName)
oldDir, err := os.Open(oldDirPath)
if err != nil {
return err
}
defer oldDir.Close()
newDir, err := os.Open(newDirPath)
if err != nil {
return err
}
defer newDir.Close()
oldInfo, err := oldDir.Stat()
if err != nil {
return err
}
newInfo, err := newDir.Stat()
if err != nil {
return err
}
if oldStat, ok := oldInfo.Sys().(*syscall.Stat_t); ok {
if newStat, ok := newInfo.Sys().(*syscall.Stat_t); ok {
// Old and new directories refer to the same location. We can only sync once.
if oldStat.Dev == newStat.Dev && oldStat.Ino == newStat.Ino {
newDir = nil
}
}
}
}
if err := os.Rename(oldName, newName); err != nil {
return err
}
var err1, err2 error
if oldDir != nil {
err1 = oldDir.Sync()
}
if newDir != nil {
err2 = newDir.Sync()
}
if err1 != nil {
return err1
}
return err2
}
const maxSymlinkTries = 10
// AtomicSymlink attempts to atomically create a symlink at linkPath, pointing
// to a given target. The process creates a temporary symlink object pointing to
// the target, and then proceeds to rename it atomically, replacing the
// linkPath.
func AtomicSymlink(target, linkPath string) error {
for tries := 0; tries < maxSymlinkTries; tries++ {
tmp := linkPath + "." + randutil.RandomString(12) + "~"
if err := os.Symlink(target, tmp); err != nil {
if os.IsExist(err) {
continue
}
return err
}
defer os.Remove(tmp)
return AtomicRename(tmp, linkPath)
}
return errors.New("cannot create a temporary symlink")
}