//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 }