github.com/sijibomii/docker@v0.0.0-20231230191044-5cf6ca554647/pkg/fileutils/fileutils.go (about) 1 package fileutils 2 3 import ( 4 "errors" 5 "fmt" 6 "io" 7 "os" 8 "path/filepath" 9 "regexp" 10 "strings" 11 "text/scanner" 12 13 "github.com/Sirupsen/logrus" 14 ) 15 16 // exclusion return true if the specified pattern is an exclusion 17 func exclusion(pattern string) bool { 18 return pattern[0] == '!' 19 } 20 21 // empty return true if the specified pattern is empty 22 func empty(pattern string) bool { 23 return pattern == "" 24 } 25 26 // CleanPatterns takes a slice of patterns returns a new 27 // slice of patterns cleaned with filepath.Clean, stripped 28 // of any empty patterns and lets the caller know whether the 29 // slice contains any exception patterns (prefixed with !). 30 func CleanPatterns(patterns []string) ([]string, [][]string, bool, error) { 31 // Loop over exclusion patterns and: 32 // 1. Clean them up. 33 // 2. Indicate whether we are dealing with any exception rules. 34 // 3. Error if we see a single exclusion marker on it's own (!). 35 cleanedPatterns := []string{} 36 patternDirs := [][]string{} 37 exceptions := false 38 for _, pattern := range patterns { 39 // Eliminate leading and trailing whitespace. 40 pattern = strings.TrimSpace(pattern) 41 if empty(pattern) { 42 continue 43 } 44 if exclusion(pattern) { 45 if len(pattern) == 1 { 46 return nil, nil, false, errors.New("Illegal exclusion pattern: !") 47 } 48 exceptions = true 49 } 50 pattern = filepath.Clean(pattern) 51 cleanedPatterns = append(cleanedPatterns, pattern) 52 if exclusion(pattern) { 53 pattern = pattern[1:] 54 } 55 patternDirs = append(patternDirs, strings.Split(pattern, string(os.PathSeparator))) 56 } 57 58 return cleanedPatterns, patternDirs, exceptions, nil 59 } 60 61 // Matches returns true if file matches any of the patterns 62 // and isn't excluded by any of the subsequent patterns. 63 func Matches(file string, patterns []string) (bool, error) { 64 file = filepath.Clean(file) 65 66 if file == "." { 67 // Don't let them exclude everything, kind of silly. 68 return false, nil 69 } 70 71 patterns, patDirs, _, err := CleanPatterns(patterns) 72 if err != nil { 73 return false, err 74 } 75 76 return OptimizedMatches(file, patterns, patDirs) 77 } 78 79 // OptimizedMatches is basically the same as fileutils.Matches() but optimized for archive.go. 80 // It will assume that the inputs have been preprocessed and therefore the function 81 // doesn't need to do as much error checking and clean-up. This was done to avoid 82 // repeating these steps on each file being checked during the archive process. 83 // The more generic fileutils.Matches() can't make these assumptions. 84 func OptimizedMatches(file string, patterns []string, patDirs [][]string) (bool, error) { 85 matched := false 86 file = filepath.FromSlash(file) 87 parentPath := filepath.Dir(file) 88 parentPathDirs := strings.Split(parentPath, string(os.PathSeparator)) 89 90 for i, pattern := range patterns { 91 negative := false 92 93 if exclusion(pattern) { 94 negative = true 95 pattern = pattern[1:] 96 } 97 98 match, err := regexpMatch(pattern, file) 99 if err != nil { 100 return false, fmt.Errorf("Error in pattern (%s): %s", pattern, err) 101 } 102 103 if !match && parentPath != "." { 104 // Check to see if the pattern matches one of our parent dirs. 105 if len(patDirs[i]) <= len(parentPathDirs) { 106 match, _ = regexpMatch(strings.Join(patDirs[i], string(os.PathSeparator)), 107 strings.Join(parentPathDirs[:len(patDirs[i])], string(os.PathSeparator))) 108 } 109 } 110 111 if match { 112 matched = !negative 113 } 114 } 115 116 if matched { 117 logrus.Debugf("Skipping excluded path: %s", file) 118 } 119 120 return matched, nil 121 } 122 123 // regexpMatch tries to match the logic of filepath.Match but 124 // does so using regexp logic. We do this so that we can expand the 125 // wildcard set to include other things, like "**" to mean any number 126 // of directories. This means that we should be backwards compatible 127 // with filepath.Match(). We'll end up supporting more stuff, due to 128 // the fact that we're using regexp, but that's ok - it does no harm. 129 // 130 // As per the comment in golangs filepath.Match, on Windows, escaping 131 // is disabled. Instead, '\\' is treated as path separator. 132 func regexpMatch(pattern, path string) (bool, error) { 133 regStr := "^" 134 135 // Do some syntax checking on the pattern. 136 // filepath's Match() has some really weird rules that are inconsistent 137 // so instead of trying to dup their logic, just call Match() for its 138 // error state and if there is an error in the pattern return it. 139 // If this becomes an issue we can remove this since its really only 140 // needed in the error (syntax) case - which isn't really critical. 141 if _, err := filepath.Match(pattern, path); err != nil { 142 return false, err 143 } 144 145 // Go through the pattern and convert it to a regexp. 146 // We use a scanner so we can support utf-8 chars. 147 var scan scanner.Scanner 148 scan.Init(strings.NewReader(pattern)) 149 150 sl := string(os.PathSeparator) 151 escSL := sl 152 if sl == `\` { 153 escSL += `\` 154 } 155 156 for scan.Peek() != scanner.EOF { 157 ch := scan.Next() 158 159 if ch == '*' { 160 if scan.Peek() == '*' { 161 // is some flavor of "**" 162 scan.Next() 163 164 if scan.Peek() == scanner.EOF { 165 // is "**EOF" - to align with .gitignore just accept all 166 regStr += ".*" 167 } else { 168 // is "**" 169 regStr += "((.*" + escSL + ")|([^" + escSL + "]*))" 170 } 171 172 // Treat **/ as ** so eat the "/" 173 if string(scan.Peek()) == sl { 174 scan.Next() 175 } 176 } else { 177 // is "*" so map it to anything but "/" 178 regStr += "[^" + escSL + "]*" 179 } 180 } else if ch == '?' { 181 // "?" is any char except "/" 182 regStr += "[^" + escSL + "]" 183 } else if strings.Index(".$", string(ch)) != -1 { 184 // Escape some regexp special chars that have no meaning 185 // in golang's filepath.Match 186 regStr += `\` + string(ch) 187 } else if ch == '\\' { 188 // escape next char. Note that a trailing \ in the pattern 189 // will be left alone (but need to escape it) 190 if sl == `\` { 191 // On windows map "\" to "\\", meaning an escaped backslash, 192 // and then just continue because filepath.Match on 193 // Windows doesn't allow escaping at all 194 regStr += escSL 195 continue 196 } 197 if scan.Peek() != scanner.EOF { 198 regStr += `\` + string(scan.Next()) 199 } else { 200 regStr += `\` 201 } 202 } else { 203 regStr += string(ch) 204 } 205 } 206 207 regStr += "$" 208 209 res, err := regexp.MatchString(regStr, path) 210 211 // Map regexp's error to filepath's so no one knows we're not using filepath 212 if err != nil { 213 err = filepath.ErrBadPattern 214 } 215 216 return res, err 217 } 218 219 // CopyFile copies from src to dst until either EOF is reached 220 // on src or an error occurs. It verifies src exists and remove 221 // the dst if it exists. 222 func CopyFile(src, dst string) (int64, error) { 223 cleanSrc := filepath.Clean(src) 224 cleanDst := filepath.Clean(dst) 225 if cleanSrc == cleanDst { 226 return 0, nil 227 } 228 sf, err := os.Open(cleanSrc) 229 if err != nil { 230 return 0, err 231 } 232 defer sf.Close() 233 if err := os.Remove(cleanDst); err != nil && !os.IsNotExist(err) { 234 return 0, err 235 } 236 df, err := os.Create(cleanDst) 237 if err != nil { 238 return 0, err 239 } 240 defer df.Close() 241 return io.Copy(df, sf) 242 } 243 244 // ReadSymlinkedDirectory returns the target directory of a symlink. 245 // The target of the symbolic link may not be a file. 246 func ReadSymlinkedDirectory(path string) (string, error) { 247 var realPath string 248 var err error 249 if realPath, err = filepath.Abs(path); err != nil { 250 return "", fmt.Errorf("unable to get absolute path for %s: %s", path, err) 251 } 252 if realPath, err = filepath.EvalSymlinks(realPath); err != nil { 253 return "", fmt.Errorf("failed to canonicalise path for %s: %s", path, err) 254 } 255 realPathInfo, err := os.Stat(realPath) 256 if err != nil { 257 return "", fmt.Errorf("failed to stat target '%s' of '%s': %s", realPath, path, err) 258 } 259 if !realPathInfo.Mode().IsDir() { 260 return "", fmt.Errorf("canonical path points to a file '%s'", realPath) 261 } 262 return realPath, nil 263 } 264 265 // CreateIfNotExists creates a file or a directory only if it does not already exist. 266 func CreateIfNotExists(path string, isDir bool) error { 267 if _, err := os.Stat(path); err != nil { 268 if os.IsNotExist(err) { 269 if isDir { 270 return os.MkdirAll(path, 0755) 271 } 272 if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { 273 return err 274 } 275 f, err := os.OpenFile(path, os.O_CREATE, 0755) 276 if err != nil { 277 return err 278 } 279 f.Close() 280 } 281 } 282 return nil 283 }