github.com/chipaca/snappy@v0.0.0-20210104084008-1f06296fe8ad/gadget/mountedfilesystem.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2019-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 gadget
    21  
    22  import (
    23  	"bytes"
    24  	"crypto"
    25  	_ "crypto/sha1"
    26  	"fmt"
    27  	"io"
    28  	"io/ioutil"
    29  	"os"
    30  	"path/filepath"
    31  	"sort"
    32  	"strings"
    33  
    34  	"github.com/snapcore/snapd/logger"
    35  	"github.com/snapcore/snapd/osutil"
    36  	"github.com/snapcore/snapd/strutil"
    37  )
    38  
    39  func checkSourceIsDir(src string) error {
    40  	if !osutil.IsDirectory(src) {
    41  		if strings.HasSuffix(src, "/") {
    42  			return fmt.Errorf("cannot specify trailing / for a source which is not a directory")
    43  		}
    44  		return fmt.Errorf("source is not a directory")
    45  	}
    46  	return nil
    47  }
    48  
    49  func checkContent(content *VolumeContent) error {
    50  	if content.ResolvedSource() == "" {
    51  		return fmt.Errorf("internal error: source cannot be unset")
    52  	}
    53  	if content.Target == "" {
    54  		return fmt.Errorf("internal error: target cannot be unset")
    55  	}
    56  	return nil
    57  }
    58  
    59  func observe(observer ContentObserver, op ContentOperation, ps *LaidOutStructure, root, dst string, data *ContentChange) (ContentChangeAction, error) {
    60  	if observer == nil {
    61  		return ChangeApply, nil
    62  	}
    63  	relativeTarget := dst
    64  	if strings.HasPrefix(dst, root) {
    65  		// target path isn't really relative, make it so now
    66  		relative, err := filepath.Rel(root, dst)
    67  		if err != nil {
    68  			return ChangeAbort, err
    69  		}
    70  		relativeTarget = relative
    71  	}
    72  	return observer.Observe(op, ps, root, relativeTarget, data)
    73  }
    74  
    75  // TODO: MountedFilesystemWriter should not be exported
    76  
    77  // MountedFilesystemWriter assists in writing contents of a structure to a
    78  // mounted filesystem.
    79  type MountedFilesystemWriter struct {
    80  	contentDir string
    81  	ps         *LaidOutStructure
    82  	observer   ContentObserver
    83  }
    84  
    85  // NewMountedFilesystemWriter returns a writer capable of writing provided
    86  // structure, with content of the structure stored in the given root directory.
    87  func NewMountedFilesystemWriter(contentDir string, ps *LaidOutStructure, observer ContentObserver) (*MountedFilesystemWriter, error) {
    88  	if ps == nil {
    89  		return nil, fmt.Errorf("internal error: *LaidOutStructure is nil")
    90  	}
    91  	if !ps.HasFilesystem() {
    92  		return nil, fmt.Errorf("structure %v has no filesystem", ps)
    93  	}
    94  	if contentDir == "" {
    95  		return nil, fmt.Errorf("internal error: gadget content directory cannot be unset")
    96  	}
    97  	fw := &MountedFilesystemWriter{
    98  		contentDir: contentDir,
    99  		ps:         ps,
   100  		observer:   observer,
   101  	}
   102  	return fw, nil
   103  }
   104  
   105  func mapPreserve(dstDir string, preserve []string) ([]string, error) {
   106  	preserveInDst := make([]string, len(preserve))
   107  	for i, p := range preserve {
   108  		inDst := filepath.Join(dstDir, p)
   109  
   110  		if osutil.IsDirectory(inDst) {
   111  			return nil, fmt.Errorf("preserved entry %q cannot be a directory", p)
   112  		}
   113  
   114  		preserveInDst[i] = inDst
   115  	}
   116  	sort.Strings(preserveInDst)
   117  
   118  	return preserveInDst, nil
   119  }
   120  
   121  // Write writes structure data into provided directory. All existing files are
   122  // overwritten, unless their paths, relative to target directory, are listed in
   123  // the preserve list. Permission bits and ownership of updated entries is not
   124  // preserved.
   125  func (m *MountedFilesystemWriter) Write(whereDir string, preserve []string) error {
   126  	if whereDir == "" {
   127  		return fmt.Errorf("internal error: destination directory cannot be unset")
   128  	}
   129  
   130  	// TODO:UC20: preserve managed boot assets
   131  	preserveInDst, err := mapPreserve(whereDir, preserve)
   132  	if err != nil {
   133  		return fmt.Errorf("cannot map preserve entries for destination %q: %v", whereDir, err)
   134  	}
   135  
   136  	for _, c := range m.ps.Content {
   137  		if err := m.writeVolumeContent(whereDir, &c, preserveInDst); err != nil {
   138  			return fmt.Errorf("cannot write filesystem content of %s: %v", c, err)
   139  		}
   140  	}
   141  	return nil
   142  }
   143  
   144  // writeDirectory copies the source directory, or its contents under target
   145  // location dst. Follows rsync like semantics, that is:
   146  //   /foo/ -> /bar - writes contents of foo under /bar
   147  //   /foo  -> /bar - writes foo and its subtree under /bar
   148  func (m *MountedFilesystemWriter) writeDirectory(volumeRoot, src, dst string, preserveInDst []string) error {
   149  	hasDirSourceSlash := strings.HasSuffix(src, "/")
   150  
   151  	if err := checkSourceIsDir(src); err != nil {
   152  		return err
   153  	}
   154  
   155  	if !hasDirSourceSlash {
   156  		// /foo -> /bar (write foo and subtree)
   157  		dst = filepath.Join(dst, filepath.Base(src))
   158  	}
   159  
   160  	fis, err := ioutil.ReadDir(src)
   161  	if err != nil {
   162  		return fmt.Errorf("cannot list directory entries: %v", err)
   163  	}
   164  
   165  	for _, fi := range fis {
   166  		pSrc := filepath.Join(src, fi.Name())
   167  		pDst := filepath.Join(dst, fi.Name())
   168  
   169  		write := m.observedWriteFileOrSymlink
   170  		if fi.IsDir() {
   171  			if err := os.MkdirAll(pDst, 0755); err != nil {
   172  				return fmt.Errorf("cannot create directory prefix: %v", err)
   173  			}
   174  
   175  			write = m.writeDirectory
   176  			pSrc += "/"
   177  		}
   178  		if err := write(volumeRoot, pSrc, pDst, preserveInDst); err != nil {
   179  			return err
   180  		}
   181  	}
   182  
   183  	return nil
   184  }
   185  
   186  func (m *MountedFilesystemWriter) observedWriteFileOrSymlink(volumeRoot, src, dst string, preserveInDst []string) error {
   187  	if strings.HasSuffix(dst, "/") {
   188  		// write to directory
   189  		dst = filepath.Join(dst, filepath.Base(src))
   190  	}
   191  
   192  	data := &ContentChange{
   193  		// we are writing a new thing
   194  		Before: "",
   195  		// with content in this file
   196  		After: src,
   197  	}
   198  	act, err := observe(m.observer, ContentWrite, m.ps, volumeRoot, dst, data)
   199  	if err != nil {
   200  		return fmt.Errorf("cannot observe file write: %v", err)
   201  	}
   202  	if act == ChangeIgnore {
   203  		return nil
   204  	}
   205  	return writeFileOrSymlink(src, dst, preserveInDst)
   206  }
   207  
   208  // writeFileOrSymlink writes the source file or a symlink at given location or
   209  // under given directory. Follows rsync like semantics, that is:
   210  //   /foo -> /bar/ - writes foo as /bar/foo
   211  //   /foo  -> /bar - writes foo as /bar
   212  // The destination location is overwritten.
   213  func writeFileOrSymlink(src, dst string, preserveInDst []string) error {
   214  	if strings.HasSuffix(dst, "/") {
   215  		// write to directory
   216  		dst = filepath.Join(dst, filepath.Base(src))
   217  	}
   218  
   219  	if osutil.FileExists(dst) && strutil.SortedListContains(preserveInDst, dst) {
   220  		// entry shall be preserved
   221  		return nil
   222  	}
   223  
   224  	if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
   225  		return fmt.Errorf("cannot create prefix directory: %v", err)
   226  	}
   227  
   228  	if osutil.IsSymlink(src) {
   229  		// recreate the symlinks as they are
   230  		to, err := os.Readlink(src)
   231  		if err != nil {
   232  			return fmt.Errorf("cannot read symlink: %v", err)
   233  		}
   234  		if err := os.Symlink(to, dst); err != nil {
   235  			return fmt.Errorf("cannot write a symlink: %v", err)
   236  		}
   237  	} else {
   238  		// TODO try to preserve ownership and permission bits
   239  
   240  		// do not follow sylimks, dst is a reflection of the src which
   241  		// is a file
   242  		if err := osutil.AtomicWriteFileCopy(dst, src, 0); err != nil {
   243  			return fmt.Errorf("cannot copy %s: %v", src, err)
   244  		}
   245  	}
   246  	return nil
   247  }
   248  
   249  func (m *MountedFilesystemWriter) writeVolumeContent(volumeRoot string, content *VolumeContent, preserveInDst []string) error {
   250  	if err := checkContent(content); err != nil {
   251  		return err
   252  	}
   253  	// TODO: ResolvedSource() will already have resolved m.contentDir
   254  	realSource := filepath.Join(m.contentDir, content.ResolvedSource())
   255  	realTarget := filepath.Join(volumeRoot, content.Target)
   256  
   257  	// filepath trims the trailing /, restore if needed
   258  	if strings.HasSuffix(content.Target, "/") {
   259  		realTarget += "/"
   260  	}
   261  	if strings.HasSuffix(content.ResolvedSource(), "/") {
   262  		realSource += "/"
   263  	}
   264  
   265  	if osutil.IsDirectory(realSource) || strings.HasSuffix(content.ResolvedSource(), "/") {
   266  		// write a directory
   267  		return m.writeDirectory(volumeRoot, realSource, realTarget, preserveInDst)
   268  	} else {
   269  		// write a file
   270  		return m.observedWriteFileOrSymlink(volumeRoot, realSource, realTarget, preserveInDst)
   271  	}
   272  }
   273  
   274  func newStampFile(stamp string) (*osutil.AtomicFile, error) {
   275  	if err := os.MkdirAll(filepath.Dir(stamp), 0755); err != nil {
   276  		return nil, fmt.Errorf("cannot create stamp file prefix: %v", err)
   277  	}
   278  	return osutil.NewAtomicFile(stamp, 0644, 0, osutil.NoChown, osutil.NoChown)
   279  }
   280  
   281  func makeStamp(stamp string) error {
   282  	f, err := newStampFile(stamp)
   283  	if err != nil {
   284  		return err
   285  	}
   286  	return f.Commit()
   287  }
   288  
   289  type mountLookupFunc func(ps *LaidOutStructure) (string, error)
   290  
   291  // mountedFilesystemUpdater assists in applying updates to a mounted filesystem.
   292  //
   293  // The update process is composed of 2 main passes, and an optional rollback:
   294  //
   295  // 1) backup, where update data and current data is analyzed to identify
   296  // identical content, stamp files are created for entries that are to be
   297  // preserved, modified or otherwise touched by the update, that is, for existing
   298  // files that would be created/overwritten, ones that are explicitly listed as
   299  // preserved, or directories to be written to
   300  //
   301  // 2) update, where update data is written to the target location
   302  //
   303  // 3) rollback (optional), where update data is rolled back and replaced with
   304  // backup copies of files, newly created directories are removed
   305  type mountedFilesystemUpdater struct {
   306  	*MountedFilesystemWriter
   307  	backupDir      string
   308  	mountPoint     string
   309  	updateObserver ContentObserver
   310  }
   311  
   312  // newMountedFilesystemUpdater returns an updater for given filesystem
   313  // structure, with structure content coming from provided root directory. The
   314  // mount is located by calling a mount lookup helper. The backup directory
   315  // contains backup state information for use during rollback.
   316  func newMountedFilesystemUpdater(rootDir string, ps *LaidOutStructure, backupDir string, mountLookup mountLookupFunc, observer ContentObserver) (*mountedFilesystemUpdater, error) {
   317  	// avoid passing observer, writes will not be observed
   318  	fw, err := NewMountedFilesystemWriter(rootDir, ps, nil)
   319  	if err != nil {
   320  		return nil, err
   321  	}
   322  	if mountLookup == nil {
   323  		return nil, fmt.Errorf("internal error: mount lookup helper must be provided")
   324  	}
   325  	if backupDir == "" {
   326  		return nil, fmt.Errorf("internal error: backup directory must not be unset")
   327  	}
   328  	mount, err := mountLookup(ps)
   329  	if err != nil {
   330  		return nil, fmt.Errorf("cannot find mount location of structure %v: %v", ps, err)
   331  	}
   332  
   333  	fu := &mountedFilesystemUpdater{
   334  		MountedFilesystemWriter: fw,
   335  		backupDir:               backupDir,
   336  		mountPoint:              mount,
   337  		updateObserver:          observer,
   338  	}
   339  	return fu, nil
   340  }
   341  
   342  func fsStructBackupPath(backupDir string, ps *LaidOutStructure) string {
   343  	return filepath.Join(backupDir, fmt.Sprintf("struct-%v", ps.Index))
   344  }
   345  
   346  // entryDestPaths resolves destination and backup paths for given
   347  // source/target combination. Backup location is within provided
   348  // backup directory or empty if directory was not provided.
   349  func (f *mountedFilesystemUpdater) entryDestPaths(dstRoot, source, target, backupDir string) (dstPath, backupPath string) {
   350  	dstBasePath := target
   351  	if strings.HasSuffix(target, "/") {
   352  		// write to a directory
   353  		dstBasePath = filepath.Join(dstBasePath, filepath.Base(source))
   354  	}
   355  	dstPath = filepath.Join(dstRoot, dstBasePath)
   356  
   357  	if backupDir != "" {
   358  		backupPath = filepath.Join(backupDir, dstBasePath)
   359  	}
   360  
   361  	return dstPath, backupPath
   362  }
   363  
   364  // entrySourcePath returns the path of given source entry within the root
   365  // directory provided during initialization.
   366  func (f *mountedFilesystemUpdater) entrySourcePath(source string) string {
   367  	srcPath := filepath.Join(f.contentDir, source)
   368  
   369  	if strings.HasSuffix(source, "/") {
   370  		// restore trailing / if one was there
   371  		srcPath += "/"
   372  	}
   373  	return srcPath
   374  }
   375  
   376  // Update applies an update to a mounted filesystem. The caller must have
   377  // executed a Backup() before, to prepare a data set for rollback purpose.
   378  func (f *mountedFilesystemUpdater) Update() error {
   379  	preserveInDst, err := mapPreserve(f.mountPoint, f.ps.Update.Preserve)
   380  	if err != nil {
   381  		return fmt.Errorf("cannot map preserve entries for mount location %q: %v", f.mountPoint, err)
   382  	}
   383  
   384  	backupRoot := fsStructBackupPath(f.backupDir, f.ps)
   385  
   386  	skipped := 0
   387  	for _, c := range f.ps.Content {
   388  		if err := f.updateVolumeContent(f.mountPoint, &c, preserveInDst, backupRoot); err != nil {
   389  			if err == ErrNoUpdate {
   390  				skipped++
   391  				continue
   392  			}
   393  			return fmt.Errorf("cannot update content: %v", err)
   394  		}
   395  	}
   396  
   397  	if skipped == len(f.ps.Content) {
   398  		return ErrNoUpdate
   399  	}
   400  
   401  	return nil
   402  }
   403  
   404  func (f *mountedFilesystemUpdater) sourceDirectoryEntries(source string) ([]os.FileInfo, error) {
   405  	srcPath := f.entrySourcePath(source)
   406  
   407  	if err := checkSourceIsDir(srcPath); err != nil {
   408  		return nil, err
   409  	}
   410  
   411  	// TODO: enable support for symlinks when needed
   412  	if osutil.IsSymlink(srcPath) {
   413  		return nil, fmt.Errorf("source is a symbolic link")
   414  	}
   415  
   416  	return ioutil.ReadDir(srcPath)
   417  }
   418  
   419  // targetInSourceDir resolves the actual target for given source directory name
   420  // and target specification.
   421  //  source: /foo/bar/ target: /baz => /bar/    (contents of /foo/bar/ under /baz)
   422  //  source: /foo/bar  target: /baz => /bar/bar (directory /foo/bar under /baz, contents under /baz/bar)
   423  func targetForSourceDir(source, target string) string {
   424  	if strings.HasSuffix(source, "/") {
   425  		// contents of source directory land under target
   426  		return target
   427  	}
   428  	// source directory lands under target
   429  	return filepath.Join(target, filepath.Base(source))
   430  }
   431  
   432  func (f *mountedFilesystemUpdater) updateDirectory(dstRoot, source, target string, preserveInDst []string, backupDir string) error {
   433  	fis, err := f.sourceDirectoryEntries(source)
   434  	if err != nil {
   435  		return fmt.Errorf("cannot list source directory %q: %v", source, err)
   436  	}
   437  
   438  	target = targetForSourceDir(source, target)
   439  
   440  	// create current target directory if needed
   441  	if err := os.MkdirAll(filepath.Join(dstRoot, target), 0755); err != nil {
   442  		return fmt.Errorf("cannot write directory: %v", err)
   443  	}
   444  	// and write the content of source to target
   445  	skipped := 0
   446  	for _, fi := range fis {
   447  		pSrc := filepath.Join(source, fi.Name())
   448  		pDst := filepath.Join(target, fi.Name())
   449  
   450  		update := f.updateOrSkipFile
   451  		if fi.IsDir() {
   452  			// continue updating contents of the directory rather
   453  			// than the directory itself
   454  			pSrc += "/"
   455  			pDst += "/"
   456  			update = f.updateDirectory
   457  		}
   458  		if err := update(dstRoot, pSrc, pDst, preserveInDst, backupDir); err != nil {
   459  			if err == ErrNoUpdate {
   460  				skipped++
   461  				continue
   462  			}
   463  			return err
   464  		}
   465  	}
   466  
   467  	if skipped == len(fis) {
   468  		return ErrNoUpdate
   469  	}
   470  
   471  	return nil
   472  }
   473  
   474  func (f *mountedFilesystemUpdater) updateOrSkipFile(dstRoot, source, target string, preserveInDst []string, backupDir string) error {
   475  	srcPath := f.entrySourcePath(source)
   476  	dstPath, backupPath := f.entryDestPaths(dstRoot, source, target, backupDir)
   477  	backupName := backupPath + ".backup"
   478  	sameStamp := backupPath + ".same"
   479  	preserveStamp := backupPath + ".preserve"
   480  	ignoreStamp := backupPath + ".ignore"
   481  
   482  	// TODO: enable support for symlinks when needed
   483  	if osutil.IsSymlink(srcPath) {
   484  		return fmt.Errorf("cannot update file %s: symbolic links are not supported", source)
   485  	}
   486  
   487  	if osutil.FileExists(ignoreStamp) {
   488  		// explicitly ignored by request of the observer
   489  		return ErrNoUpdate
   490  	}
   491  
   492  	if osutil.FileExists(dstPath) {
   493  		if strutil.SortedListContains(preserveInDst, dstPath) || osutil.FileExists(preserveStamp) {
   494  			// file is to be preserved
   495  			return ErrNoUpdate
   496  		}
   497  		if osutil.FileExists(sameStamp) {
   498  			// file is the same as current copy
   499  			return ErrNoUpdate
   500  		}
   501  		if !osutil.FileExists(backupName) {
   502  			// not preserved & different than the update, error out
   503  			// as there is no backup
   504  			return fmt.Errorf("missing backup file %q for %v", backupName, target)
   505  		}
   506  	}
   507  
   508  	return writeFileOrSymlink(srcPath, dstPath, preserveInDst)
   509  }
   510  
   511  func (f *mountedFilesystemUpdater) updateVolumeContent(volumeRoot string, content *VolumeContent, preserveInDst []string, backupDir string) error {
   512  	if err := checkContent(content); err != nil {
   513  		return err
   514  	}
   515  
   516  	// TODO: ResolvedSource() will already have resolved f.entrySourcePath
   517  	srcPath := f.entrySourcePath(content.ResolvedSource())
   518  
   519  	if osutil.IsDirectory(srcPath) || strings.HasSuffix(content.ResolvedSource(), "/") {
   520  		// TODO: pass both Unresolved and resolved Source (unresolved for better error reporting)
   521  		return f.updateDirectory(volumeRoot, content.ResolvedSource(), content.Target, preserveInDst, backupDir)
   522  	} else {
   523  		// TODO: pass both Unresolved and resolved Source (unresolved for better error reporting)
   524  		return f.updateOrSkipFile(volumeRoot, content.ResolvedSource(), content.Target, preserveInDst, backupDir)
   525  	}
   526  }
   527  
   528  // Backup analyzes a mounted filesystem and prepares a rollback state should the
   529  // update be applied. The content of the filesystem is processed, files and
   530  // directories that would be modified by the update are backed up, while
   531  // identical/preserved files may be stamped to improve the later step of update
   532  // process.
   533  //
   534  // The backup directory structure mirrors the structure of destination
   535  // location. Given the following destination structure:
   536  //
   537  // foo
   538  // ├── a
   539  // ├── b
   540  // ├── bar
   541  // │   ├── baz
   542  // │   │   └── d
   543  // │   └── z
   544  // ├── c
   545  // └── d
   546  //
   547  // The structure of backup looks like this:
   548  //
   549  // foo-backup
   550  // ├── a.backup           <-- backup copy of ./a
   551  // ├── bar
   552  // │   ├── baz
   553  // │   │   └── d.backup   <-- backup copy of ./bar/baz/d
   554  // │   └── baz.backup     <-- stamp indicating ./bar/baz existed before the update
   555  // ├── bar.backup         <-- stamp indicating ./bar existed before the update
   556  // ├── b.same             <-- stamp indicating ./b is identical to the update data
   557  // ├── c.ignore           <-- stamp indicating change to ./c was requested to be ignored
   558  // └── d.preserve         <-- stamp indicating ./d is to be preserved
   559  //
   560  func (f *mountedFilesystemUpdater) Backup() error {
   561  	backupRoot := fsStructBackupPath(f.backupDir, f.ps)
   562  
   563  	if err := os.MkdirAll(backupRoot, 0755); err != nil {
   564  		return fmt.Errorf("cannot create backup directory: %v", err)
   565  	}
   566  
   567  	preserveInDst, err := mapPreserve(f.mountPoint, f.ps.Update.Preserve)
   568  	if err != nil {
   569  		return fmt.Errorf("cannot map preserve entries for mount location %q: %v", f.mountPoint, err)
   570  	}
   571  
   572  	for _, c := range f.ps.Content {
   573  		if err := f.backupVolumeContent(f.mountPoint, &c, preserveInDst, backupRoot); err != nil {
   574  			return fmt.Errorf("cannot backup content: %v", err)
   575  		}
   576  	}
   577  
   578  	return nil
   579  }
   580  
   581  func (f *mountedFilesystemUpdater) backupOrCheckpointDirectory(dstRoot, source, target string, preserveInDst []string, backupDir string) error {
   582  	fis, err := f.sourceDirectoryEntries(source)
   583  	if err != nil {
   584  		return fmt.Errorf("cannot backup directory %q: %v", source, err)
   585  	}
   586  
   587  	target = targetForSourceDir(source, target)
   588  
   589  	for _, fi := range fis {
   590  		pSrc := filepath.Join(source, fi.Name())
   591  		pDst := filepath.Join(target, fi.Name())
   592  
   593  		backup := f.observedBackupOrCheckpointFile
   594  		if fi.IsDir() {
   595  			// continue backing up the contents of the directory
   596  			// rather than the directory itself
   597  			pSrc += "/"
   598  			pDst += "/"
   599  			backup = f.backupOrCheckpointDirectory
   600  		}
   601  		if err := f.checkpointPrefix(dstRoot, pDst, backupDir); err != nil {
   602  			return err
   603  		}
   604  		if err := backup(dstRoot, pSrc, pDst, preserveInDst, backupDir); err != nil {
   605  			return err
   606  		}
   607  	}
   608  
   609  	return nil
   610  }
   611  
   612  // checkpointPrefix creates stamps for each part of the destination prefix that exists
   613  func (f *mountedFilesystemUpdater) checkpointPrefix(dstRoot, target string, backupDir string) error {
   614  	// check how much of the prefix needs to be created
   615  	for prefix := filepath.Dir(target); prefix != "." && prefix != "/"; prefix = filepath.Dir(prefix) {
   616  		prefixDst, prefixBackupBase := f.entryDestPaths(dstRoot, "", prefix, backupDir)
   617  
   618  		// TODO: enable support for symlinks when needed
   619  		if osutil.IsSymlink(prefixDst) {
   620  			return fmt.Errorf("cannot create a checkpoint for directory %v: symbolic links are not supported", prefix)
   621  		}
   622  
   623  		prefixBackupName := prefixBackupBase + ".backup"
   624  		if osutil.FileExists(prefixBackupName) {
   625  			continue
   626  		}
   627  		if !osutil.IsDirectory(prefixDst) {
   628  			// does not exist now, will be created on the fly and
   629  			// removed during rollback
   630  			continue
   631  		}
   632  		if err := os.MkdirAll(filepath.Dir(prefixBackupName), 0755); err != nil {
   633  			return fmt.Errorf("cannot create backup prefix: %v", err)
   634  		}
   635  		if err := makeStamp(prefixBackupName); err != nil {
   636  			return fmt.Errorf("cannot create a checkpoint for directory: %v", err)
   637  		}
   638  	}
   639  	return nil
   640  }
   641  
   642  func (f *mountedFilesystemUpdater) ignoreChange(change *ContentChange, backupPath string) error {
   643  	preserveStamp := backupPath + ".preserve"
   644  	backupName := backupPath + ".backup"
   645  	sameStamp := backupPath + ".same"
   646  	ignoreStamp := backupPath + ".ignore"
   647  	if err := makeStamp(ignoreStamp); err != nil {
   648  		return fmt.Errorf("cannot create a checkpoint file: %v", err)
   649  	}
   650  	for _, name := range []string{backupName, sameStamp, preserveStamp} {
   651  		if err := os.Remove(name); err != nil && !os.IsNotExist(err) {
   652  			return fmt.Errorf("cannot remove existing stamp file: %v", err)
   653  		}
   654  	}
   655  	return nil
   656  }
   657  
   658  func (f *mountedFilesystemUpdater) observedBackupOrCheckpointFile(dstRoot, source, target string, preserveInDst []string, backupDir string) error {
   659  	change, err := f.backupOrCheckpointFile(dstRoot, source, target, preserveInDst, backupDir)
   660  	if err != nil {
   661  		return err
   662  	}
   663  	if change != nil {
   664  		dstPath, backupPath := f.entryDestPaths(dstRoot, source, target, backupDir)
   665  		act, err := observe(f.updateObserver, ContentUpdate, f.ps, f.mountPoint, dstPath, change)
   666  		if err != nil {
   667  			return fmt.Errorf("cannot observe pending file write %v\n", err)
   668  		}
   669  		if act == ChangeIgnore {
   670  			// observer asked for the change to be ignored
   671  			if err := f.ignoreChange(change, backupPath); err != nil {
   672  				return fmt.Errorf("cannot ignore content change: %v", err)
   673  			}
   674  		}
   675  	}
   676  	return nil
   677  }
   678  
   679  // backupOrCheckpointFile analyzes a given source file from the gadget and a
   680  // target location under the provided destination root directory. When both
   681  // files are identical, creates a stamp that allows the update to skip the file.
   682  // When content of the new file is different, a backup of the original file is
   683  // created. Returns a content change if a file will be written by the update
   684  // pass.
   685  func (f *mountedFilesystemUpdater) backupOrCheckpointFile(dstRoot, source, target string, preserveInDst []string, backupDir string) (change *ContentChange, err error) {
   686  	srcPath := f.entrySourcePath(source)
   687  	dstPath, backupPath := f.entryDestPaths(dstRoot, source, target, backupDir)
   688  
   689  	backupName := backupPath + ".backup"
   690  	sameStamp := backupPath + ".same"
   691  	preserveStamp := backupPath + ".preserve"
   692  	ignoreStamp := backupPath + ".ignore"
   693  
   694  	changeWithBackup := &ContentChange{
   695  		// content of the new data
   696  		After: srcPath,
   697  		// this is the original data that was present before the
   698  		// update
   699  		Before: backupName,
   700  	}
   701  	changeNewFile := &ContentChange{
   702  		// content of the new data
   703  		After: srcPath,
   704  	}
   705  
   706  	if osutil.FileExists(ignoreStamp) {
   707  		// observer already requested the change to the target location
   708  		// to be ignored
   709  		return nil, nil
   710  	}
   711  
   712  	// TODO: enable support for symlinks when needed
   713  	if osutil.IsSymlink(dstPath) {
   714  		return nil, fmt.Errorf("cannot backup file %s: symbolic links are not supported", target)
   715  	}
   716  
   717  	if !osutil.FileExists(dstPath) {
   718  		// destination does not exist and will be created when writing
   719  		// the udpate, no need for backup
   720  		return changeNewFile, nil
   721  	}
   722  	// destination file exists beyond this point
   723  
   724  	if osutil.FileExists(backupName) {
   725  		// file already checked and backed up
   726  		return changeWithBackup, nil
   727  	}
   728  	if osutil.FileExists(sameStamp) {
   729  		// file already checked, same as the update, move on
   730  		return nil, nil
   731  	}
   732  	// TODO: correctly identify new files that were written by a partially
   733  	// executed update pass
   734  
   735  	if strutil.SortedListContains(preserveInDst, dstPath) {
   736  		if osutil.FileExists(preserveStamp) {
   737  			// already stamped
   738  			return nil, nil
   739  		}
   740  		// make a stamp
   741  		if err := makeStamp(preserveStamp); err != nil {
   742  			return nil, fmt.Errorf("cannot create preserve stamp: %v", err)
   743  		}
   744  		return nil, nil
   745  	}
   746  
   747  	// try to find out whether the update and the existing file are
   748  	// identical
   749  
   750  	orig, err := os.Open(dstPath)
   751  	if err != nil {
   752  		return nil, fmt.Errorf("cannot open destination file: %v", err)
   753  	}
   754  
   755  	// backup of the original content
   756  	backup, err := newStampFile(backupName)
   757  	if err != nil {
   758  		return nil, fmt.Errorf("cannot create backup file: %v", err)
   759  	}
   760  	// becomes a backup copy or a noop if canceled
   761  	defer backup.Commit()
   762  
   763  	// checksum the original data while it's being copied
   764  	origHash := crypto.SHA1.New()
   765  	htr := io.TeeReader(orig, origHash)
   766  
   767  	_, err = io.Copy(backup, htr)
   768  	if err != nil {
   769  		backup.Cancel()
   770  		return nil, fmt.Errorf("cannot backup original file: %v", err)
   771  	}
   772  
   773  	// digest of the update
   774  	updateDigest, _, err := osutil.FileDigest(srcPath, crypto.SHA1)
   775  	if err != nil {
   776  		backup.Cancel()
   777  		return nil, fmt.Errorf("cannot checksum update file: %v", err)
   778  	}
   779  	// digest of the currently present data
   780  	origDigest := origHash.Sum(nil)
   781  
   782  	// TODO: look into comparing the streams directly
   783  	if bytes.Equal(origDigest, updateDigest) {
   784  		// mark that files are identical and update can be skipped, no
   785  		// backup is needed
   786  		if err := makeStamp(sameStamp); err != nil {
   787  			return nil, fmt.Errorf("cannot create a checkpoint file: %v", err)
   788  		}
   789  
   790  		// makes the deferred commit a noop
   791  		backup.Cancel()
   792  		return nil, nil
   793  	}
   794  
   795  	// update will overwrite existing file, a backup copy is created on
   796  	// Commit()
   797  	return changeWithBackup, nil
   798  }
   799  
   800  func (f *mountedFilesystemUpdater) backupVolumeContent(volumeRoot string, content *VolumeContent, preserveInDst []string, backupDir string) error {
   801  	if err := checkContent(content); err != nil {
   802  		return err
   803  	}
   804  
   805  	// TODO: ResolvedSource() will already have resolved f.entrySourcePath
   806  	srcPath := f.entrySourcePath(content.ResolvedSource())
   807  
   808  	if err := f.checkpointPrefix(volumeRoot, content.Target, backupDir); err != nil {
   809  		return err
   810  	}
   811  	if osutil.IsDirectory(srcPath) || strings.HasSuffix(content.ResolvedSource(), "/") {
   812  		// backup directory contents
   813  		// TODO: pass both Unresolved and resolved Source (unresolved for better error reporting)
   814  		return f.backupOrCheckpointDirectory(volumeRoot, content.ResolvedSource(), content.Target, preserveInDst, backupDir)
   815  	} else {
   816  		// backup a file
   817  		return f.observedBackupOrCheckpointFile(volumeRoot, content.ResolvedSource(), content.Target, preserveInDst, backupDir)
   818  	}
   819  }
   820  
   821  // Rollback attempts to revert changes done by the update step, using state
   822  // information collected during backup phase. Files that were modified by the
   823  // update are stored from their backup copies, newly added directories are
   824  // removed.
   825  func (f *mountedFilesystemUpdater) Rollback() error {
   826  	backupRoot := fsStructBackupPath(f.backupDir, f.ps)
   827  
   828  	preserveInDst, err := mapPreserve(f.mountPoint, f.ps.Update.Preserve)
   829  	if err != nil {
   830  		return fmt.Errorf("cannot map preserve entries for mount location %q: %v", f.mountPoint, err)
   831  	}
   832  
   833  	for _, c := range f.ps.Content {
   834  		if err := f.rollbackVolumeContent(f.mountPoint, &c, preserveInDst, backupRoot); err != nil {
   835  			return fmt.Errorf("cannot rollback content: %v", err)
   836  		}
   837  	}
   838  	return nil
   839  }
   840  
   841  func (f *mountedFilesystemUpdater) rollbackPrefix(dstRoot, target string, backupDir string) error {
   842  	for prefix := filepath.Dir(target); prefix != "/" && prefix != "."; prefix = filepath.Dir(prefix) {
   843  		prefixDstPath, prefixBackupPath := f.entryDestPaths(dstRoot, "", prefix, backupDir)
   844  		if !osutil.FileExists(prefixBackupPath + ".backup") {
   845  			// try remove
   846  			if err := os.Remove(prefixDstPath); err != nil {
   847  				logger.Noticef("cannot remove gadget directory %q: %v", prefix, err)
   848  			}
   849  		}
   850  	}
   851  	return nil
   852  }
   853  
   854  func (f *mountedFilesystemUpdater) rollbackDirectory(dstRoot, source, target string, preserveInDst []string, backupDir string) error {
   855  	fis, err := f.sourceDirectoryEntries(source)
   856  	if err != nil {
   857  		return fmt.Errorf("cannot rollback directory %q: %v", source, err)
   858  	}
   859  
   860  	target = targetForSourceDir(source, target)
   861  
   862  	for _, fi := range fis {
   863  		pSrc := filepath.Join(source, fi.Name())
   864  		pDst := filepath.Join(target, fi.Name())
   865  
   866  		rollback := f.rollbackFile
   867  		if fi.IsDir() {
   868  			// continue rolling back the contents of the directory
   869  			// rather than the directory itself
   870  			rollback = f.rollbackDirectory
   871  			pSrc += "/"
   872  			pDst += "/"
   873  		}
   874  		if err := rollback(dstRoot, pSrc, pDst, preserveInDst, backupDir); err != nil {
   875  			return err
   876  		}
   877  		if err := f.rollbackPrefix(dstRoot, pDst, backupDir); err != nil {
   878  			return err
   879  		}
   880  	}
   881  
   882  	return nil
   883  }
   884  
   885  func (f *mountedFilesystemUpdater) rollbackFile(dstRoot, source, target string, preserveInDst []string, backupDir string) error {
   886  	srcPath := f.entrySourcePath(source)
   887  	dstPath, backupPath := f.entryDestPaths(dstRoot, source, target, backupDir)
   888  
   889  	backupName := backupPath + ".backup"
   890  	sameStamp := backupPath + ".same"
   891  	preserveStamp := backupPath + ".preserve"
   892  	ignoreStamp := backupPath + ".ignore"
   893  
   894  	if strutil.SortedListContains(preserveInDst, dstPath) && osutil.FileExists(preserveStamp) {
   895  		// file was preserved at original location by being
   896  		// explicitly listed
   897  		return nil
   898  	}
   899  	if osutil.FileExists(sameStamp) {
   900  		// contents are the same as original, do nothing
   901  		return nil
   902  	}
   903  	if osutil.FileExists(ignoreStamp) {
   904  		// observer requested the changes to the target to be ignored
   905  		// previously
   906  		return nil
   907  	}
   908  
   909  	data := &ContentChange{
   910  		After: srcPath,
   911  		// original content was in the backup file
   912  		Before: backupName,
   913  	}
   914  
   915  	if osutil.FileExists(backupName) {
   916  		// restore backup -> destination
   917  		if err := writeFileOrSymlink(backupName, dstPath, nil); err != nil {
   918  			return err
   919  		}
   920  	} else {
   921  		if err := os.Remove(dstPath); err != nil && !os.IsNotExist(err) {
   922  			return fmt.Errorf("cannot remove written update: %v", err)
   923  		}
   924  		// since it's a new file, there was no original content
   925  		data.Before = ""
   926  	}
   927  	// avoid passing source path during rollback, the file has been restored
   928  	// to the disk already
   929  	_, err := observe(f.updateObserver, ContentRollback, f.ps, f.mountPoint, dstPath, data)
   930  	if err != nil {
   931  		return fmt.Errorf("cannot observe pending file rollback %v\n", err)
   932  	}
   933  
   934  	return nil
   935  }
   936  
   937  func (f *mountedFilesystemUpdater) rollbackVolumeContent(volumeRoot string, content *VolumeContent, preserveInDst []string, backupDir string) error {
   938  	if err := checkContent(content); err != nil {
   939  		return err
   940  	}
   941  
   942  	// TODO: ResolvedSource() will already have resolved f.entrySourcePath
   943  	srcPath := f.entrySourcePath(content.ResolvedSource())
   944  
   945  	var err error
   946  	if osutil.IsDirectory(srcPath) || strings.HasSuffix(content.ResolvedSource(), "/") {
   947  		// rollback directory
   948  		err = f.rollbackDirectory(volumeRoot, content.ResolvedSource(), content.Target, preserveInDst, backupDir)
   949  	} else {
   950  		// rollback file
   951  		err = f.rollbackFile(volumeRoot, content.ResolvedSource(), content.Target, preserveInDst, backupDir)
   952  	}
   953  	if err != nil {
   954  		return err
   955  	}
   956  
   957  	return f.rollbackPrefix(volumeRoot, content.Target, backupDir)
   958  }