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  }