github.com/flavio/docker@v0.1.3-0.20170117145210-f63d1a6eec47/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 returns true if the specified pattern is an exclusion 17 func exclusion(pattern string) bool { 18 return pattern[0] == '!' 19 } 20 21 // empty returns 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 its 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 // Treat **/ as ** so eat the "/" 165 if string(scan.Peek()) == sl { 166 scan.Next() 167 } 168 169 if scan.Peek() == scanner.EOF { 170 // is "**EOF" - to align with .gitignore just accept all 171 regStr += ".*" 172 } else { 173 // is "**" 174 // Note that this allows for any # of /'s (even 0) because 175 // the .* will eat everything, even /'s 176 regStr += "(.*" + escSL + ")?" 177 } 178 } else { 179 // is "*" so map it to anything but "/" 180 regStr += "[^" + escSL + "]*" 181 } 182 } else if ch == '?' { 183 // "?" is any char except "/" 184 regStr += "[^" + escSL + "]" 185 } else if ch == '.' || ch == '$' { 186 // Escape some regexp special chars that have no meaning 187 // in golang's filepath.Match 188 regStr += `\` + string(ch) 189 } else if ch == '\\' { 190 // escape next char. Note that a trailing \ in the pattern 191 // will be left alone (but need to escape it) 192 if sl == `\` { 193 // On windows map "\" to "\\", meaning an escaped backslash, 194 // and then just continue because filepath.Match on 195 // Windows doesn't allow escaping at all 196 regStr += escSL 197 continue 198 } 199 if scan.Peek() != scanner.EOF { 200 regStr += `\` + string(scan.Next()) 201 } else { 202 regStr += `\` 203 } 204 } else { 205 regStr += string(ch) 206 } 207 } 208 209 regStr += "$" 210 211 res, err := regexp.MatchString(regStr, path) 212 213 // Map regexp's error to filepath's so no one knows we're not using filepath 214 if err != nil { 215 err = filepath.ErrBadPattern 216 } 217 218 return res, err 219 } 220 221 // CopyFile copies from src to dst until either EOF is reached 222 // on src or an error occurs. It verifies src exists and removes 223 // the dst if it exists. 224 func CopyFile(src, dst string) (int64, error) { 225 cleanSrc := filepath.Clean(src) 226 cleanDst := filepath.Clean(dst) 227 if cleanSrc == cleanDst { 228 return 0, nil 229 } 230 sf, err := os.Open(cleanSrc) 231 if err != nil { 232 return 0, err 233 } 234 defer sf.Close() 235 if err := os.Remove(cleanDst); err != nil && !os.IsNotExist(err) { 236 return 0, err 237 } 238 df, err := os.Create(cleanDst) 239 if err != nil { 240 return 0, err 241 } 242 defer df.Close() 243 return io.Copy(df, sf) 244 } 245 246 // ReadSymlinkedDirectory returns the target directory of a symlink. 247 // The target of the symbolic link may not be a file. 248 func ReadSymlinkedDirectory(path string) (string, error) { 249 var realPath string 250 var err error 251 if realPath, err = filepath.Abs(path); err != nil { 252 return "", fmt.Errorf("unable to get absolute path for %s: %s", path, err) 253 } 254 if realPath, err = filepath.EvalSymlinks(realPath); err != nil { 255 return "", fmt.Errorf("failed to canonicalise path for %s: %s", path, err) 256 } 257 realPathInfo, err := os.Stat(realPath) 258 if err != nil { 259 return "", fmt.Errorf("failed to stat target '%s' of '%s': %s", realPath, path, err) 260 } 261 if !realPathInfo.Mode().IsDir() { 262 return "", fmt.Errorf("canonical path points to a file '%s'", realPath) 263 } 264 return realPath, nil 265 } 266 267 // CreateIfNotExists creates a file or a directory only if it does not already exist. 268 func CreateIfNotExists(path string, isDir bool) error { 269 if _, err := os.Stat(path); err != nil { 270 if os.IsNotExist(err) { 271 if isDir { 272 return os.MkdirAll(path, 0755) 273 } 274 if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { 275 return err 276 } 277 f, err := os.OpenFile(path, os.O_CREATE, 0755) 278 if err != nil { 279 return err 280 } 281 f.Close() 282 } 283 } 284 return nil 285 }