1
0

generic_list.go 9.4 KB

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