github.com/kunnos/engine@v1.13.1/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  				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 ch == '.' || ch == '$' {
   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 removes
   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  }