watch_pattern.go 4.9 KB

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