github.com/atc0005/elbow@v0.8.8/internal/paths/paths.go (about) 1 // Copyright 2020 Adam Chalkley 2 // 3 // https://github.com/atc0005/elbow 4 // 5 // Licensed under the Apache License, Version 2.0 (the "License"); 6 // you may not use this file except in compliance with the License. 7 // You may obtain a copy of the License at 8 // 9 // https://www.apache.org/licenses/LICENSE-2.0 10 // 11 // Unless required by applicable law or agreed to in writing, software 12 // distributed under the License is distributed on an "AS IS" BASIS, 13 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 // See the License for the specific language governing permissions and 15 // limitations under the License. 16 17 // Package paths provides various functions and types related to processing 18 // paths in the filesystem, often for the purpose of removing older/unwanted 19 // files. 20 package paths 21 22 import ( 23 "fmt" 24 "os" 25 "path/filepath" 26 "strings" 27 28 "github.com/atc0005/elbow/internal/config" 29 "github.com/atc0005/elbow/internal/matches" 30 "github.com/sirupsen/logrus" 31 ) 32 33 // ProcessingResults is used to collect execution results for use in logging 34 // and output summary presentation to the user 35 type ProcessingResults struct { 36 37 // Number of files eligible for removal. This is before files are excluded 38 // per user request. 39 EligibleRemove int 40 41 // Number of files successfully removed. 42 SuccessRemoved int 43 44 // Number of files failed to remove. 45 FailedRemoved int 46 47 // Size of all files eligible for removal. 48 EligibleFileSize int64 49 50 // Size of all files successfully removed. 51 SuccessTotalFileSize int64 52 53 // Size of all files failed to remove. 54 FailedTotalFileSize int64 55 56 // Size of all files successfully and unsuccessfully removed. This is 57 // essentially the size of eligible files to be removed minus any files 58 // that are excluded by user request. 59 TotalProcessedFileSize int64 60 } 61 62 // PathPruningResults represents the number of files that were successfully 63 // removed and those that were not. This is used in various calculations and 64 // to provide a brief summary of results to the user at program completion. 65 type PathPruningResults struct { 66 SuccessfulRemovals matches.FileMatches 67 FailedRemovals matches.FileMatches 68 } 69 70 // CleanPath receives a slice of FileMatch objects and removes each file. Any 71 // errors encountered while removing files may optionally be ignored via 72 // command-line flag(default is to return immediately upon first error). The 73 // total number of files successfully removed is returned along with an error 74 // code (nil if no errors were encountered). 75 func CleanPath(files matches.FileMatches, config *config.Config) (PathPruningResults, error) { 76 77 log := config.GetLogger() 78 79 for _, file := range files { 80 log.WithFields(logrus.Fields{ 81 "fullpath": strings.TrimSpace(file.Path), 82 "shortpath": file.Name(), 83 "size": file.Size(), 84 "modified": file.ModTime().Format("2006-01-02 15:04:05"), 85 "removal_enabled": config.GetRemove(), 86 }).Debug("Matching file") 87 } 88 89 var removalResults PathPruningResults 90 91 if !config.GetRemove() { 92 93 log.Info("File removal not enabled, not removing files") 94 95 // Nothing to show for this yet, but since the initial state reflects 96 // that we can return it as-is 97 return removalResults, nil 98 } 99 100 for _, file := range files { 101 102 log.WithFields(logrus.Fields{ 103 "removal_enabled": config.GetRemove(), 104 105 // fully-qualified path to the file 106 "file": file.Path, 107 }).Debug("Removing file") 108 109 // We need to reference the full path here, not the short name since 110 // the current working directory may not be the same directory 111 // where the file is located 112 err := os.Remove(file.Path) 113 if err != nil { 114 log.WithFields(logrus.Fields{ 115 116 // Include full details for troubleshooting purposes 117 "file": file, 118 }).Errorf("Error encountered while removing file: %s", err) 119 120 // Record failed removal, proceed to the next file 121 removalResults.FailedRemovals = append(removalResults.FailedRemovals, file) 122 123 // Confirm that we should ignore errors (likely enabled) 124 if !config.GetIgnoreErrors() { 125 remainingFiles := len(files) - len(removalResults.FailedRemovals) - len(removalResults.SuccessfulRemovals) 126 log.Debugf("Abandoning removal of %d remaining files", remainingFiles) 127 break 128 } 129 130 log.Debug("Ignoring error as requested") 131 continue 132 } 133 134 // Record successful removal 135 removalResults.SuccessfulRemovals = append(removalResults.SuccessfulRemovals, file) 136 } 137 138 return removalResults, nil 139 140 } 141 142 // PathExists confirms that the specified path exists 143 func PathExists(path string) (bool, error) { 144 145 // Make sure path isn't empty 146 if strings.TrimSpace(path) == "" { 147 return false, fmt.Errorf("specified path is empty string") 148 } 149 150 _, statErr := os.Stat(path) 151 if statErr != nil { 152 if !os.IsNotExist(statErr) { 153 // ERROR: another error occurred aside from file not found 154 return false, fmt.Errorf( 155 "error checking path %s: %w", 156 path, 157 statErr, 158 ) 159 } 160 // file not found 161 return false, nil 162 } 163 164 // file found 165 return true, nil 166 167 } 168 169 // ProcessPath accepts a configuration object and a path to process and 170 // returns a slice of FileMatch objects 171 func ProcessPath(config *config.Config, path string) (matches.FileMatches, error) { 172 173 log := config.GetLogger() 174 175 var fileMatches matches.FileMatches 176 var err error 177 178 log.WithFields(logrus.Fields{ 179 "recursive_search": config.GetRecursiveSearch(), 180 }).Debugf("Recursive search: %t", config.GetRecursiveSearch()) 181 182 if config.GetRecursiveSearch() { 183 184 // Walk walks the file tree rooted at root, calling the anonymous function 185 // for each file or directory in the tree, including root. All errors that 186 // arise visiting files and directories are filtered by the anonymous 187 // function. The files are walked in lexical order, which makes the output 188 // deterministic but means that for very large directories Walk can be 189 // inefficient. Walk does not follow symbolic links. 190 err = filepath.Walk(path, func(path string, info os.FileInfo, err error) error { 191 192 // If an error is received, check to see whether we should ignore 193 // it or return it. If we return a non-nil error, this will stop 194 // the filepath.Walk() function from continuing to walk the path, 195 // and your main function will immediately move to the next line. 196 // If the option to ignore errors is set, processing of the current 197 // path will continue until complete 198 if err != nil { 199 if !config.GetIgnoreErrors() { 200 return err 201 } 202 203 log.WithFields(logrus.Fields{ 204 "ignore_errors": config.GetIgnoreErrors(), 205 }).Warn("Error encountered:", err) 206 207 log.WithFields(logrus.Fields{ 208 "ignore_errors": config.GetIgnoreErrors(), 209 }).Warn("Ignoring error as requested") 210 211 } 212 213 // make sure we're not working with the root directory itself 214 if path != "." { 215 216 // ignore directories 217 if info.IsDir() { 218 return nil 219 } 220 221 // ignore non-matching extension (only applies if user chose 222 // one or more extensions to match against) 223 if !matches.HasMatchingExtension(path, config) { 224 return nil 225 } 226 227 // ignore non-matching filename pattern (only applies if user 228 // specified a filename pattern) 229 if !matches.HasMatchingFilenamePattern(path, config) { 230 return nil 231 } 232 233 // ignore non-matching modification age 234 if !matches.HasMatchingAge(info, config) { 235 return nil 236 } 237 238 // If we made it to this point, then we must assume that the file 239 // has met all criteria to be removed by this application. 240 fileMatch := matches.FileMatch{FileInfo: info, Path: path} 241 fileMatches = append(fileMatches, fileMatch) 242 243 } 244 245 return err 246 }) 247 248 } else { 249 250 // If RecursiveSearch is not enabled, process just the provided StartPath 251 // NOTE: The same cleanPath() function is used in either case, the 252 // difference is in how the FileMatches slice is populated. 253 254 files, err := os.ReadDir(path) 255 if err != nil { 256 // TODO: Do we really want to exit early at this point if there are 257 // failures evaluating some of the files? 258 // Is it possible to partially evaluate some of the files? 259 // 260 // return nil, fmt.Errorf( 261 // "error reading directory %s: %w", 262 // path, 263 // err, 264 // ) 265 log.Errorf("Error reading directory %s: %s", path, err) 266 } 267 268 // Build collection of FileMatch objects for later evaluation. 269 for _, file := range files { 270 271 // ignore directories 272 if file.IsDir() { 273 continue 274 } 275 276 fileInfo, err := file.Info() 277 if err != nil { 278 return nil, fmt.Errorf( 279 "file %s renamed or removed since directory read: %w", 280 fileInfo.Name(), 281 err, 282 ) 283 } 284 285 // Apply validity checks against filename. If validity fails, 286 // go to the next file in the list. 287 288 // ignore invalid extensions (only applies if user chose one 289 // or more extensions to match against) 290 if !matches.HasMatchingExtension(fileInfo.Name(), config) { 291 continue 292 } 293 294 // ignore invalid filename patterns (only applies if user 295 // specified a filename pattern) 296 if !matches.HasMatchingFilenamePattern(fileInfo.Name(), config) { 297 continue 298 } 299 300 // ignore non-matching modification age 301 if !matches.HasMatchingAge(fileInfo, config) { 302 continue 303 } 304 305 // If we made it to this point, then we must assume that the file 306 // has met all criteria to be removed by this application. 307 fileMatch := matches.FileMatch{ 308 FileInfo: fileInfo, 309 Path: filepath.Join(path, file.Name()), 310 } 311 312 fileMatches = append(fileMatches, fileMatch) 313 } 314 } 315 316 return fileMatches, err 317 }