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  }