github.com/rigado/snapd@v2.42.5-go-mod+incompatible/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/ioutil" 26 "os" 27 "path/filepath" 28 "sort" 29 ) 30 31 // FileState describes the expected content and meta data of a single file. 32 type FileState struct { 33 Content []byte 34 Mode os.FileMode 35 } 36 37 // ErrSameState is returned when the state of a file has not changed. 38 var ErrSameState = fmt.Errorf("file state has not changed") 39 40 // EnsureDirStateGlobs ensures that directory content matches expectations. 41 // 42 // EnsureDirStateGlobs enumerates all the files in the specified directory that 43 // match the provided set of pattern (globs). Each enumerated file is checked 44 // to ensure that the contents, permissions are what is desired. Unexpected 45 // files are removed. Missing files are created and differing files are 46 // corrected. Files not matching any pattern are ignored. 47 // 48 // Note that EnsureDirStateGlobs only checks for permissions and content. Other 49 // security mechanisms, including file ownership and extended attributes are 50 // *not* supported. 51 // 52 // The content map describes each of the files that are intended to exist in 53 // the directory. Map keys must be file names relative to the directory. 54 // Sub-directories in the name are not allowed. 55 // 56 // If writing any of the files fails, EnsureDirStateGlobs switches to erase mode 57 // where *all* of the files managed by the glob pattern are removed (including 58 // those that may have been already written). The return value is an empty list 59 // of changed files, the real list of removed files and the first error. 60 // 61 // If an error happens while removing files then such a file is not removed but 62 // the removal continues until the set of managed files matching the glob is 63 // exhausted. 64 // 65 // In all cases, the function returns the first error it has encountered. 66 func EnsureDirStateGlobs(dir string, globs []string, content map[string]*FileState) (changed, removed []string, err error) { 67 // Check syntax before doing anything. 68 if _, index, err := matchAny(globs, "foo"); err != nil { 69 return nil, nil, fmt.Errorf("internal error: EnsureDirState got invalid pattern %q: %s", globs[index], err) 70 } 71 for baseName := range content { 72 if filepath.Base(baseName) != baseName { 73 return nil, nil, fmt.Errorf("internal error: EnsureDirState got filename %q which has a path component", baseName) 74 } 75 if ok, _, _ := matchAny(globs, baseName); !ok { 76 if len(globs) == 1 { 77 return nil, nil, fmt.Errorf("internal error: EnsureDirState got filename %q which doesn't match the glob pattern %q", baseName, globs[0]) 78 } 79 return nil, nil, fmt.Errorf("internal error: EnsureDirState got filename %q which doesn't match any glob patterns %q", baseName, globs) 80 } 81 } 82 // Change phase (create/change files described by content) 83 var firstErr error 84 for baseName, fileState := range content { 85 filePath := filepath.Join(dir, baseName) 86 err := EnsureFileState(filePath, fileState) 87 if err == ErrSameState { 88 continue 89 } 90 if err != nil { 91 // On write failure, switch to erase mode. Desired content is set 92 // to nothing (no content) changed files are forgotten and the 93 // writing loop stops. The subsequent erase loop will remove all 94 // the managed content. 95 firstErr = err 96 content = nil 97 changed = nil 98 break 99 } 100 changed = append(changed, baseName) 101 } 102 // Delete phase (remove files matching the glob that are not in content) 103 matches := make(map[string]bool) 104 for _, glob := range globs { 105 m, err := filepath.Glob(filepath.Join(dir, glob)) 106 if err != nil { 107 sort.Strings(changed) 108 return changed, nil, err 109 } 110 for _, path := range m { 111 matches[path] = true 112 } 113 } 114 115 for path := range matches { 116 baseName := filepath.Base(path) 117 if content[baseName] != nil { 118 continue 119 } 120 err := os.Remove(path) 121 if err != nil { 122 if firstErr == nil { 123 firstErr = err 124 } 125 continue 126 } 127 removed = append(removed, baseName) 128 } 129 sort.Strings(changed) 130 sort.Strings(removed) 131 return changed, removed, firstErr 132 } 133 134 func matchAny(globs []string, path string) (ok bool, index int, err error) { 135 for index, glob := range globs { 136 if ok, err := filepath.Match(glob, path); ok || err != nil { 137 return ok, index, err 138 } 139 } 140 return false, 0, nil 141 } 142 143 // EnsureDirState ensures that directory content matches expectations. 144 // 145 // This is like EnsureDirStateGlobs but it only supports one glob at a time. 146 func EnsureDirState(dir string, glob string, content map[string]*FileState) (changed, removed []string, err error) { 147 return EnsureDirStateGlobs(dir, []string{glob}, content) 148 } 149 150 // Equals returns whether the file exists in the expected state. 151 func (fileState *FileState) Equals(filePath string) (bool, error) { 152 stat, err := os.Stat(filePath) 153 if err != nil { 154 if os.IsNotExist(err) { 155 // not existing is not an error 156 return false, nil 157 } 158 return false, err 159 } 160 if stat.Mode().Perm() == fileState.Mode.Perm() && stat.Size() == int64(len(fileState.Content)) { 161 content, err := ioutil.ReadFile(filePath) 162 if err != nil { 163 return false, err 164 } 165 if bytes.Equal(content, fileState.Content) { 166 return true, nil 167 } 168 } 169 return false, nil 170 } 171 172 // EnsureFileState ensures that the file is in the expected state. It will not attempt 173 // to remove the file if no content is provided. 174 func EnsureFileState(filePath string, fileState *FileState) error { 175 equal, err := fileState.Equals(filePath) 176 if err != nil { 177 return err 178 } 179 if equal { 180 // Return a special error if the file doesn't need to be changed 181 return ErrSameState 182 } 183 return AtomicWriteFile(filePath, fileState.Content, fileState.Mode, 0) 184 }