generic_list.go 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. package config
  2. import (
  3. "context"
  4. "os"
  5. "path/filepath"
  6. "strings"
  7. "github.com/0xJacky/Nginx-UI/internal/cache"
  8. "github.com/0xJacky/Nginx-UI/internal/nginx"
  9. "github.com/0xJacky/Nginx-UI/model"
  10. "github.com/samber/lo"
  11. "github.com/uozi-tech/cosy/logger"
  12. )
  13. // GenericListOptions represents the options for listing configurations
  14. type GenericListOptions struct {
  15. Search string
  16. Name string
  17. Status string
  18. OrderBy string
  19. Sort string
  20. NamespaceID uint64
  21. IncludeDirs bool // Whether to include directories in the results, default is false (filter out directories)
  22. }
  23. // ConfigEntity represents a generic configuration entity interface
  24. type ConfigEntity interface {
  25. GetPath() string
  26. GetNamespaceID() uint64
  27. GetNamespace() *model.Namespace
  28. }
  29. // ConfigPaths holds the directory paths for available and enabled configurations
  30. type ConfigPaths struct {
  31. AvailableDir string
  32. EnabledDir string
  33. }
  34. // StatusMapBuilder is a function type for building status maps with custom logic
  35. type StatusMapBuilder func(configFiles, enabledConfig []os.DirEntry) map[string]ConfigStatus
  36. // ConfigBuilder is a function type for building Config objects with custom logic
  37. type ConfigBuilder func(fileName string, fileInfo os.FileInfo, status ConfigStatus, namespaceID uint64, namespace *model.Namespace) Config
  38. // FilterMatcher is a function type for custom filtering logic
  39. type FilterMatcher func(fileName string, status ConfigStatus, namespaceID uint64, options *GenericListOptions) bool
  40. // GenericConfigProcessor holds all the custom functions for processing configurations
  41. type GenericConfigProcessor struct {
  42. Paths ConfigPaths
  43. StatusMapBuilder StatusMapBuilder
  44. ConfigBuilder ConfigBuilder
  45. FilterMatcher FilterMatcher
  46. }
  47. // GetGenericConfigs is a unified function for retrieving and processing configurations
  48. func GetGenericConfigs[T ConfigEntity](
  49. ctx context.Context,
  50. options *GenericListOptions,
  51. entities []T,
  52. processor *GenericConfigProcessor,
  53. ) ([]Config, error) {
  54. // Read configuration directories
  55. configFiles, err := os.ReadDir(nginx.GetConfPath(processor.Paths.AvailableDir))
  56. if err != nil {
  57. return nil, err
  58. }
  59. enabledConfig, err := os.ReadDir(nginx.GetConfPath(processor.Paths.EnabledDir))
  60. if err != nil {
  61. return nil, err
  62. }
  63. // Build configuration status map using custom logic
  64. statusMap := processor.StatusMapBuilder(configFiles, enabledConfig)
  65. // Create entities map for quick lookup
  66. entitiesMap := lo.SliceToMap(entities, func(item T) (string, T) {
  67. return filepath.Base(item.GetPath()), item
  68. })
  69. // If fuzzy search is enabled, use search index to filter files
  70. var searchFilteredFiles []string
  71. var hasSearchResults bool
  72. if options.Search != "" {
  73. logger.Debugf("Starting fuzzy search for query '%s' in directory '%s'", options.Search, processor.Paths.AvailableDir)
  74. searchFilteredFiles, err = performFuzzySearch(ctx, options.Search, processor.Paths.AvailableDir)
  75. if err != nil {
  76. // Fallback to original behavior if search fails
  77. logger.Debugf("Fuzzy search failed, falling back to simple string matching: %v", err)
  78. searchFilteredFiles = nil
  79. hasSearchResults = false
  80. } else {
  81. hasSearchResults = true
  82. logger.Debugf("Fuzzy search completed, found %d matching files", len(searchFilteredFiles))
  83. }
  84. }
  85. // Process and filter configurations
  86. var configs []Config
  87. for _, file := range configFiles {
  88. if file.IsDir() && !options.IncludeDirs {
  89. continue
  90. }
  91. fileInfo, err := file.Info()
  92. if err != nil {
  93. continue
  94. }
  95. fileName := file.Name()
  96. status := statusMap[fileName]
  97. // Get environment group info from database
  98. var namespaceID uint64
  99. var namespace *model.Namespace
  100. if entity, ok := entitiesMap[fileName]; ok {
  101. namespaceID = entity.GetNamespaceID()
  102. namespace = entity.GetNamespace()
  103. }
  104. // Apply filters using custom logic
  105. if !processor.FilterMatcher(fileName, status, namespaceID, options) {
  106. continue
  107. }
  108. // Apply fuzzy search filter if enabled
  109. if hasSearchResults {
  110. // Check if the file is in the search results
  111. if !contains(searchFilteredFiles, fileName) {
  112. // For directories, perform simple string matching since they are not indexed
  113. if fileInfo.IsDir() {
  114. // Only include directories if IncludeDirs is true and they match the search
  115. if options.IncludeDirs {
  116. // Perform case-insensitive substring matching for directories
  117. if !strings.Contains(strings.ToLower(fileName), strings.ToLower(options.Search)) {
  118. continue
  119. }
  120. } else {
  121. // Directories should have been filtered out earlier, but skip just in case
  122. continue
  123. }
  124. } else {
  125. // For regular files, if they're not in the search results, skip them
  126. continue
  127. }
  128. }
  129. } else if options.Search != "" {
  130. // Fallback to simple string matching if search index failed or returned no results
  131. if !strings.Contains(strings.ToLower(fileName), strings.ToLower(options.Search)) {
  132. continue
  133. }
  134. }
  135. // Build configuration using custom logic
  136. configs = append(configs, processor.ConfigBuilder(fileName, fileInfo, status, namespaceID, namespace))
  137. }
  138. // Sort and return
  139. sortedConfigs := Sort(options.OrderBy, options.Sort, configs)
  140. // Debug log the final results
  141. if options.Search != "" {
  142. logger.Debugf("Final search results for query '%s': returning %d configs out of %d total files",
  143. options.Search, len(sortedConfigs), len(configFiles))
  144. }
  145. return sortedConfigs, nil
  146. }
  147. // performFuzzySearch performs fuzzy search using the search index
  148. func performFuzzySearch(ctx context.Context, query, availableDir string) ([]string, error) {
  149. // Determine search type based on directory
  150. var searchType string
  151. switch {
  152. case strings.Contains(availableDir, "sites"):
  153. searchType = "site"
  154. case strings.Contains(availableDir, "streams"):
  155. searchType = "stream"
  156. default:
  157. searchType = "config"
  158. }
  159. // Perform search with the determined type
  160. var results []cache.SearchResult
  161. var err error
  162. // Use a larger limit to ensure we get all matching results
  163. // Since we're filtering by filename, we want to get all possible matches
  164. // Set a reasonable upper limit to prevent performance issues
  165. searchLimit := 5000
  166. if searchType != "" {
  167. results, err = cache.GetSearchIndexer().SearchByType(ctx, query, searchType, searchLimit)
  168. } else {
  169. results, err = cache.GetSearchIndexer().Search(ctx, query, searchLimit)
  170. }
  171. if err != nil {
  172. return nil, err
  173. }
  174. // Extract filenames from search results
  175. var filenames []string
  176. for _, result := range results {
  177. filename := filepath.Base(result.Document.Path)
  178. filenames = append(filenames, filename)
  179. }
  180. // Debug log the search results
  181. logger.Debugf("Search engine returned files for query '%s' in dir '%s': %v (total: %d)",
  182. query, availableDir, filenames, len(filenames))
  183. return filenames, nil
  184. }
  185. // contains checks if a string slice contains a specific string
  186. func contains(slice []string, item string) bool {
  187. for _, s := range slice {
  188. if s == item {
  189. return true
  190. }
  191. }
  192. return false
  193. }
  194. // DefaultStatusMapBuilder provides the basic status map building logic
  195. func DefaultStatusMapBuilder(configFiles, enabledConfig []os.DirEntry) map[string]ConfigStatus {
  196. statusMap := make(map[string]ConfigStatus)
  197. // Initialize all as disabled
  198. for _, file := range configFiles {
  199. statusMap[file.Name()] = StatusDisabled
  200. }
  201. // Update enabled status
  202. for _, enabledFile := range enabledConfig {
  203. name := nginx.GetConfNameBySymlinkName(enabledFile.Name())
  204. statusMap[name] = StatusEnabled
  205. }
  206. return statusMap
  207. }
  208. // SiteStatusMapBuilder provides status map building logic with maintenance support
  209. func SiteStatusMapBuilder(maintenanceSuffix string) StatusMapBuilder {
  210. return func(configFiles, enabledConfig []os.DirEntry) map[string]ConfigStatus {
  211. statusMap := make(map[string]ConfigStatus)
  212. // Initialize all as disabled
  213. for _, file := range configFiles {
  214. statusMap[file.Name()] = StatusDisabled
  215. }
  216. // Update enabled and maintenance status
  217. for _, enabledSite := range enabledConfig {
  218. name := enabledSite.Name()
  219. if strings.HasSuffix(name, maintenanceSuffix) {
  220. originalName := strings.TrimSuffix(name, maintenanceSuffix)
  221. statusMap[originalName] = StatusMaintenance
  222. } else {
  223. statusMap[nginx.GetConfNameBySymlinkName(name)] = StatusEnabled
  224. }
  225. }
  226. return statusMap
  227. }
  228. }
  229. // DefaultFilterMatcher provides the standard filtering logic with name search
  230. func DefaultFilterMatcher(fileName string, status ConfigStatus, namespaceID uint64, options *GenericListOptions) bool {
  231. // Exact name matching
  232. if options.Name != "" && fileName != options.Name {
  233. return false
  234. }
  235. if options.Status != "" && status != ConfigStatus(options.Status) {
  236. return false
  237. }
  238. if options.NamespaceID != 0 && namespaceID != options.NamespaceID {
  239. return false
  240. }
  241. return true
  242. }
  243. // FuzzyFilterMatcher provides filtering logic with fuzzy search support
  244. func FuzzyFilterMatcher(fileName string, status ConfigStatus, namespaceID uint64, options *GenericListOptions) bool {
  245. // Exact name matching takes precedence over fuzzy search
  246. if options.Name != "" && fileName != options.Name {
  247. return false
  248. }
  249. if options.Status != "" && status != ConfigStatus(options.Status) {
  250. return false
  251. }
  252. if options.NamespaceID != 0 && namespaceID != options.NamespaceID {
  253. return false
  254. }
  255. return true
  256. }
  257. // DefaultConfigBuilder provides basic config building logic
  258. func DefaultConfigBuilder(fileName string, fileInfo os.FileInfo, status ConfigStatus, namespaceID uint64, namespace *model.Namespace) Config {
  259. return Config{
  260. Name: fileName,
  261. ModifiedAt: fileInfo.ModTime(),
  262. Size: fileInfo.Size(),
  263. IsDir: fileInfo.IsDir(),
  264. Status: status,
  265. NamespaceID: namespaceID,
  266. Namespace: namespace,
  267. }
  268. }