index.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454
  1. package cache
  2. import (
  3. "context"
  4. "fmt"
  5. "io/fs"
  6. "os"
  7. "path/filepath"
  8. "strings"
  9. "sync"
  10. "time"
  11. "github.com/0xJacky/Nginx-UI/internal/event"
  12. "github.com/0xJacky/Nginx-UI/internal/nginx"
  13. "github.com/fsnotify/fsnotify"
  14. "github.com/uozi-tech/cosy/logger"
  15. )
  16. // ScanCallback is called during config scanning with file path and content
  17. type ScanCallback func(configPath string, content []byte) error
  18. // Scanner watches and scans nginx config files
  19. type Scanner struct {
  20. ctx context.Context
  21. watcher *fsnotify.Watcher
  22. scanTicker *time.Ticker
  23. scanning bool
  24. scanMutex sync.RWMutex
  25. }
  26. var (
  27. scanner *Scanner
  28. scannerInitMutex sync.Mutex
  29. scanCallbacks = make([]ScanCallback, 0)
  30. scanCallbacksMutex sync.RWMutex
  31. )
  32. // InitScanner initializes the config scanner
  33. func InitScanner(ctx context.Context) {
  34. if nginx.GetConfPath() == "" {
  35. logger.Error("Nginx config path is not set")
  36. return
  37. }
  38. scanner := GetScanner()
  39. if err := scanner.Initialize(ctx); err != nil {
  40. logger.Error("Failed to initialize config scanner:", err)
  41. }
  42. }
  43. // shouldSkipPath checks if a path should be skipped during scanning or watching
  44. func shouldSkipPath(path string) bool {
  45. // Define directories to exclude from scanning/watching
  46. excludedDirs := []string{
  47. nginx.GetConfPath("ssl"), // SSL certificates and keys
  48. nginx.GetConfPath("cache"), // Nginx cache files
  49. nginx.GetConfPath("logs"), // Log files directory
  50. nginx.GetConfPath("temp"), // Temporary files directory
  51. nginx.GetConfPath("proxy_temp"), // Proxy temporary files
  52. nginx.GetConfPath("client_body_temp"), // Client body temporary files
  53. nginx.GetConfPath("fastcgi_temp"), // FastCGI temporary files
  54. nginx.GetConfPath("uwsgi_temp"), // uWSGI temporary files
  55. nginx.GetConfPath("scgi_temp"), // SCGI temporary files
  56. }
  57. // Check if path starts with any excluded directory
  58. for _, excludedDir := range excludedDirs {
  59. if excludedDir != "" && strings.HasPrefix(path, excludedDir) {
  60. return true
  61. }
  62. }
  63. return false
  64. }
  65. // GetScanner returns the singleton scanner instance
  66. func GetScanner() *Scanner {
  67. scannerInitMutex.Lock()
  68. defer scannerInitMutex.Unlock()
  69. if scanner == nil {
  70. scanner = &Scanner{}
  71. }
  72. return scanner
  73. }
  74. // RegisterCallback adds a callback to be executed during scans
  75. func RegisterCallback(callback ScanCallback) {
  76. scanCallbacksMutex.Lock()
  77. defer scanCallbacksMutex.Unlock()
  78. scanCallbacks = append(scanCallbacks, callback)
  79. }
  80. // Initialize sets up the scanner and starts watching
  81. func (s *Scanner) Initialize(ctx context.Context) error {
  82. watcher, err := fsnotify.NewWatcher()
  83. if err != nil {
  84. return err
  85. }
  86. s.watcher = watcher
  87. s.ctx = ctx
  88. // Initial scan
  89. if err := s.ScanAllConfigs(); err != nil {
  90. return err
  91. }
  92. // Watch all directories recursively
  93. if err := s.watchAllDirectories(); err != nil {
  94. return err
  95. }
  96. // Start background processes
  97. go s.watchForChanges()
  98. go s.periodicScan()
  99. go s.handleShutdown()
  100. return nil
  101. }
  102. // watchAllDirectories recursively adds all directories under nginx config path to watcher
  103. func (s *Scanner) watchAllDirectories() error {
  104. root := nginx.GetConfPath()
  105. return filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
  106. if err != nil {
  107. return err
  108. }
  109. if d.IsDir() {
  110. // Skip excluded directories (ssl, cache, logs, temp, etc.)
  111. if shouldSkipPath(path) {
  112. return filepath.SkipDir
  113. }
  114. // Resolve symlinks to get the actual directory path to watch
  115. actualPath := path
  116. if d.Type()&os.ModeSymlink != 0 {
  117. // This is a symlink, resolve it to get the target path
  118. if resolvedPath, err := filepath.EvalSymlinks(path); err == nil {
  119. actualPath = resolvedPath
  120. logger.Debug("Resolved symlink for watching:", path, "->", actualPath)
  121. } else {
  122. logger.Debug("Failed to resolve symlink, skipping:", path, err)
  123. return filepath.SkipDir
  124. }
  125. }
  126. if err := s.watcher.Add(actualPath); err != nil {
  127. logger.Error("Failed to watch directory:", actualPath, err)
  128. return err
  129. }
  130. logger.Debug("Watching directory:", actualPath)
  131. }
  132. return nil
  133. })
  134. }
  135. // periodicScan runs periodic scans every 5 minutes
  136. func (s *Scanner) periodicScan() {
  137. s.scanTicker = time.NewTicker(5 * time.Minute)
  138. defer s.scanTicker.Stop()
  139. for {
  140. select {
  141. case <-s.ctx.Done():
  142. return
  143. case <-s.scanTicker.C:
  144. if err := s.ScanAllConfigs(); err != nil {
  145. logger.Error("Periodic scan failed:", err)
  146. }
  147. }
  148. }
  149. }
  150. // handleShutdown listens for context cancellation and shuts down gracefully
  151. func (s *Scanner) handleShutdown() {
  152. <-s.ctx.Done()
  153. logger.Info("Shutting down Index Scanner")
  154. s.Shutdown()
  155. }
  156. // watchForChanges handles file system events
  157. func (s *Scanner) watchForChanges() {
  158. for {
  159. select {
  160. case <-s.ctx.Done():
  161. return
  162. case event, ok := <-s.watcher.Events:
  163. if !ok {
  164. return
  165. }
  166. s.handleFileEvent(event)
  167. case err, ok := <-s.watcher.Errors:
  168. if !ok {
  169. return
  170. }
  171. logger.Error("Watcher error:", err)
  172. }
  173. }
  174. }
  175. // handleFileEvent processes individual file system events
  176. func (s *Scanner) handleFileEvent(event fsnotify.Event) {
  177. // Only handle relevant events
  178. if !event.Has(fsnotify.Create) && !event.Has(fsnotify.Write) &&
  179. !event.Has(fsnotify.Rename) && !event.Has(fsnotify.Remove) {
  180. return
  181. }
  182. // Skip excluded directories (ssl, cache, etc.)
  183. if shouldSkipPath(event.Name) {
  184. return
  185. }
  186. // Add new directories to watch
  187. if event.Has(fsnotify.Create) {
  188. if fi, err := os.Stat(event.Name); err == nil && fi.IsDir() {
  189. if err := s.watcher.Add(event.Name); err != nil {
  190. logger.Error("Failed to add new directory to watcher:", event.Name, err)
  191. } else {
  192. logger.Debug("Added new directory to watcher:", event.Name)
  193. }
  194. }
  195. }
  196. // Handle file changes
  197. if event.Has(fsnotify.Remove) {
  198. logger.Debug("Config removed:", event.Name)
  199. return
  200. }
  201. // Use Lstat to get symlink info without following it
  202. fi, err := os.Lstat(event.Name)
  203. if err != nil {
  204. return
  205. }
  206. // If it's a symlink, we need to check what it points to
  207. var targetIsDir bool
  208. if fi.Mode()&os.ModeSymlink != 0 {
  209. // For symlinks, check the target
  210. targetFi, err := os.Stat(event.Name)
  211. if err != nil {
  212. logger.Debug("Symlink target not accessible:", event.Name, err)
  213. return
  214. }
  215. targetIsDir = targetFi.IsDir()
  216. logger.Debug("Symlink changed:", event.Name, "-> target is dir:", targetIsDir)
  217. } else {
  218. targetIsDir = fi.IsDir()
  219. }
  220. if targetIsDir {
  221. logger.Debug("Directory changed:", event.Name)
  222. } else {
  223. logger.Debug("File changed:", event.Name)
  224. time.Sleep(100 * time.Millisecond) // Allow file write to complete
  225. s.scanSingleFile(event.Name)
  226. }
  227. }
  228. // scanSingleFile scans a single config file without recursion
  229. func (s *Scanner) scanSingleFile(filePath string) error {
  230. s.setScanningState(true)
  231. defer s.setScanningState(false)
  232. // Check if path should be skipped
  233. if shouldSkipPath(filePath) {
  234. return nil
  235. }
  236. // Get file info to check type and size
  237. fileInfo, err := os.Lstat(filePath) // Use Lstat to avoid following symlinks
  238. if err != nil {
  239. return err
  240. }
  241. // Skip directories
  242. if fileInfo.IsDir() {
  243. logger.Debugf("Skipping directory: %s", filePath)
  244. return nil
  245. }
  246. // Handle symlinks carefully
  247. if fileInfo.Mode()&os.ModeSymlink != 0 {
  248. // Check what the symlink points to
  249. targetInfo, err := os.Stat(filePath)
  250. if err != nil {
  251. logger.Debugf("Skipping symlink with inaccessible target: %s (%v)", filePath, err)
  252. return nil
  253. }
  254. // Skip symlinks to directories
  255. if targetInfo.IsDir() {
  256. logger.Debugf("Skipping symlink to directory: %s", filePath)
  257. return nil
  258. }
  259. // Process symlinks to files, but use the target's info for size check
  260. fileInfo = targetInfo
  261. logger.Debugf("Processing symlink to file: %s", filePath)
  262. }
  263. // Skip non-regular files (devices, pipes, sockets, etc.)
  264. if !fileInfo.Mode().IsRegular() {
  265. logger.Debugf("Skipping non-regular file: %s (mode: %s)", filePath, fileInfo.Mode())
  266. return nil
  267. }
  268. // Skip files larger than 1MB before reading
  269. if fileInfo.Size() > 1024*1024 {
  270. logger.Debugf("Skipping large file: %s (size: %d bytes)", filePath, fileInfo.Size())
  271. return nil
  272. }
  273. // Read file content
  274. content, err := os.ReadFile(filePath)
  275. if err != nil {
  276. return err
  277. }
  278. // Execute callbacks
  279. s.executeCallbacks(filePath, content)
  280. return nil
  281. }
  282. // setScanningState updates the scanning state and publishes events
  283. func (s *Scanner) setScanningState(scanning bool) {
  284. s.scanMutex.Lock()
  285. defer s.scanMutex.Unlock()
  286. if s.scanning != scanning {
  287. s.scanning = scanning
  288. event.Publish(event.Event{
  289. Type: event.EventTypeIndexScanning,
  290. Data: scanning,
  291. })
  292. }
  293. }
  294. // executeCallbacks runs all registered callbacks
  295. func (s *Scanner) executeCallbacks(filePath string, content []byte) {
  296. scanCallbacksMutex.RLock()
  297. defer scanCallbacksMutex.RUnlock()
  298. for _, callback := range scanCallbacks {
  299. if err := callback(filePath, content); err != nil {
  300. logger.Error("Callback error for", filePath, ":", err)
  301. }
  302. }
  303. }
  304. // ScanAllConfigs scans all nginx configuration files
  305. func (s *Scanner) ScanAllConfigs() error {
  306. s.setScanningState(true)
  307. defer s.setScanningState(false)
  308. root := nginx.GetConfPath()
  309. // Scan all files in the config directory and subdirectories
  310. return filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
  311. if err != nil {
  312. return err
  313. }
  314. // Skip excluded directories (ssl, cache, logs, temp, etc.)
  315. if d.IsDir() && shouldSkipPath(path) {
  316. return filepath.SkipDir
  317. }
  318. // Handle symlinks to directories specially
  319. if d.Type()&os.ModeSymlink != 0 {
  320. if targetInfo, err := os.Stat(path); err == nil && targetInfo.IsDir() {
  321. // This is a symlink to a directory, we should traverse its contents
  322. // but not process the symlink itself as a file
  323. logger.Debug("Found symlink to directory, will traverse contents:", path)
  324. // Manually scan the symlink target directory since WalkDir doesn't follow symlinks
  325. if err := s.scanSymlinkDirectory(path); err != nil {
  326. logger.Error("Failed to scan symlink directory:", path, err)
  327. }
  328. return nil
  329. }
  330. }
  331. // Only process regular files (not directories, not symlinks to directories)
  332. if !d.IsDir() {
  333. if err := s.scanSingleFile(path); err != nil {
  334. logger.Error("Failed to scan config:", path, err)
  335. }
  336. }
  337. return nil
  338. })
  339. }
  340. // scanSymlinkDirectory recursively scans a symlink directory and its contents
  341. func (s *Scanner) scanSymlinkDirectory(symlinkPath string) error {
  342. // Resolve the symlink to get the actual target path
  343. targetPath, err := filepath.EvalSymlinks(symlinkPath)
  344. if err != nil {
  345. return fmt.Errorf("failed to resolve symlink %s: %w", symlinkPath, err)
  346. }
  347. logger.Debug("Scanning symlink directory contents:", symlinkPath, "->", targetPath)
  348. // Use WalkDir on the resolved target path
  349. return filepath.WalkDir(targetPath, func(path string, d fs.DirEntry, err error) error {
  350. if err != nil {
  351. return err
  352. }
  353. // Skip excluded directories
  354. if d.IsDir() && shouldSkipPath(path) {
  355. return filepath.SkipDir
  356. }
  357. // Only process regular files (not directories, not symlinks to directories)
  358. if !d.IsDir() {
  359. // Handle symlinks to directories (skip them)
  360. if d.Type()&os.ModeSymlink != 0 {
  361. if targetInfo, err := os.Stat(path); err == nil && targetInfo.IsDir() {
  362. logger.Debug("Skipping symlink to directory in symlink scan:", path)
  363. return nil
  364. }
  365. }
  366. if err := s.scanSingleFile(path); err != nil {
  367. logger.Error("Failed to scan config in symlink directory:", path, err)
  368. }
  369. }
  370. return nil
  371. })
  372. }
  373. // Shutdown cleans up scanner resources
  374. func (s *Scanner) Shutdown() {
  375. if s.watcher != nil {
  376. s.watcher.Close()
  377. }
  378. if s.scanTicker != nil {
  379. s.scanTicker.Stop()
  380. }
  381. }
  382. // IsScanningInProgress returns whether a scan is currently running
  383. func IsScanningInProgress() bool {
  384. s := GetScanner()
  385. s.scanMutex.RLock()
  386. defer s.scanMutex.RUnlock()
  387. return s.scanning
  388. }