watch_pattern.go 4.8 KB

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