github.com/chipaca/snappy@v0.0.0-20210104084008-1f06296fe8ad/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  }