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