123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173 |
- //go:build !nowatcher
- package watcher
- import (
- "github.com/dunglas/frankenphp/internal/fastabs"
- "path/filepath"
- "strings"
- "go.uber.org/zap"
- )
- type watchPattern struct {
- dir string
- patterns []string
- trigger chan struct{}
- failureCount int
- }
- func parseFilePatterns(filePatterns []string) ([]*watchPattern, error) {
- watchPatterns := make([]*watchPattern, 0, len(filePatterns))
- for _, filePattern := range filePatterns {
- watchPattern, err := parseFilePattern(filePattern)
- if err != nil {
- return nil, err
- }
- watchPatterns = append(watchPatterns, watchPattern)
- }
- return watchPatterns, nil
- }
- // this method prepares the watchPattern struct for a single file pattern (aka /path/*pattern)
- // TODO: using '/' is more efficient than filepath functions, but does not work on windows
- func parseFilePattern(filePattern string) (*watchPattern, error) {
- w := &watchPattern{}
- // first we clean the pattern
- absPattern, err := fastabs.FastAbs(filePattern)
- if err != nil {
- return nil, err
- }
- w.dir = absPattern
- // then we split the pattern to determine where the directory ends and the pattern starts
- splitPattern := strings.Split(absPattern, "/")
- patternWithoutDir := ""
- for i, part := range splitPattern {
- isFilename := i == len(splitPattern)-1 && strings.Contains(part, ".")
- isGlobCharacter := strings.ContainsAny(part, "[*?{")
- if isFilename || isGlobCharacter {
- patternWithoutDir = filepath.Join(splitPattern[i:]...)
- w.dir = filepath.Join(splitPattern[:i]...)
- break
- }
- }
- // now we split the pattern according to the recursive '**' syntax
- w.patterns = strings.Split(patternWithoutDir, "**")
- for i, pattern := range w.patterns {
- w.patterns[i] = strings.Trim(pattern, "/")
- }
- // finally, we remove the trailing slash and add leading slash
- w.dir = "/" + strings.Trim(w.dir, "/")
- return w, nil
- }
- func (watchPattern *watchPattern) allowReload(fileName string, eventType int, pathType int) bool {
- if !isValidEventType(eventType) || !isValidPathType(pathType, fileName) {
- return false
- }
- return isValidPattern(fileName, watchPattern.dir, watchPattern.patterns)
- }
- // 0:rename,1:modify,2:create,3:destroy,4:owner,5:other,
- func isValidEventType(eventType int) bool {
- return eventType <= 3
- }
- // 0:dir,1:file,2:hard_link,3:sym_link,4:watcher,5:other,
- func isValidPathType(pathType int, fileName string) bool {
- if pathType == 4 {
- logger.Debug("special edant/watcher event", zap.String("fileName", fileName))
- }
- return pathType <= 2
- }
- func isValidPattern(fileName string, dir string, patterns []string) bool {
- // first we remove the dir from the pattern
- if !strings.HasPrefix(fileName, dir) {
- return false
- }
- // remove the dir and '/' from the filename
- fileNameWithoutDir := strings.TrimPrefix(strings.TrimPrefix(fileName, dir), "/")
- // if the pattern has size 1 we can match it directly against the filename
- if len(patterns) == 1 {
- return matchBracketPattern(patterns[0], fileNameWithoutDir)
- }
- return matchPatterns(patterns, fileNameWithoutDir)
- }
- func matchPatterns(patterns []string, fileName string) bool {
- partsToMatch := strings.Split(fileName, "/")
- cursor := 0
- // if there are multiple patterns due to '**' we need to match them individually
- for i, pattern := range patterns {
- patternSize := strings.Count(pattern, "/") + 1
- // if we are at the last pattern we will start matching from the end of the filename
- if i == len(patterns)-1 {
- cursor = len(partsToMatch) - patternSize
- }
- // the cursor will move through the fileName until the pattern matches
- for j := cursor; j < len(partsToMatch); j++ {
- cursor = j
- subPattern := strings.Join(partsToMatch[j:j+patternSize], "/")
- if matchBracketPattern(pattern, subPattern) {
- cursor = j + patternSize - 1
- break
- }
- if cursor > len(partsToMatch)-patternSize-1 {
- return false
- }
- }
- }
- return true
- }
- // we also check for the following bracket syntax: /path/*.{php,twig,yaml}
- func matchBracketPattern(pattern string, fileName string) bool {
- openingBracket := strings.Index(pattern, "{")
- closingBracket := strings.Index(pattern, "}")
- // if there are no brackets we can match regularly
- if openingBracket == -1 || closingBracket == -1 {
- return matchPattern(pattern, fileName)
- }
- beforeTheBrackets := pattern[:openingBracket]
- betweenTheBrackets := pattern[openingBracket+1 : closingBracket]
- afterTheBrackets := pattern[closingBracket+1:]
- // all bracket entries are checked individually, only one needs to match
- // *.{php,twig,yaml} -> *.php, *.twig, *.yaml
- for _, pattern := range strings.Split(betweenTheBrackets, ",") {
- if matchPattern(beforeTheBrackets+pattern+afterTheBrackets, fileName) {
- return true
- }
- }
- return false
- }
- func matchPattern(pattern string, fileName string) bool {
- if pattern == "" {
- return true
- }
- patternMatches, err := filepath.Match(pattern, fileName)
- if err != nil {
- logger.Error("failed to match filename", zap.String("file", fileName), zap.Error(err))
- return false
- }
- return patternMatches
- }
|