index.go 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  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. // shouldSkipPath checks if a path should be skipped during scanning or watching
  43. func shouldSkipPath(path string) bool {
  44. // Define directories to exclude from scanning/watching
  45. excludedDirs := []string{
  46. nginx.GetConfPath("ssl"), // SSL certificates and keys
  47. nginx.GetConfPath("cache"), // Nginx cache files
  48. nginx.GetConfPath("logs"), // Log files directory
  49. nginx.GetConfPath("temp"), // Temporary files directory
  50. nginx.GetConfPath("proxy_temp"), // Proxy temporary files
  51. nginx.GetConfPath("client_body_temp"), // Client body temporary files
  52. nginx.GetConfPath("fastcgi_temp"), // FastCGI temporary files
  53. nginx.GetConfPath("uwsgi_temp"), // uWSGI temporary files
  54. nginx.GetConfPath("scgi_temp"), // SCGI temporary files
  55. }
  56. // Check if path starts with any excluded directory
  57. for _, excludedDir := range excludedDirs {
  58. if excludedDir != "" && strings.HasPrefix(path, excludedDir) {
  59. return true
  60. }
  61. }
  62. return false
  63. }
  64. // GetScanner returns the singleton scanner instance
  65. func GetScanner() *Scanner {
  66. scannerInitMutex.Lock()
  67. defer scannerInitMutex.Unlock()
  68. if scanner == nil {
  69. scanner = &Scanner{}
  70. }
  71. return scanner
  72. }
  73. // RegisterCallback adds a callback to be executed during scans
  74. func RegisterCallback(callback ScanCallback) {
  75. scanCallbacksMutex.Lock()
  76. defer scanCallbacksMutex.Unlock()
  77. scanCallbacks = append(scanCallbacks, callback)
  78. }
  79. // Initialize sets up the scanner and starts watching
  80. func (s *Scanner) Initialize(ctx context.Context) error {
  81. watcher, err := fsnotify.NewWatcher()
  82. if err != nil {
  83. return err
  84. }
  85. s.watcher = watcher
  86. s.ctx = ctx
  87. // Initial scan
  88. if err := s.ScanAllConfigs(); err != nil {
  89. return err
  90. }
  91. // Watch all directories recursively
  92. if err := s.watchAllDirectories(); err != nil {
  93. return err
  94. }
  95. // Start background processes
  96. go s.watchForChanges()
  97. go s.periodicScan()
  98. go s.handleShutdown()
  99. return nil
  100. }
  101. // watchAllDirectories recursively adds all directories under nginx config path to watcher
  102. func (s *Scanner) watchAllDirectories() error {
  103. root := nginx.GetConfPath()
  104. return filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
  105. if err != nil {
  106. return err
  107. }
  108. if d.IsDir() {
  109. // Skip excluded directories (ssl, cache, logs, temp, etc.)
  110. if shouldSkipPath(path) {
  111. return filepath.SkipDir
  112. }
  113. if err := s.watcher.Add(path); err != nil {
  114. logger.Error("Failed to watch directory:", path, err)
  115. return err
  116. }
  117. // logger.Debug("Watching directory:", path)
  118. }
  119. return nil
  120. })
  121. }
  122. // periodicScan runs periodic scans every 5 minutes
  123. func (s *Scanner) periodicScan() {
  124. s.scanTicker = time.NewTicker(5 * time.Minute)
  125. defer s.scanTicker.Stop()
  126. for {
  127. select {
  128. case <-s.ctx.Done():
  129. return
  130. case <-s.scanTicker.C:
  131. if err := s.ScanAllConfigs(); err != nil {
  132. logger.Error("Periodic scan failed:", err)
  133. }
  134. }
  135. }
  136. }
  137. // handleShutdown listens for context cancellation and shuts down gracefully
  138. func (s *Scanner) handleShutdown() {
  139. <-s.ctx.Done()
  140. logger.Info("Shutting down Index Scanner")
  141. s.Shutdown()
  142. }
  143. // watchForChanges handles file system events
  144. func (s *Scanner) watchForChanges() {
  145. for {
  146. select {
  147. case <-s.ctx.Done():
  148. return
  149. case event, ok := <-s.watcher.Events:
  150. if !ok {
  151. return
  152. }
  153. s.handleFileEvent(event)
  154. case err, ok := <-s.watcher.Errors:
  155. if !ok {
  156. return
  157. }
  158. logger.Error("Watcher error:", err)
  159. }
  160. }
  161. }
  162. // handleFileEvent processes individual file system events
  163. func (s *Scanner) handleFileEvent(event fsnotify.Event) {
  164. // Only handle relevant events
  165. if !event.Has(fsnotify.Create) && !event.Has(fsnotify.Write) &&
  166. !event.Has(fsnotify.Rename) && !event.Has(fsnotify.Remove) {
  167. return
  168. }
  169. // Skip excluded directories (ssl, cache, etc.)
  170. if shouldSkipPath(event.Name) {
  171. return
  172. }
  173. // Add new directories to watch
  174. if event.Has(fsnotify.Create) {
  175. if fi, err := os.Stat(event.Name); err == nil && fi.IsDir() {
  176. if err := s.watcher.Add(event.Name); err != nil {
  177. logger.Error("Failed to add new directory to watcher:", event.Name, err)
  178. } else {
  179. logger.Debug("Added new directory to watcher:", event.Name)
  180. }
  181. }
  182. }
  183. // Handle file changes
  184. if event.Has(fsnotify.Remove) {
  185. logger.Debug("Config removed:", event.Name)
  186. return
  187. }
  188. fi, err := os.Stat(event.Name)
  189. if err != nil {
  190. return
  191. }
  192. if fi.IsDir() {
  193. logger.Debug("Directory changed:", event.Name)
  194. } else {
  195. logger.Debug("File changed:", event.Name)
  196. time.Sleep(100 * time.Millisecond) // Allow file write to complete
  197. s.scanSingleFile(event.Name)
  198. }
  199. }
  200. // scanSingleFile scans a single config file without recursion
  201. func (s *Scanner) scanSingleFile(filePath string) error {
  202. s.setScanningState(true)
  203. defer s.setScanningState(false)
  204. // Check if path should be skipped
  205. if shouldSkipPath(filePath) {
  206. return nil
  207. }
  208. // Get file info to check type and size
  209. fileInfo, err := os.Lstat(filePath) // Use Lstat to avoid following symlinks
  210. if err != nil {
  211. return err
  212. }
  213. // Skip directories
  214. if fileInfo.IsDir() {
  215. logger.Debugf("Skipping directory: %s", filePath)
  216. return nil
  217. }
  218. // Skip symlinks to avoid potential issues
  219. if fileInfo.Mode()&os.ModeSymlink != 0 {
  220. logger.Debugf("Skipping symlink: %s", filePath)
  221. return nil
  222. }
  223. // Skip non-regular files (devices, pipes, sockets, etc.)
  224. if !fileInfo.Mode().IsRegular() {
  225. logger.Debugf("Skipping non-regular file: %s (mode: %s)", filePath, fileInfo.Mode())
  226. return nil
  227. }
  228. // Skip files larger than 1MB before reading
  229. if fileInfo.Size() > 1024*1024 {
  230. logger.Debugf("Skipping large file: %s (size: %d bytes)", filePath, fileInfo.Size())
  231. return nil
  232. }
  233. // Read file content
  234. content, err := os.ReadFile(filePath)
  235. if err != nil {
  236. return err
  237. }
  238. // Execute callbacks
  239. s.executeCallbacks(filePath, content)
  240. return nil
  241. }
  242. // setScanningState updates the scanning state and publishes events
  243. func (s *Scanner) setScanningState(scanning bool) {
  244. s.scanMutex.Lock()
  245. defer s.scanMutex.Unlock()
  246. if s.scanning != scanning {
  247. s.scanning = scanning
  248. event.Publish(event.Event{
  249. Type: event.EventTypeIndexScanning,
  250. Data: scanning,
  251. })
  252. }
  253. }
  254. // executeCallbacks runs all registered callbacks
  255. func (s *Scanner) executeCallbacks(filePath string, content []byte) {
  256. scanCallbacksMutex.RLock()
  257. defer scanCallbacksMutex.RUnlock()
  258. for _, callback := range scanCallbacks {
  259. if err := callback(filePath, content); err != nil {
  260. logger.Error("Callback error for", filePath, ":", err)
  261. }
  262. }
  263. }
  264. // ScanAllConfigs scans all nginx configuration files
  265. func (s *Scanner) ScanAllConfigs() error {
  266. s.setScanningState(true)
  267. defer s.setScanningState(false)
  268. root := nginx.GetConfPath()
  269. // Scan all files in the config directory and subdirectories
  270. return filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
  271. if err != nil {
  272. return err
  273. }
  274. // Skip excluded directories (ssl, cache, logs, temp, etc.)
  275. if d.IsDir() && shouldSkipPath(path) {
  276. return filepath.SkipDir
  277. }
  278. // Only process regular files
  279. if !d.IsDir() {
  280. if err := s.scanSingleFile(path); err != nil {
  281. logger.Error("Failed to scan config:", path, err)
  282. }
  283. }
  284. return nil
  285. })
  286. }
  287. // Shutdown cleans up scanner resources
  288. func (s *Scanner) Shutdown() {
  289. if s.watcher != nil {
  290. s.watcher.Close()
  291. }
  292. if s.scanTicker != nil {
  293. s.scanTicker.Stop()
  294. }
  295. }
  296. // IsScanningInProgress returns whether a scan is currently running
  297. func IsScanningInProgress() bool {
  298. s := GetScanner()
  299. s.scanMutex.RLock()
  300. defer s.scanMutex.RUnlock()
  301. return s.scanning
  302. }