github.com/rigado/snapd@v2.42.5-go-mod+incompatible/osutil/io.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2014-2015 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 "io" 26 "os" 27 "path/filepath" 28 "strings" 29 30 "github.com/snapcore/snapd/osutil/sys" 31 "github.com/snapcore/snapd/strutil" 32 ) 33 34 // AtomicWriteFlags are a bitfield of flags for AtomicWriteFile 35 type AtomicWriteFlags uint 36 37 const ( 38 // AtomicWriteFollow makes AtomicWriteFile follow symlinks 39 AtomicWriteFollow AtomicWriteFlags = 1 << iota 40 ) 41 42 // Allow disabling sync for testing. This brings massive improvements on 43 // certain filesystems (like btrfs) and very much noticeable improvements in 44 // all unit tests in genreal. 45 var snapdUnsafeIO bool = len(os.Args) > 0 && strings.HasSuffix(os.Args[0], ".test") && GetenvBool("SNAPD_UNSAFE_IO", true) 46 47 // An AtomicFile is similar to an os.File but it has an additional 48 // Commit() method that does whatever needs to be done so the 49 // modification is "atomic": an AtomicFile will do its best to leave 50 // either the previous content or the new content in permanent 51 // storage. It also has a Cancel() method to abort and clean up. 52 type AtomicFile struct { 53 *os.File 54 55 target string 56 tmpname string 57 uid sys.UserID 58 gid sys.GroupID 59 closed bool 60 renamed bool 61 } 62 63 // NewAtomicFile builds an AtomicFile backed by an *os.File that will have 64 // the given filename, permissions and uid/gid when Committed. 65 // 66 // It _might_ be implemented using O_TMPFILE (see open(2)). 67 // 68 // Note that it won't follow symlinks and will replace existing symlinks with 69 // the real file, unless the AtomicWriteFollow flag is specified. 70 // 71 // It is the caller's responsibility to clean up on error, by calling Cancel(). 72 // 73 // It is also the caller's responsibility to coordinate access to this, if it 74 // is used from different goroutines. 75 // 76 // Also note that there are a number of scenarios where Commit fails and then 77 // Cancel also fails. In all these scenarios your filesystem was probably in a 78 // rather poor state. Good luck. 79 func NewAtomicFile(filename string, perm os.FileMode, flags AtomicWriteFlags, uid sys.UserID, gid sys.GroupID) (aw *AtomicFile, err error) { 80 if flags&AtomicWriteFollow != 0 { 81 if fn, err := os.Readlink(filename); err == nil || (fn != "" && os.IsNotExist(err)) { 82 if filepath.IsAbs(fn) { 83 filename = fn 84 } else { 85 filename = filepath.Join(filepath.Dir(filename), fn) 86 } 87 } 88 } 89 // The tilde is appended so that programs that inspect all files in some 90 // directory are more likely to ignore this file as an editor backup file. 91 // 92 // This fixes an issue in apparmor-utils package, specifically in 93 // aa-enforce. Tools from this package enumerate all profiles by loading 94 // parsing any file found in /etc/apparmor.d/, skipping only very specific 95 // suffixes, such as the one we selected below. 96 tmp := filename + "." + strutil.MakeRandomString(12) + "~" 97 98 fd, err := os.OpenFile(tmp, os.O_WRONLY|os.O_CREATE|os.O_TRUNC|os.O_EXCL, perm) 99 if err != nil { 100 return nil, err 101 } 102 103 return &AtomicFile{ 104 File: fd, 105 target: filename, 106 tmpname: tmp, 107 uid: uid, 108 gid: gid, 109 }, nil 110 } 111 112 // ErrCannotCancel means the Commit operation failed at the last step, and 113 // your luck has run out. 114 var ErrCannotCancel = errors.New("cannot cancel: file has already been renamed") 115 116 func (aw *AtomicFile) Close() error { 117 aw.closed = true 118 return aw.File.Close() 119 } 120 121 // Cancel closes the AtomicWriter, and cleans up any artifacts. Cancel 122 // can fail if Commit() was (even partially) successful, but calling 123 // Cancel after a successful Commit does nothing beyond returning 124 // error--so it's always safe to defer a Cancel(). 125 func (aw *AtomicFile) Cancel() error { 126 if aw.renamed { 127 return ErrCannotCancel 128 } 129 130 var e1, e2 error 131 if aw.tmpname != "" { 132 e1 = os.Remove(aw.tmpname) 133 } 134 if !aw.closed { 135 e2 = aw.Close() 136 } 137 if e1 != nil { 138 return e1 139 } 140 return e2 141 } 142 143 var chown = sys.Chown 144 145 const NoChown = sys.FlagID 146 147 // Commit the modification; make it permanent. 148 // 149 // If Commit succeeds, the writer is closed and further attempts to 150 // write will fail. If Commit fails, the writer _might_ be closed; 151 // Cancel() needs to be called to clean up. 152 func (aw *AtomicFile) Commit() error { 153 if aw.uid != NoChown || aw.gid != NoChown { 154 if err := chown(aw.File, aw.uid, aw.gid); err != nil { 155 return err 156 } 157 } 158 159 var dir *os.File 160 if !snapdUnsafeIO { 161 // XXX: if go switches to use aio_fsync, we need to open the dir for writing 162 d, err := os.Open(filepath.Dir(aw.target)) 163 if err != nil { 164 return err 165 } 166 dir = d 167 defer dir.Close() 168 169 if err := aw.Sync(); err != nil { 170 return err 171 } 172 } 173 174 if err := aw.Close(); err != nil { 175 return err 176 } 177 178 if err := os.Rename(aw.tmpname, aw.target); err != nil { 179 return err 180 } 181 aw.renamed = true // it is now too late to Cancel() 182 183 if !snapdUnsafeIO { 184 return dir.Sync() 185 } 186 187 return nil 188 } 189 190 // The AtomicWrite* family of functions work like ioutil.WriteFile(), but the 191 // file created is an AtomicWriter, which is Committed before returning. 192 // 193 // AtomicWriteChown and AtomicWriteFileChown take an uid and a gid that can be 194 // used to specify the ownership of the created file. A special value of 195 // 0xffffffff (math.MaxUint32, or NoChown for convenience) can be used to 196 // request no change to that attribute. 197 // 198 // AtomicWriteFile and AtomicWriteFileChown take the content to be written as a 199 // []byte, and so work exactly like io.WriteFile(); AtomicWrite and 200 // AtomicWriteChown take an io.Reader which is copied into the file instead, 201 // and so are more amenable to streaming. 202 func AtomicWrite(filename string, reader io.Reader, perm os.FileMode, flags AtomicWriteFlags) (err error) { 203 return AtomicWriteChown(filename, reader, perm, flags, NoChown, NoChown) 204 } 205 206 func AtomicWriteFile(filename string, data []byte, perm os.FileMode, flags AtomicWriteFlags) (err error) { 207 return AtomicWriteChown(filename, bytes.NewReader(data), perm, flags, NoChown, NoChown) 208 } 209 210 func AtomicWriteFileChown(filename string, data []byte, perm os.FileMode, flags AtomicWriteFlags, uid sys.UserID, gid sys.GroupID) (err error) { 211 return AtomicWriteChown(filename, bytes.NewReader(data), perm, flags, uid, gid) 212 } 213 214 func AtomicWriteChown(filename string, reader io.Reader, perm os.FileMode, flags AtomicWriteFlags, uid sys.UserID, gid sys.GroupID) (err error) { 215 aw, err := NewAtomicFile(filename, perm, flags, uid, gid) 216 if err != nil { 217 return err 218 } 219 220 // Cancel once Committed is a NOP :-) 221 defer aw.Cancel() 222 223 if _, err := io.Copy(aw, reader); err != nil { 224 return err 225 } 226 227 return aw.Commit() 228 }