github.com/demonoid81/moby@v0.0.0-20200517203328-62dd8e17c460/pkg/fileutils/fileutils.go (about) 1 package fileutils // import "github.com/demonoid81/moby/pkg/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 // PatternMatcher allows checking paths against a list of patterns 17 type PatternMatcher struct { 18 patterns []*Pattern 19 exclusions bool 20 } 21 22 // NewPatternMatcher creates a new matcher object for specific patterns that can 23 // be used later to match against patterns against paths 24 func NewPatternMatcher(patterns []string) (*PatternMatcher, error) { 25 pm := &PatternMatcher{ 26 patterns: make([]*Pattern, 0, len(patterns)), 27 } 28 for _, p := range patterns { 29 // Eliminate leading and trailing whitespace. 30 p = strings.TrimSpace(p) 31 if p == "" { 32 continue 33 } 34 p = filepath.Clean(p) 35 newp := &Pattern{} 36 if p[0] == '!' { 37 if len(p) == 1 { 38 return nil, errors.New("illegal exclusion pattern: \"!\"") 39 } 40 newp.exclusion = true 41 p = p[1:] 42 pm.exclusions = true 43 } 44 // Do some syntax checking on the pattern. 45 // filepath's Match() has some really weird rules that are inconsistent 46 // so instead of trying to dup their logic, just call Match() for its 47 // error state and if there is an error in the pattern return it. 48 // If this becomes an issue we can remove this since its really only 49 // needed in the error (syntax) case - which isn't really critical. 50 if _, err := filepath.Match(p, "."); err != nil { 51 return nil, err 52 } 53 newp.cleanedPattern = p 54 newp.dirs = strings.Split(p, string(os.PathSeparator)) 55 pm.patterns = append(pm.patterns, newp) 56 } 57 return pm, nil 58 } 59 60 // Matches matches path against all the patterns. Matches is not safe to be 61 // called concurrently 62 func (pm *PatternMatcher) Matches(file string) (bool, error) { 63 matched := false 64 file = filepath.FromSlash(file) 65 parentPath := filepath.Dir(file) 66 parentPathDirs := strings.Split(parentPath, string(os.PathSeparator)) 67 68 for _, pattern := range pm.patterns { 69 negative := false 70 71 if pattern.exclusion { 72 negative = true 73 } 74 75 match, err := pattern.match(file) 76 if err != nil { 77 return false, err 78 } 79 80 if !match && parentPath != "." { 81 // Check to see if the pattern matches one of our parent dirs. 82 if len(pattern.dirs) <= len(parentPathDirs) { 83 match, _ = pattern.match(strings.Join(parentPathDirs[:len(pattern.dirs)], string(os.PathSeparator))) 84 } 85 } 86 87 if match { 88 matched = !negative 89 } 90 } 91 92 if matched { 93 logrus.Debugf("Skipping excluded path: %s", file) 94 } 95 96 return matched, nil 97 } 98 99 // Exclusions returns true if any of the patterns define exclusions 100 func (pm *PatternMatcher) Exclusions() bool { 101 return pm.exclusions 102 } 103 104 // Patterns returns array of active patterns 105 func (pm *PatternMatcher) Patterns() []*Pattern { 106 return pm.patterns 107 } 108 109 // Pattern defines a single regexp used to filter file paths. 110 type Pattern struct { 111 cleanedPattern string 112 dirs []string 113 regexp *regexp.Regexp 114 exclusion bool 115 } 116 117 func (p *Pattern) String() string { 118 return p.cleanedPattern 119 } 120 121 // Exclusion returns true if this pattern defines exclusion 122 func (p *Pattern) Exclusion() bool { 123 return p.exclusion 124 } 125 126 func (p *Pattern) match(path string) (bool, error) { 127 128 if p.regexp == nil { 129 if err := p.compile(); err != nil { 130 return false, filepath.ErrBadPattern 131 } 132 } 133 134 b := p.regexp.MatchString(path) 135 136 return b, nil 137 } 138 139 func (p *Pattern) compile() error { 140 regStr := "^" 141 pattern := p.cleanedPattern 142 // Go through the pattern and convert it to a regexp. 143 // We use a scanner so we can support utf-8 chars. 144 var scan scanner.Scanner 145 scan.Init(strings.NewReader(pattern)) 146 147 sl := string(os.PathSeparator) 148 escSL := sl 149 if sl == `\` { 150 escSL += `\` 151 } 152 153 for scan.Peek() != scanner.EOF { 154 ch := scan.Next() 155 156 if ch == '*' { 157 if scan.Peek() == '*' { 158 // is some flavor of "**" 159 scan.Next() 160 161 // Treat **/ as ** so eat the "/" 162 if string(scan.Peek()) == sl { 163 scan.Next() 164 } 165 166 if scan.Peek() == scanner.EOF { 167 // is "**EOF" - to align with .gitignore just accept all 168 regStr += ".*" 169 } else { 170 // is "**" 171 // Note that this allows for any # of /'s (even 0) because 172 // the .* will eat everything, even /'s 173 regStr += "(.*" + escSL + ")?" 174 } 175 } else { 176 // is "*" so map it to anything but "/" 177 regStr += "[^" + escSL + "]*" 178 } 179 } else if ch == '?' { 180 // "?" is any char except "/" 181 regStr += "[^" + escSL + "]" 182 } else if ch == '.' || ch == '$' { 183 // Escape some regexp special chars that have no meaning 184 // in golang's filepath.Match 185 regStr += `\` + string(ch) 186 } else if ch == '\\' { 187 // escape next char. Note that a trailing \ in the pattern 188 // will be left alone (but need to escape it) 189 if sl == `\` { 190 // On windows map "\" to "\\", meaning an escaped backslash, 191 // and then just continue because filepath.Match on 192 // Windows doesn't allow escaping at all 193 regStr += escSL 194 continue 195 } 196 if scan.Peek() != scanner.EOF { 197 regStr += `\` + string(scan.Next()) 198 } else { 199 regStr += `\` 200 } 201 } else { 202 regStr += string(ch) 203 } 204 } 205 206 regStr += "$" 207 208 re, err := regexp.Compile(regStr) 209 if err != nil { 210 return err 211 } 212 213 p.regexp = re 214 return nil 215 } 216 217 // Matches returns true if file matches any of the patterns 218 // and isn't excluded by any of the subsequent patterns. 219 func Matches(file string, patterns []string) (bool, error) { 220 pm, err := NewPatternMatcher(patterns) 221 if err != nil { 222 return false, err 223 } 224 file = filepath.Clean(file) 225 226 if file == "." { 227 // Don't let them exclude everything, kind of silly. 228 return false, nil 229 } 230 231 return pm.Matches(file) 232 } 233 234 // CopyFile copies from src to dst until either EOF is reached 235 // on src or an error occurs. It verifies src exists and removes 236 // the dst if it exists. 237 func CopyFile(src, dst string) (int64, error) { 238 cleanSrc := filepath.Clean(src) 239 cleanDst := filepath.Clean(dst) 240 if cleanSrc == cleanDst { 241 return 0, nil 242 } 243 sf, err := os.Open(cleanSrc) 244 if err != nil { 245 return 0, err 246 } 247 defer sf.Close() 248 if err := os.Remove(cleanDst); err != nil && !os.IsNotExist(err) { 249 return 0, err 250 } 251 df, err := os.Create(cleanDst) 252 if err != nil { 253 return 0, err 254 } 255 defer df.Close() 256 return io.Copy(df, sf) 257 } 258 259 // ReadSymlinkedDirectory returns the target directory of a symlink. 260 // The target of the symbolic link may not be a file. 261 func ReadSymlinkedDirectory(path string) (string, error) { 262 var realPath string 263 var err error 264 if realPath, err = filepath.Abs(path); err != nil { 265 return "", fmt.Errorf("unable to get absolute path for %s: %s", path, err) 266 } 267 if realPath, err = filepath.EvalSymlinks(realPath); err != nil { 268 return "", fmt.Errorf("failed to canonicalise path for %s: %s", path, err) 269 } 270 realPathInfo, err := os.Stat(realPath) 271 if err != nil { 272 return "", fmt.Errorf("failed to stat target '%s' of '%s': %s", realPath, path, err) 273 } 274 if !realPathInfo.Mode().IsDir() { 275 return "", fmt.Errorf("canonical path points to a file '%s'", realPath) 276 } 277 return realPath, nil 278 } 279 280 // CreateIfNotExists creates a file or a directory only if it does not already exist. 281 func CreateIfNotExists(path string, isDir bool) error { 282 if _, err := os.Stat(path); err != nil { 283 if os.IsNotExist(err) { 284 if isDir { 285 return os.MkdirAll(path, 0755) 286 } 287 if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { 288 return err 289 } 290 f, err := os.OpenFile(path, os.O_CREATE, 0755) 291 if err != nil { 292 return err 293 } 294 f.Close() 295 } 296 } 297 return nil 298 }