index.go 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. package cache
  2. import (
  3. "context"
  4. "io/fs"
  5. "os"
  6. "path/filepath"
  7. "strings"
  8. "sync"
  9. "time"
  10. "github.com/0xJacky/Nginx-UI/internal/event"
  11. "github.com/0xJacky/Nginx-UI/internal/nginx"
  12. "github.com/fsnotify/fsnotify"
  13. "github.com/uozi-tech/cosy/logger"
  14. )
  15. // ScanCallback is called during config scanning with file path and content
  16. type ScanCallback func(configPath string, content []byte) error
  17. // Scanner watches and scans nginx config files
  18. type Scanner struct {
  19. ctx context.Context
  20. watcher *fsnotify.Watcher
  21. scanTicker *time.Ticker
  22. scanning bool
  23. scanMutex sync.RWMutex
  24. }
  25. var (
  26. scanner *Scanner
  27. scannerInitMutex sync.Mutex
  28. scanCallbacks = make([]ScanCallback, 0)
  29. scanCallbacksMutex sync.RWMutex
  30. )
  31. // InitScanner initializes the config scanner
  32. func InitScanner(ctx context.Context) {
  33. if nginx.GetConfPath() == "" {
  34. logger.Error("Nginx config path is not set")
  35. return
  36. }
  37. scanner := GetScanner()
  38. if err := scanner.Initialize(ctx); err != nil {
  39. logger.Error("Failed to initialize config scanner:", err)
  40. }
  41. }
  42. // GetScanner returns the singleton scanner instance
  43. func GetScanner() *Scanner {
  44. scannerInitMutex.Lock()
  45. defer scannerInitMutex.Unlock()
  46. if scanner == nil {
  47. scanner = &Scanner{}
  48. }
  49. return scanner
  50. }
  51. // RegisterCallback adds a callback to be executed during scans
  52. func RegisterCallback(callback ScanCallback) {
  53. scanCallbacksMutex.Lock()
  54. defer scanCallbacksMutex.Unlock()
  55. scanCallbacks = append(scanCallbacks, callback)
  56. }
  57. // Initialize sets up the scanner and starts watching
  58. func (s *Scanner) Initialize(ctx context.Context) error {
  59. watcher, err := fsnotify.NewWatcher()
  60. if err != nil {
  61. return err
  62. }
  63. s.watcher = watcher
  64. s.ctx = ctx
  65. // Initial scan
  66. if err := s.ScanAllConfigs(); err != nil {
  67. return err
  68. }
  69. // Watch all directories recursively
  70. if err := s.watchAllDirectories(); err != nil {
  71. return err
  72. }
  73. // Start background processes
  74. go s.watchForChanges()
  75. go s.periodicScan()
  76. go s.handleShutdown()
  77. return nil
  78. }
  79. // watchAllDirectories recursively adds all directories under nginx config path to watcher
  80. func (s *Scanner) watchAllDirectories() error {
  81. root := nginx.GetConfPath()
  82. sslDir := nginx.GetConfPath("ssl")
  83. return filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
  84. if err != nil {
  85. return err
  86. }
  87. if d.IsDir() {
  88. // Skip ssl directory
  89. if path == sslDir {
  90. return filepath.SkipDir
  91. }
  92. if err := s.watcher.Add(path); err != nil {
  93. logger.Error("Failed to watch directory:", path, err)
  94. return err
  95. }
  96. logger.Debug("Watching directory:", path)
  97. }
  98. return nil
  99. })
  100. }
  101. // periodicScan runs periodic scans every 5 minutes
  102. func (s *Scanner) periodicScan() {
  103. s.scanTicker = time.NewTicker(5 * time.Minute)
  104. defer s.scanTicker.Stop()
  105. for {
  106. select {
  107. case <-s.ctx.Done():
  108. return
  109. case <-s.scanTicker.C:
  110. if err := s.ScanAllConfigs(); err != nil {
  111. logger.Error("Periodic scan failed:", err)
  112. }
  113. }
  114. }
  115. }
  116. // handleShutdown listens for context cancellation and shuts down gracefully
  117. func (s *Scanner) handleShutdown() {
  118. <-s.ctx.Done()
  119. logger.Debug("Shutting down scanner")
  120. s.Shutdown()
  121. }
  122. // watchForChanges handles file system events
  123. func (s *Scanner) watchForChanges() {
  124. for {
  125. select {
  126. case <-s.ctx.Done():
  127. return
  128. case event, ok := <-s.watcher.Events:
  129. if !ok {
  130. return
  131. }
  132. s.handleFileEvent(event)
  133. case err, ok := <-s.watcher.Errors:
  134. if !ok {
  135. return
  136. }
  137. logger.Error("Watcher error:", err)
  138. }
  139. }
  140. }
  141. // handleFileEvent processes individual file system events
  142. func (s *Scanner) handleFileEvent(event fsnotify.Event) {
  143. // Only handle relevant events
  144. if !event.Has(fsnotify.Create) && !event.Has(fsnotify.Write) &&
  145. !event.Has(fsnotify.Rename) && !event.Has(fsnotify.Remove) {
  146. return
  147. }
  148. // Skip ssl directory
  149. sslDir := nginx.GetConfPath("ssl")
  150. if strings.HasPrefix(event.Name, sslDir) {
  151. return
  152. }
  153. // Add new directories to watch
  154. if event.Has(fsnotify.Create) {
  155. if fi, err := os.Stat(event.Name); err == nil && fi.IsDir() {
  156. if err := s.watcher.Add(event.Name); err != nil {
  157. logger.Error("Failed to add new directory to watcher:", event.Name, err)
  158. } else {
  159. logger.Debug("Added new directory to watcher:", event.Name)
  160. }
  161. }
  162. }
  163. // Handle file changes
  164. if event.Has(fsnotify.Remove) {
  165. logger.Debug("Config removed:", event.Name)
  166. return
  167. }
  168. fi, err := os.Stat(event.Name)
  169. if err != nil {
  170. return
  171. }
  172. if fi.IsDir() {
  173. logger.Debug("Directory changed:", event.Name)
  174. } else {
  175. logger.Debug("File changed:", event.Name)
  176. time.Sleep(100 * time.Millisecond) // Allow file write to complete
  177. s.scanSingleFile(event.Name)
  178. }
  179. }
  180. // scanSingleFile scans a single config file without recursion
  181. func (s *Scanner) scanSingleFile(filePath string) error {
  182. s.setScanningState(true)
  183. defer s.setScanningState(false)
  184. // Read file content
  185. content, err := os.ReadFile(filePath)
  186. if err != nil {
  187. return err
  188. }
  189. // Execute callbacks
  190. s.executeCallbacks(filePath, content)
  191. return nil
  192. }
  193. // setScanningState updates the scanning state and publishes events
  194. func (s *Scanner) setScanningState(scanning bool) {
  195. s.scanMutex.Lock()
  196. defer s.scanMutex.Unlock()
  197. if s.scanning != scanning {
  198. s.scanning = scanning
  199. event.Publish(event.Event{
  200. Type: event.EventTypeIndexScanning,
  201. Data: scanning,
  202. })
  203. }
  204. }
  205. // executeCallbacks runs all registered callbacks
  206. func (s *Scanner) executeCallbacks(filePath string, content []byte) {
  207. scanCallbacksMutex.RLock()
  208. defer scanCallbacksMutex.RUnlock()
  209. for _, callback := range scanCallbacks {
  210. if err := callback(filePath, content); err != nil {
  211. logger.Error("Callback error for", filePath, ":", err)
  212. }
  213. }
  214. }
  215. // ScanAllConfigs scans all nginx configuration files
  216. func (s *Scanner) ScanAllConfigs() error {
  217. s.setScanningState(true)
  218. defer s.setScanningState(false)
  219. root := nginx.GetConfPath()
  220. sslDir := nginx.GetConfPath("ssl")
  221. // Scan all files in the config directory and subdirectories
  222. return filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
  223. if err != nil {
  224. return err
  225. }
  226. // Skip ssl directory
  227. if d.IsDir() && path == sslDir {
  228. return filepath.SkipDir
  229. }
  230. // Only process regular files
  231. if !d.IsDir() {
  232. if err := s.scanSingleFile(path); err != nil {
  233. logger.Error("Failed to scan config:", path, err)
  234. }
  235. }
  236. return nil
  237. })
  238. }
  239. // Shutdown cleans up scanner resources
  240. func (s *Scanner) Shutdown() {
  241. if s.watcher != nil {
  242. s.watcher.Close()
  243. }
  244. if s.scanTicker != nil {
  245. s.scanTicker.Stop()
  246. }
  247. }
  248. // IsScanningInProgress returns whether a scan is currently running
  249. func IsScanningInProgress() bool {
  250. s := GetScanner()
  251. s.scanMutex.RLock()
  252. defer s.scanMutex.RUnlock()
  253. return s.scanning
  254. }