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 }