123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307 |
- package config
- import (
- "context"
- "os"
- "path/filepath"
- "strings"
- "github.com/0xJacky/Nginx-UI/internal/cache"
- "github.com/0xJacky/Nginx-UI/internal/nginx"
- "github.com/0xJacky/Nginx-UI/model"
- "github.com/samber/lo"
- "github.com/uozi-tech/cosy/logger"
- )
- // GenericListOptions represents the options for listing configurations
- type GenericListOptions struct {
- Search string
- Status string
- OrderBy string
- Sort string
- EnvGroupID uint64
- IncludeDirs bool // Whether to include directories in the results, default is false (filter out directories)
- }
- // ConfigEntity represents a generic configuration entity interface
- type ConfigEntity interface {
- GetPath() string
- GetEnvGroupID() uint64
- GetEnvGroup() *model.EnvGroup
- }
- // ConfigPaths holds the directory paths for available and enabled configurations
- type ConfigPaths struct {
- AvailableDir string
- EnabledDir string
- }
- // StatusMapBuilder is a function type for building status maps with custom logic
- type StatusMapBuilder func(configFiles, enabledConfig []os.DirEntry) map[string]ConfigStatus
- // ConfigBuilder is a function type for building Config objects with custom logic
- type ConfigBuilder func(fileName string, fileInfo os.FileInfo, status ConfigStatus, envGroupID uint64, envGroup *model.EnvGroup) Config
- // FilterMatcher is a function type for custom filtering logic
- type FilterMatcher func(fileName string, status ConfigStatus, envGroupID uint64, options *GenericListOptions) bool
- // GenericConfigProcessor holds all the custom functions for processing configurations
- type GenericConfigProcessor struct {
- Paths ConfigPaths
- StatusMapBuilder StatusMapBuilder
- ConfigBuilder ConfigBuilder
- FilterMatcher FilterMatcher
- }
- // GetGenericConfigs is a unified function for retrieving and processing configurations
- func GetGenericConfigs[T ConfigEntity](
- ctx context.Context,
- options *GenericListOptions,
- entities []T,
- processor *GenericConfigProcessor,
- ) ([]Config, error) {
- // Read configuration directories
- configFiles, err := os.ReadDir(nginx.GetConfPath(processor.Paths.AvailableDir))
- if err != nil {
- return nil, err
- }
- enabledConfig, err := os.ReadDir(nginx.GetConfPath(processor.Paths.EnabledDir))
- if err != nil {
- return nil, err
- }
- // Build configuration status map using custom logic
- statusMap := processor.StatusMapBuilder(configFiles, enabledConfig)
- // Create entities map for quick lookup
- entitiesMap := lo.SliceToMap(entities, func(item T) (string, T) {
- return filepath.Base(item.GetPath()), item
- })
- // If fuzzy search is enabled, use search index to filter files
- var searchFilteredFiles []string
- var hasSearchResults bool
- if options.Search != "" {
- logger.Debugf("Starting fuzzy search for query '%s' in directory '%s'", options.Search, processor.Paths.AvailableDir)
- searchFilteredFiles, err = performFuzzySearch(ctx, options.Search, processor.Paths.AvailableDir)
- if err != nil {
- // Fallback to original behavior if search fails
- logger.Debugf("Fuzzy search failed, falling back to simple string matching: %v", err)
- searchFilteredFiles = nil
- hasSearchResults = false
- } else {
- hasSearchResults = true
- logger.Debugf("Fuzzy search completed, found %d matching files", len(searchFilteredFiles))
- }
- }
- // Process and filter configurations
- var configs []Config
- for _, file := range configFiles {
- if file.IsDir() && !options.IncludeDirs {
- continue
- }
- fileInfo, err := file.Info()
- if err != nil {
- continue
- }
- fileName := file.Name()
- status := statusMap[fileName]
- // Get environment group info from database
- var envGroupID uint64
- var envGroup *model.EnvGroup
- if entity, ok := entitiesMap[fileName]; ok {
- envGroupID = entity.GetEnvGroupID()
- envGroup = entity.GetEnvGroup()
- }
- // Apply filters using custom logic
- if !processor.FilterMatcher(fileName, status, envGroupID, options) {
- continue
- }
- // Apply fuzzy search filter if enabled
- if hasSearchResults {
- // Check if the file is in the search results
- if !contains(searchFilteredFiles, fileName) {
- // For directories, perform simple string matching since they are not indexed
- if fileInfo.IsDir() {
- // Only include directories if IncludeDirs is true and they match the search
- if options.IncludeDirs {
- // Perform case-insensitive substring matching for directories
- if !strings.Contains(strings.ToLower(fileName), strings.ToLower(options.Search)) {
- continue
- }
- } else {
- // Directories should have been filtered out earlier, but skip just in case
- continue
- }
- } else {
- // For regular files, if they're not in the search results, skip them
- continue
- }
- }
- } else if options.Search != "" {
- // Fallback to simple string matching if search index failed or returned no results
- if !strings.Contains(strings.ToLower(fileName), strings.ToLower(options.Search)) {
- continue
- }
- }
- // Build configuration using custom logic
- configs = append(configs, processor.ConfigBuilder(fileName, fileInfo, status, envGroupID, envGroup))
- }
- // Sort and return
- sortedConfigs := Sort(options.OrderBy, options.Sort, configs)
- // Debug log the final results
- if options.Search != "" {
- logger.Debugf("Final search results for query '%s': returning %d configs out of %d total files",
- options.Search, len(sortedConfigs), len(configFiles))
- }
- return sortedConfigs, nil
- }
- // performFuzzySearch performs fuzzy search using the search index
- func performFuzzySearch(ctx context.Context, query, availableDir string) ([]string, error) {
- // Determine search type based on directory
- var searchType string
- switch {
- case strings.Contains(availableDir, "sites"):
- searchType = "site"
- case strings.Contains(availableDir, "streams"):
- searchType = "stream"
- default:
- searchType = "config"
- }
- // Perform search with the determined type
- var results []cache.SearchResult
- var err error
- // Use a larger limit to ensure we get all matching results
- // Since we're filtering by filename, we want to get all possible matches
- // Set a reasonable upper limit to prevent performance issues
- searchLimit := 5000
- if searchType != "" {
- results, err = cache.GetSearchIndexer().SearchByType(ctx, query, searchType, searchLimit)
- } else {
- results, err = cache.GetSearchIndexer().Search(ctx, query, searchLimit)
- }
- if err != nil {
- return nil, err
- }
- // Extract filenames from search results
- var filenames []string
- for _, result := range results {
- filename := filepath.Base(result.Document.Path)
- filenames = append(filenames, filename)
- }
- // Debug log the search results
- logger.Debugf("Search engine returned files for query '%s' in dir '%s': %v (total: %d)",
- query, availableDir, filenames, len(filenames))
- return filenames, nil
- }
- // contains checks if a string slice contains a specific string
- func contains(slice []string, item string) bool {
- for _, s := range slice {
- if s == item {
- return true
- }
- }
- return false
- }
- // DefaultStatusMapBuilder provides the basic status map building logic
- func DefaultStatusMapBuilder(configFiles, enabledConfig []os.DirEntry) map[string]ConfigStatus {
- statusMap := make(map[string]ConfigStatus)
- // Initialize all as disabled
- for _, file := range configFiles {
- statusMap[file.Name()] = StatusDisabled
- }
- // Update enabled status
- for _, enabledFile := range enabledConfig {
- name := nginx.GetConfNameBySymlinkName(enabledFile.Name())
- statusMap[name] = StatusEnabled
- }
- return statusMap
- }
- // SiteStatusMapBuilder provides status map building logic with maintenance support
- func SiteStatusMapBuilder(maintenanceSuffix string) StatusMapBuilder {
- return func(configFiles, enabledConfig []os.DirEntry) map[string]ConfigStatus {
- statusMap := make(map[string]ConfigStatus)
- // Initialize all as disabled
- for _, file := range configFiles {
- statusMap[file.Name()] = StatusDisabled
- }
- // Update enabled and maintenance status
- for _, enabledSite := range enabledConfig {
- name := enabledSite.Name()
- if strings.HasSuffix(name, maintenanceSuffix) {
- originalName := strings.TrimSuffix(name, maintenanceSuffix)
- statusMap[originalName] = StatusMaintenance
- } else {
- statusMap[nginx.GetConfNameBySymlinkName(name)] = StatusEnabled
- }
- }
- return statusMap
- }
- }
- // DefaultFilterMatcher provides the standard filtering logic without name search
- func DefaultFilterMatcher(fileName string, status ConfigStatus, envGroupID uint64, options *GenericListOptions) bool {
- // Remove name filtering as it's now handled by fuzzy search
- if options.Status != "" && status != ConfigStatus(options.Status) {
- return false
- }
- if options.EnvGroupID != 0 && envGroupID != options.EnvGroupID {
- return false
- }
- return true
- }
- // FuzzyFilterMatcher provides filtering logic with fuzzy search support
- func FuzzyFilterMatcher(fileName string, status ConfigStatus, envGroupID uint64, options *GenericListOptions) bool {
- // Name filtering is handled by fuzzy search in GetGenericConfigs
- // Only apply other filters here
- if options.Status != "" && status != ConfigStatus(options.Status) {
- return false
- }
- if options.EnvGroupID != 0 && envGroupID != options.EnvGroupID {
- return false
- }
- return true
- }
- // DefaultConfigBuilder provides basic config building logic
- func DefaultConfigBuilder(fileName string, fileInfo os.FileInfo, status ConfigStatus, envGroupID uint64, envGroup *model.EnvGroup) Config {
- return Config{
- Name: fileName,
- ModifiedAt: fileInfo.ModTime(),
- Size: fileInfo.Size(),
- IsDir: fileInfo.IsDir(),
- Status: status,
- EnvGroupID: envGroupID,
- EnvGroup: envGroup,
- }
- }
|