github.com/stolowski/snapd@v0.0.0-20210407085831-115137ce5a22/osutil/syncdir.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2016 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 "bytes" 24 "fmt" 25 "io" 26 "io/ioutil" 27 "os" 28 "path/filepath" 29 "sort" 30 ) 31 32 // FileState is an interface for conveying the desired state of a some file. 33 type FileState interface { 34 State() (reader io.ReadCloser, size int64, mode os.FileMode, err error) 35 } 36 37 // FileReference describes the desired content by referencing an existing file. 38 type FileReference struct { 39 Path string 40 } 41 42 // State returns a reader of the referenced file, along with other meta-data. 43 func (fref FileReference) State() (io.ReadCloser, int64, os.FileMode, error) { 44 file, err := os.Open(fref.Path) 45 if err != nil { 46 return nil, 0, os.FileMode(0), err 47 } 48 fi, err := file.Stat() 49 if err != nil { 50 return nil, 0, os.FileMode(0), err 51 } 52 return file, fi.Size(), fi.Mode(), nil 53 } 54 55 // FileReferencePlusMode describes the desired content by referencing an existing file and providing custom mode. 56 type FileReferencePlusMode struct { 57 FileReference 58 Mode os.FileMode 59 } 60 61 // State returns a reader of the referenced file, substituting the mode. 62 func (fcref FileReferencePlusMode) State() (io.ReadCloser, int64, os.FileMode, error) { 63 reader, size, _, err := fcref.FileReference.State() 64 if err != nil { 65 return nil, 0, os.FileMode(0), err 66 } 67 return reader, size, fcref.Mode, nil 68 } 69 70 // MemoryFileState describes the desired content by providing an in-memory copy. 71 type MemoryFileState struct { 72 Content []byte 73 Mode os.FileMode 74 } 75 76 // State returns a reader of the in-memory contents of a file, along with other meta-data. 77 func (blob *MemoryFileState) State() (io.ReadCloser, int64, os.FileMode, error) { 78 return ioutil.NopCloser(bytes.NewReader(blob.Content)), int64(len(blob.Content)), blob.Mode, nil 79 } 80 81 // ErrSameState is returned when the state of a file has not changed. 82 var ErrSameState = fmt.Errorf("file state has not changed") 83 84 // EnsureDirStateGlobs ensures that directory content matches expectations. 85 // 86 // EnsureDirStateGlobs enumerates all the files in the specified directory that 87 // match the provided set of pattern (globs). Each enumerated file is checked 88 // to ensure that the contents, permissions are what is desired. Unexpected 89 // files are removed. Missing files are created and differing files are 90 // corrected. Files not matching any pattern are ignored. 91 // 92 // Note that EnsureDirStateGlobs only checks for permissions and content. Other 93 // security mechanisms, including file ownership and extended attributes are 94 // *not* supported. 95 // 96 // The content map describes each of the files that are intended to exist in 97 // the directory. Map keys must be file names relative to the directory. 98 // Sub-directories in the name are not allowed. 99 // 100 // If writing any of the files fails, EnsureDirStateGlobs switches to erase mode 101 // where *all* of the files managed by the glob pattern are removed (including 102 // those that may have been already written). The return value is an empty list 103 // of changed files, the real list of removed files and the first error. 104 // 105 // If an error happens while removing files then such a file is not removed but 106 // the removal continues until the set of managed files matching the glob is 107 // exhausted. 108 // 109 // In all cases, the function returns the first error it has encountered. 110 func EnsureDirStateGlobs(dir string, globs []string, content map[string]FileState) (changed, removed []string, err error) { 111 // Check syntax before doing anything. 112 if _, index, err := matchAny(globs, "foo"); err != nil { 113 return nil, nil, fmt.Errorf("internal error: EnsureDirState got invalid pattern %q: %s", globs[index], err) 114 } 115 for baseName := range content { 116 if filepath.Base(baseName) != baseName { 117 return nil, nil, fmt.Errorf("internal error: EnsureDirState got filename %q which has a path component", baseName) 118 } 119 if ok, _, _ := matchAny(globs, baseName); !ok { 120 if len(globs) == 1 { 121 return nil, nil, fmt.Errorf("internal error: EnsureDirState got filename %q which doesn't match the glob pattern %q", baseName, globs[0]) 122 } 123 return nil, nil, fmt.Errorf("internal error: EnsureDirState got filename %q which doesn't match any glob patterns %q", baseName, globs) 124 } 125 } 126 // Change phase (create/change files described by content) 127 var firstErr error 128 for baseName, fileState := range content { 129 filePath := filepath.Join(dir, baseName) 130 err := EnsureFileState(filePath, fileState) 131 if err == ErrSameState { 132 continue 133 } 134 if err != nil { 135 // On write failure, switch to erase mode. Desired content is set 136 // to nothing (no content) changed files are forgotten and the 137 // writing loop stops. The subsequent erase loop will remove all 138 // the managed content. 139 firstErr = err 140 content = nil 141 changed = nil 142 break 143 } 144 changed = append(changed, baseName) 145 } 146 // Delete phase (remove files matching the glob that are not in content) 147 matches := make(map[string]bool) 148 for _, glob := range globs { 149 m, err := filepath.Glob(filepath.Join(dir, glob)) 150 if err != nil { 151 sort.Strings(changed) 152 return changed, nil, err 153 } 154 for _, path := range m { 155 matches[path] = true 156 } 157 } 158 159 for path := range matches { 160 baseName := filepath.Base(path) 161 if content[baseName] != nil { 162 continue 163 } 164 err := os.Remove(path) 165 if err != nil { 166 if firstErr == nil { 167 firstErr = err 168 } 169 continue 170 } 171 removed = append(removed, baseName) 172 } 173 sort.Strings(changed) 174 sort.Strings(removed) 175 return changed, removed, firstErr 176 } 177 178 func matchAny(globs []string, path string) (ok bool, index int, err error) { 179 for index, glob := range globs { 180 if ok, err := filepath.Match(glob, path); ok || err != nil { 181 return ok, index, err 182 } 183 } 184 return false, 0, nil 185 } 186 187 // EnsureDirState ensures that directory content matches expectations. 188 // 189 // This is like EnsureDirStateGlobs but it only supports one glob at a time. 190 func EnsureDirState(dir string, glob string, content map[string]FileState) (changed, removed []string, err error) { 191 return EnsureDirStateGlobs(dir, []string{glob}, content) 192 } 193 194 // fileStateEqualTo returns whether the file exists in the expected state. 195 func fileStateEqualTo(filePath string, state FileState) (bool, error) { 196 other := &FileReference{Path: filePath} 197 198 // Open views to both files so that we can compare them. 199 readerA, sizeA, modeA, err := state.State() 200 if err != nil { 201 return false, err 202 } 203 defer readerA.Close() 204 205 readerB, sizeB, modeB, err := other.State() 206 if err != nil { 207 if os.IsNotExist(err) { 208 // Not existing is not an error 209 return false, nil 210 } 211 return false, err 212 } 213 defer readerB.Close() 214 215 // If the files have different size or different mode they are not 216 // identical and need to be re-created. Mode change could be optimized to 217 // avoid re-writing the whole file. 218 if modeA.Perm() != modeB.Perm() || sizeA != sizeB { 219 return false, nil 220 } 221 // The files have the same size so they might be identical. 222 // Do a block-wise comparison to determine that. 223 return streamsEqualChunked(readerA, readerB, 0), nil 224 } 225 226 // EnsureFileState ensures that the file is in the expected state. It will not 227 // attempt to remove the file if no content is provided. 228 func EnsureFileState(filePath string, state FileState) error { 229 equal, err := fileStateEqualTo(filePath, state) 230 if err != nil { 231 return err 232 } 233 if equal { 234 // Return a special error if the file doesn't need to be changed 235 return ErrSameState 236 } 237 reader, _, mode, err := state.State() 238 if err != nil { 239 return err 240 } 241 return AtomicWrite(filePath, reader, mode, 0) 242 }