watch_pattern.go 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. //go:build !nowatcher
  2. package watcher
  3. import (
  4. "github.com/dunglas/frankenphp/internal/fastabs"
  5. "path/filepath"
  6. "strings"
  7. "go.uber.org/zap"
  8. )
  9. type watchPattern struct {
  10. dir string
  11. patterns []string
  12. trigger chan struct{}
  13. failureCount int
  14. }
  15. func parseFilePatterns(filePatterns []string) ([]*watchPattern, error) {
  16. watchPatterns := make([]*watchPattern, 0, len(filePatterns))
  17. for _, filePattern := range filePatterns {
  18. watchPattern, err := parseFilePattern(filePattern)
  19. if err != nil {
  20. return nil, err
  21. }
  22. watchPatterns = append(watchPatterns, watchPattern)
  23. }
  24. return watchPatterns, nil
  25. }
  26. // this method prepares the watchPattern struct for a single file pattern (aka /path/*pattern)
  27. // TODO: using '/' is more efficient than filepath functions, but does not work on windows
  28. func parseFilePattern(filePattern string) (*watchPattern, error) {
  29. w := &watchPattern{}
  30. // first we clean the pattern
  31. absPattern, err := fastabs.FastAbs(filePattern)
  32. if err != nil {
  33. return nil, err
  34. }
  35. w.dir = absPattern
  36. // then we split the pattern to determine where the directory ends and the pattern starts
  37. splitPattern := strings.Split(absPattern, "/")
  38. patternWithoutDir := ""
  39. for i, part := range splitPattern {
  40. isFilename := i == len(splitPattern)-1 && strings.Contains(part, ".")
  41. isGlobCharacter := strings.ContainsAny(part, "[*?{")
  42. if isFilename || isGlobCharacter {
  43. patternWithoutDir = filepath.Join(splitPattern[i:]...)
  44. w.dir = filepath.Join(splitPattern[:i]...)
  45. break
  46. }
  47. }
  48. // now we split the pattern according to the recursive '**' syntax
  49. w.patterns = strings.Split(patternWithoutDir, "**")
  50. for i, pattern := range w.patterns {
  51. w.patterns[i] = strings.Trim(pattern, "/")
  52. }
  53. // finally, we remove the trailing slash and add leading slash
  54. w.dir = "/" + strings.Trim(w.dir, "/")
  55. return w, nil
  56. }
  57. func (watchPattern *watchPattern) allowReload(fileName string, eventType int, pathType int) bool {
  58. if !isValidEventType(eventType) || !isValidPathType(pathType, fileName) {
  59. return false
  60. }
  61. return isValidPattern(fileName, watchPattern.dir, watchPattern.patterns)
  62. }
  63. // 0:rename,1:modify,2:create,3:destroy,4:owner,5:other,
  64. func isValidEventType(eventType int) bool {
  65. return eventType <= 3
  66. }
  67. // 0:dir,1:file,2:hard_link,3:sym_link,4:watcher,5:other,
  68. func isValidPathType(pathType int, fileName string) bool {
  69. if pathType == 4 {
  70. logger.Debug("special edant/watcher event", zap.String("fileName", fileName))
  71. }
  72. return pathType <= 2
  73. }
  74. func isValidPattern(fileName string, dir string, patterns []string) bool {
  75. // first we remove the dir from the pattern
  76. if !strings.HasPrefix(fileName, dir) {
  77. return false
  78. }
  79. // remove the dir and '/' from the filename
  80. fileNameWithoutDir := strings.TrimPrefix(strings.TrimPrefix(fileName, dir), "/")
  81. // if the pattern has size 1 we can match it directly against the filename
  82. if len(patterns) == 1 {
  83. return matchBracketPattern(patterns[0], fileNameWithoutDir)
  84. }
  85. return matchPatterns(patterns, fileNameWithoutDir)
  86. }
  87. func matchPatterns(patterns []string, fileName string) bool {
  88. partsToMatch := strings.Split(fileName, "/")
  89. cursor := 0
  90. // if there are multiple patterns due to '**' we need to match them individually
  91. for i, pattern := range patterns {
  92. patternSize := strings.Count(pattern, "/") + 1
  93. // if we are at the last pattern we will start matching from the end of the filename
  94. if i == len(patterns)-1 {
  95. cursor = len(partsToMatch) - patternSize
  96. }
  97. // the cursor will move through the fileName until the pattern matches
  98. for j := cursor; j < len(partsToMatch); j++ {
  99. cursor = j
  100. subPattern := strings.Join(partsToMatch[j:j+patternSize], "/")
  101. if matchBracketPattern(pattern, subPattern) {
  102. cursor = j + patternSize - 1
  103. break
  104. }
  105. if cursor > len(partsToMatch)-patternSize-1 {
  106. return false
  107. }
  108. }
  109. }
  110. return true
  111. }
  112. // we also check for the following bracket syntax: /path/*.{php,twig,yaml}
  113. func matchBracketPattern(pattern string, fileName string) bool {
  114. openingBracket := strings.Index(pattern, "{")
  115. closingBracket := strings.Index(pattern, "}")
  116. // if there are no brackets we can match regularly
  117. if openingBracket == -1 || closingBracket == -1 {
  118. return matchPattern(pattern, fileName)
  119. }
  120. beforeTheBrackets := pattern[:openingBracket]
  121. betweenTheBrackets := pattern[openingBracket+1 : closingBracket]
  122. afterTheBrackets := pattern[closingBracket+1:]
  123. // all bracket entries are checked individually, only one needs to match
  124. // *.{php,twig,yaml} -> *.php, *.twig, *.yaml
  125. for _, pattern := range strings.Split(betweenTheBrackets, ",") {
  126. if matchPattern(beforeTheBrackets+pattern+afterTheBrackets, fileName) {
  127. return true
  128. }
  129. }
  130. return false
  131. }
  132. func matchPattern(pattern string, fileName string) bool {
  133. if pattern == "" {
  134. return true
  135. }
  136. patternMatches, err := filepath.Match(pattern, fileName)
  137. if err != nil {
  138. logger.Error("failed to match filename", zap.String("file", fileName), zap.Error(err))
  139. return false
  140. }
  141. return patternMatches
  142. }