github.com/hugh712/snapd@v0.0.0-20200910133618-1a99902bd583/osutil/io.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2014-2020 Canonical Ltd 5 * 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License version 3 as 8 * published by the Free Software Foundation. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 * 18 */ 19 20 package osutil 21 22 import ( 23 "bytes" 24 "errors" 25 "fmt" 26 "io" 27 "os" 28 "path/filepath" 29 "syscall" 30 31 "github.com/snapcore/snapd/osutil/sys" 32 "github.com/snapcore/snapd/randutil" 33 ) 34 35 // AtomicWriteFlags are a bitfield of flags for AtomicWriteFile 36 type AtomicWriteFlags uint 37 38 const ( 39 // AtomicWriteFollow makes AtomicWriteFile follow symlinks 40 AtomicWriteFollow AtomicWriteFlags = 1 << iota 41 ) 42 43 // Allow disabling sync for testing. This brings massive improvements on 44 // certain filesystems (like btrfs) and very much noticeable improvements in 45 // all unit tests in genreal. 46 var snapdUnsafeIO bool = IsTestBinary() && GetenvBool("SNAPD_UNSAFE_IO", true) 47 48 // An AtomicFile is similar to an os.File but it has an additional 49 // Commit() method that does whatever needs to be done so the 50 // modification is "atomic": an AtomicFile will do its best to leave 51 // either the previous content or the new content in permanent 52 // storage. It also has a Cancel() method to abort and clean up. 53 type AtomicFile struct { 54 *os.File 55 56 target string 57 tmpname string 58 uid sys.UserID 59 gid sys.GroupID 60 closed bool 61 renamed bool 62 } 63 64 // NewAtomicFile builds an AtomicFile backed by an *os.File that will have 65 // the given filename, permissions and uid/gid when Committed. 66 // 67 // It _might_ be implemented using O_TMPFILE (see open(2)). 68 // 69 // Note that it won't follow symlinks and will replace existing symlinks with 70 // the real file, unless the AtomicWriteFollow flag is specified. 71 // 72 // It is the caller's responsibility to clean up on error, by calling Cancel(). 73 // 74 // It is also the caller's responsibility to coordinate access to this, if it 75 // is used from different goroutines. 76 // 77 // Also note that there are a number of scenarios where Commit fails and then 78 // Cancel also fails. In all these scenarios your filesystem was probably in a 79 // rather poor state. Good luck. 80 func NewAtomicFile(filename string, perm os.FileMode, flags AtomicWriteFlags, uid sys.UserID, gid sys.GroupID) (aw *AtomicFile, err error) { 81 if flags&AtomicWriteFollow != 0 { 82 if fn, err := os.Readlink(filename); err == nil || (fn != "" && os.IsNotExist(err)) { 83 if filepath.IsAbs(fn) { 84 filename = fn 85 } else { 86 filename = filepath.Join(filepath.Dir(filename), fn) 87 } 88 } 89 } 90 // The tilde is appended so that programs that inspect all files in some 91 // directory are more likely to ignore this file as an editor backup file. 92 // 93 // This fixes an issue in apparmor-utils package, specifically in 94 // aa-enforce. Tools from this package enumerate all profiles by loading 95 // parsing any file found in /etc/apparmor.d/, skipping only very specific 96 // suffixes, such as the one we selected below. 97 tmp := filename + "." + randutil.RandomString(12) + "~" 98 99 fd, err := os.OpenFile(tmp, os.O_WRONLY|os.O_CREATE|os.O_TRUNC|os.O_EXCL, perm) 100 if err != nil { 101 return nil, err 102 } 103 104 return &AtomicFile{ 105 File: fd, 106 target: filename, 107 tmpname: tmp, 108 uid: uid, 109 gid: gid, 110 }, nil 111 } 112 113 // ErrCannotCancel means the Commit operation failed at the last step, and 114 // your luck has run out. 115 var ErrCannotCancel = errors.New("cannot cancel: file has already been renamed") 116 117 func (aw *AtomicFile) Close() error { 118 aw.closed = true 119 return aw.File.Close() 120 } 121 122 // Cancel closes the AtomicWriter, and cleans up any artifacts. Cancel 123 // can fail if Commit() was (even partially) successful, but calling 124 // Cancel after a successful Commit does nothing beyond returning 125 // error--so it's always safe to defer a Cancel(). 126 func (aw *AtomicFile) Cancel() error { 127 if aw.renamed { 128 return ErrCannotCancel 129 } 130 131 var e1, e2 error 132 if aw.tmpname != "" { 133 e1 = os.Remove(aw.tmpname) 134 } 135 if !aw.closed { 136 e2 = aw.Close() 137 } 138 if e1 != nil { 139 return e1 140 } 141 return e2 142 } 143 144 var chown = sys.Chown 145 146 const NoChown = sys.FlagID 147 148 func (aw *AtomicFile) commit() error { 149 if aw.uid != NoChown || aw.gid != NoChown { 150 if err := chown(aw.File, aw.uid, aw.gid); err != nil { 151 return err 152 } 153 } 154 155 var dir *os.File 156 if !snapdUnsafeIO { 157 // XXX: if go switches to use aio_fsync, we need to open the dir for writing 158 d, err := os.Open(filepath.Dir(aw.target)) 159 if err != nil { 160 return err 161 } 162 dir = d 163 defer dir.Close() 164 165 if err := aw.Sync(); err != nil { 166 return err 167 } 168 } 169 170 if err := aw.Close(); err != nil { 171 return err 172 } 173 174 if err := os.Rename(aw.tmpname, aw.target); err != nil { 175 return err 176 } 177 aw.renamed = true // it is now too late to Cancel() 178 179 if !snapdUnsafeIO { 180 return dir.Sync() 181 } 182 183 return nil 184 } 185 186 // Commit the modification; make it permanent. 187 // 188 // If Commit succeeds, the writer is closed and further attempts to 189 // write will fail. If Commit fails, the writer _might_ be closed; 190 // Cancel() needs to be called to clean up. 191 func (aw *AtomicFile) Commit() error { 192 return aw.commit() 193 } 194 195 // CommitAs commits the file under a new target name, following the same rules 196 // as Commit. The new target name must be located in the same directory as the 197 // original filename provided when creating AtomicFile. 198 // 199 // The call is useful when the target name is not known until the end (eg. it 200 // may depend on data being written to the file), in which case one can create 201 // AtomicFile using a temporary name and later override the actual name by 202 // calling CommitAs. 203 func (aw *AtomicFile) CommitAs(filename string) error { 204 if dir := filepath.Dir(filename); dir != filepath.Dir(aw.target) { 205 return fmt.Errorf("cannot commit as %q to a different directory %q", filepath.Base(filename), dir) 206 } 207 aw.target = filename 208 return aw.commit() 209 } 210 211 // The AtomicWrite* family of functions work like ioutil.WriteFile(), but the 212 // file created is an AtomicWriter, which is Committed before returning. 213 // 214 // AtomicWriteChown and AtomicWriteFileChown take an uid and a gid that can be 215 // used to specify the ownership of the created file. A special value of 216 // 0xffffffff (math.MaxUint32, or NoChown for convenience) can be used to 217 // request no change to that attribute. 218 // 219 // AtomicWriteFile and AtomicWriteFileChown take the content to be written as a 220 // []byte, and so work exactly like io.WriteFile(); AtomicWrite and 221 // AtomicWriteChown take an io.Reader which is copied into the file instead, 222 // and so are more amenable to streaming. 223 func AtomicWrite(filename string, reader io.Reader, perm os.FileMode, flags AtomicWriteFlags) (err error) { 224 return AtomicWriteChown(filename, reader, perm, flags, NoChown, NoChown) 225 } 226 227 func AtomicWriteFile(filename string, data []byte, perm os.FileMode, flags AtomicWriteFlags) (err error) { 228 return AtomicWriteChown(filename, bytes.NewReader(data), perm, flags, NoChown, NoChown) 229 } 230 231 func AtomicWriteFileChown(filename string, data []byte, perm os.FileMode, flags AtomicWriteFlags, uid sys.UserID, gid sys.GroupID) (err error) { 232 return AtomicWriteChown(filename, bytes.NewReader(data), perm, flags, uid, gid) 233 } 234 235 func AtomicWriteChown(filename string, reader io.Reader, perm os.FileMode, flags AtomicWriteFlags, uid sys.UserID, gid sys.GroupID) (err error) { 236 aw, err := NewAtomicFile(filename, perm, flags, uid, gid) 237 if err != nil { 238 return err 239 } 240 241 // Cancel once Committed is a NOP :-) 242 defer aw.Cancel() 243 244 if _, err := io.Copy(aw, reader); err != nil { 245 return err 246 } 247 248 return aw.Commit() 249 } 250 251 // AtomicRename attempts to rename a path from oldName to newName atomically. 252 func AtomicRename(oldName, newName string) error { 253 var oldDir, newDir *os.File 254 255 // snapdUnsafeIO controls the ability to ignore expensive disk 256 // synchronization. It is only used inside tests. 257 if !snapdUnsafeIO { 258 oldDirPath := filepath.Dir(oldName) 259 newDirPath := filepath.Dir(newName) 260 261 oldDir, err := os.Open(oldDirPath) 262 if err != nil { 263 return err 264 } 265 defer oldDir.Close() 266 267 newDir, err := os.Open(newDirPath) 268 if err != nil { 269 return err 270 } 271 defer newDir.Close() 272 273 oldInfo, err := oldDir.Stat() 274 if err != nil { 275 return err 276 } 277 newInfo, err := newDir.Stat() 278 if err != nil { 279 return err 280 } 281 if oldStat, ok := oldInfo.Sys().(*syscall.Stat_t); ok { 282 if newStat, ok := newInfo.Sys().(*syscall.Stat_t); ok { 283 // Old and new directories refer to the same location. We can only sync once. 284 if oldStat.Dev == newStat.Dev && oldStat.Ino == newStat.Ino { 285 newDir = nil 286 } 287 } 288 } 289 } 290 291 if err := os.Rename(oldName, newName); err != nil { 292 return err 293 } 294 var err1, err2 error 295 if oldDir != nil { 296 err1 = oldDir.Sync() 297 } 298 if newDir != nil { 299 err2 = newDir.Sync() 300 } 301 if err1 != nil { 302 return err1 303 } 304 return err2 305 } 306 307 const maxSymlinkTries = 10 308 309 // AtomicSymlink attempts to atomically create a symlink at linkPath, pointing 310 // to a given target. The process creates a temporary symlink object pointing to 311 // the target, and then proceeds to rename it atomically, replacing the 312 // linkPath. 313 func AtomicSymlink(target, linkPath string) error { 314 for tries := 0; tries < maxSymlinkTries; tries++ { 315 tmp := linkPath + "." + randutil.RandomString(12) + "~" 316 if err := os.Symlink(target, tmp); err != nil { 317 if os.IsExist(err) { 318 continue 319 } 320 return err 321 } 322 defer os.Remove(tmp) 323 return AtomicRename(tmp, linkPath) 324 } 325 return errors.New("cannot create a temporary symlink") 326 }