github.com/stolowski/snapd@v0.0.0-20210407085831-115137ce5a22/osutil/synctree.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2019 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  	"fmt"
    24  	"os"
    25  	"path/filepath"
    26  	"sort"
    27  	"syscall"
    28  )
    29  
    30  func appendWithPrefix(paths []string, prefix string, filenames []string) []string {
    31  	for _, filename := range filenames {
    32  		paths = append(paths, filepath.Join(prefix, filename))
    33  	}
    34  	return paths
    35  }
    36  
    37  func removeEmptyDirs(baseDir, relPath string) error {
    38  	for relPath != "." {
    39  		if err := os.Remove(filepath.Join(baseDir, relPath)); err != nil {
    40  			// If the directory doesn't exist, then stop.
    41  			if os.IsNotExist(err) {
    42  				return nil
    43  			}
    44  			// If the directory is not empty, then stop.
    45  			if pathErr, ok := err.(*os.PathError); ok && pathErr.Err == syscall.ENOTEMPTY {
    46  				return nil
    47  			}
    48  			return err
    49  		}
    50  		relPath = filepath.Dir(relPath)
    51  	}
    52  	return nil
    53  }
    54  
    55  func matchAnyComponent(globs []string, path string) (ok bool, index int) {
    56  	for path != "." {
    57  		component := filepath.Base(path)
    58  		if ok, index, _ = matchAny(globs, component); ok {
    59  			return ok, index
    60  		}
    61  		path = filepath.Dir(path)
    62  	}
    63  	return false, 0
    64  }
    65  
    66  // EnsureTreeState ensures that a directory tree content matches expectations.
    67  //
    68  // EnsureTreeState walks subdirectories of the base directory, and
    69  // uses EnsureDirStateGlobs to synchronise content with the
    70  // corresponding entry in the content map.  Any non-existent
    71  // subdirectories in the content map will be created.
    72  //
    73  // After synchronising all subdirectories, any subdirectories where
    74  // files were removed that are now empty will itself be removed, plus
    75  // its parent directories up to but not including the base directory.
    76  //
    77  // While there is a sanity check to prevent creation of directories
    78  // that match the file glob pattern, it is the caller's responsibility
    79  // to not create directories that may match globs passed to other
    80  // invocations.
    81  //
    82  // For example, if the glob "snap.$SNAP_NAME.*" is used then the
    83  // caller should avoid trying to populate any directories matching
    84  // "snap.*".
    85  //
    86  // If an error occurs, all matching files are removed from the tree.
    87  //
    88  // A list of changed and removed files is returned, as relative paths
    89  // to the base directory.
    90  func EnsureTreeState(baseDir string, globs []string, content map[string]map[string]FileState) (changed, removed []string, err error) {
    91  	// Sanity check globs before doing anything
    92  	if _, index, err := matchAny(globs, "foo"); err != nil {
    93  		return nil, nil, fmt.Errorf("internal error: EnsureTreeState got invalid pattern %q: %s", globs[index], err)
    94  	}
    95  	// Sanity check directory paths and file names in content dict
    96  	for relPath, dirContent := range content {
    97  		if filepath.IsAbs(relPath) {
    98  			return nil, nil, fmt.Errorf("internal error: EnsureTreeState got absolute directory %q", relPath)
    99  		}
   100  		if ok, index := matchAnyComponent(globs, relPath); ok {
   101  			return nil, nil, fmt.Errorf("internal error: EnsureTreeState got path %q that matches glob pattern %q", relPath, globs[index])
   102  		}
   103  		for baseName := range dirContent {
   104  			if filepath.Base(baseName) != baseName {
   105  				return nil, nil, fmt.Errorf("internal error: EnsureTreeState got filename %q in %q, which has a path component", baseName, relPath)
   106  			}
   107  			if ok, _, _ := matchAny(globs, baseName); !ok {
   108  				return nil, nil, fmt.Errorf("internal error: EnsureTreeState got filename %q in %q, which doesn't match any glob patterns %q", baseName, relPath, globs)
   109  			}
   110  		}
   111  	}
   112  	// Find all existing subdirectories under the base dir.  Don't
   113  	// perform any modifications here because, as it may confuse
   114  	// Walk().
   115  	subdirs := make(map[string]bool)
   116  	err = filepath.Walk(baseDir, func(path string, fileInfo os.FileInfo, err error) error {
   117  		if err != nil {
   118  			return err
   119  		}
   120  		if !fileInfo.IsDir() {
   121  			return nil
   122  		}
   123  		relPath, err := filepath.Rel(baseDir, path)
   124  		if err != nil {
   125  			return err
   126  		}
   127  		subdirs[relPath] = true
   128  		return nil
   129  	})
   130  	if err != nil {
   131  		return nil, nil, err
   132  	}
   133  	// Ensure we process directories listed in content
   134  	for relPath := range content {
   135  		subdirs[relPath] = true
   136  	}
   137  
   138  	maybeEmpty := []string{}
   139  
   140  	var firstErr error
   141  	for relPath := range subdirs {
   142  		dirContent := content[relPath]
   143  		path := filepath.Join(baseDir, relPath)
   144  		if err := os.MkdirAll(path, 0755); err != nil {
   145  			firstErr = err
   146  			break
   147  		}
   148  		dirChanged, dirRemoved, err := EnsureDirStateGlobs(path, globs, dirContent)
   149  		changed = appendWithPrefix(changed, relPath, dirChanged)
   150  		removed = appendWithPrefix(removed, relPath, dirRemoved)
   151  		if err != nil {
   152  			firstErr = err
   153  			break
   154  		}
   155  		if len(removed) != 0 {
   156  			maybeEmpty = append(maybeEmpty, relPath)
   157  		}
   158  	}
   159  	// As with EnsureDirState, if an error occurred we want to
   160  	// delete all matching files under the whole baseDir
   161  	// hierarchy.  This also means emptying subdirectories that
   162  	// were successfully synchronised.
   163  	if firstErr != nil {
   164  		// changed paths will be deleted by this next step
   165  		changed = nil
   166  		for relPath := range subdirs {
   167  			path := filepath.Join(baseDir, relPath)
   168  			if !IsDirectory(path) {
   169  				continue
   170  			}
   171  			_, dirRemoved, _ := EnsureDirStateGlobs(path, globs, nil)
   172  			removed = appendWithPrefix(removed, relPath, dirRemoved)
   173  			if len(removed) != 0 {
   174  				maybeEmpty = append(maybeEmpty, relPath)
   175  			}
   176  		}
   177  	}
   178  	sort.Strings(changed)
   179  	sort.Strings(removed)
   180  
   181  	// For directories where files were removed, attempt to remove
   182  	// empty directories.
   183  	for _, relPath := range maybeEmpty {
   184  		if err := removeEmptyDirs(baseDir, relPath); err != nil {
   185  			if firstErr != nil {
   186  				firstErr = err
   187  			}
   188  		}
   189  	}
   190  	return changed, removed, firstErr
   191  }