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