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 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356
|
// -*- 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"
"time"
"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
mtime time.Time
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
// SetModTime sets the given modification time on the created file.
func (aw *AtomicFile) SetModTime(t time.Time) {
aw.mtime = t
}
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 !aw.mtime.IsZero() {
if err := os.Chtimes(aw.tmpname, time.Now(), aw.mtime); 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 os.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) (err error) {
var oldDir, newDir *os.File
// snapdUnsafeIO controls the ability to ignore expensive disk
// synchronization. It is only used inside tests.
if !snapdUnsafeIO {
// if called with a path with trailing '/', filepath.Dir
// returns the dir itself instead of the parent
oldName = filepath.Clean(oldName)
newName = filepath.Clean(newName)
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 maxLinkTries = 10
func atomicLinkOp(target, linkPath string, op func(target, tmp string) error, kind string) error {
for tries := 0; tries < maxLinkTries; tries++ {
tmp := linkPath + "." + randutil.RandomString(12) + "~"
if err := op(target, tmp); err != nil {
if os.IsExist(err) {
continue
}
return err
}
defer os.Remove(tmp)
return AtomicRename(tmp, linkPath)
}
return fmt.Errorf("cannot create a temporary %s", kind)
}
// 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 {
return atomicLinkOp(target, linkPath, os.Symlink, "symlink")
}
// AtomicLink attempts to atomically create a hardlink at linkPath, referencing
// the given target. The process creates a temporary hardlink object pointing
// to the target, and then proceeds to rename it atomically, replacing the
// linkPath.
func AtomicLink(target, linkPath string) error {
return atomicLinkOp(target, linkPath, os.Link, "link")
}
|