package nextcloud import ( "context" "sort" "strings" ) const ( searchSuggestLimit = 8 searchDefaultLimit = 100 searchSuggestMaxDirs = 400 searchFullMaxDirs = 2000 searchMaxCollect = 500 ) type SearchScope string const ( SearchScopeAll SearchScope = "all" SearchScopeShared SearchScope = "shared" SearchScopeFolder SearchScope = "folder" ) type SearchOptions struct { Query string Scope SearchScope BasePath string Suggest bool Limit int MaxDirs int } func (c *Client) SearchFiles(ctx context.Context, userID string, opts SearchOptions) ([]FileInfo, error) { q := strings.ToLower(strings.TrimSpace(opts.Query)) if q == "" { return []FileInfo{}, nil } limit := opts.Limit if limit <= 0 { if opts.Suggest { limit = searchSuggestLimit } else { limit = searchDefaultLimit } } maxDirs := opts.MaxDirs if maxDirs <= 0 { if opts.Suggest { maxDirs = searchSuggestMaxDirs } else { maxDirs = searchFullMaxDirs } } scope := opts.Scope if scope == "" { scope = SearchScopeAll } var roots []string switch scope { case SearchScopeShared: shared, err := c.ListSharedWithMe(ctx, userID) if err != nil { return nil, err } roots = make([]string, 0, len(shared)) for _, item := range shared { roots = append(roots, item.Path) } case SearchScopeFolder: base := normalizeSearchPath(opts.BasePath) if base == "" { base = "/" } roots = []string{base} default: roots = []string{"/"} } results, err := c.searchRecursive(ctx, userID, roots, q, limit, maxDirs) if err != nil { return nil, err } sortSearchResults(results, q) if len(results) > limit { results = results[:limit] } return results, nil } func normalizeSearchPath(path string) string { path = strings.TrimSpace(path) if path == "" || path == "/" { return "/" } if !strings.HasPrefix(path, "/") { path = "/" + path } return strings.TrimSuffix(path, "/") } func fileMatchesQuery(f FileInfo, q string) bool { return strings.Contains(strings.ToLower(f.Name), q) || strings.Contains(strings.ToLower(f.Path), q) } func isDirectoryEntry(f FileInfo) bool { if f.Type == "directory" { return true } return strings.HasPrefix(strings.ToLower(f.MimeType), "httpd/unix-directory") } func (c *Client) searchRecursive( ctx context.Context, userID string, roots []string, q string, limit, maxDirs int, ) ([]FileInfo, error) { queue := make([]string, 0, len(roots)) seen := make(map[string]struct{}, len(roots)) for _, root := range roots { root = normalizeSearchPath(root) if root == "" { continue } if _, ok := seen[root]; ok { continue } seen[root] = struct{}{} queue = append(queue, root) } results := make([]FileInfo, 0, limit) visited := 0 collectCap := searchMaxCollect if limit*5 > collectCap { collectCap = limit * 5 } for len(queue) > 0 && visited < maxDirs && len(results) < collectCap { dir := queue[0] queue = queue[1:] visited++ files, err := c.ListFiles(ctx, userID, dir) if err != nil { continue } for _, f := range files { if fileMatchesQuery(f, q) { results = append(results, f) if len(results) >= collectCap { break } } if !isDirectoryEntry(f) { continue } child := normalizeSearchPath(f.Path) if child == "" || child == "/" { continue } if _, ok := seen[child]; ok { continue } seen[child] = struct{}{} queue = append(queue, child) } } return results, nil } func searchMatchScore(f FileInfo, q string) int { name := strings.ToLower(f.Name) path := strings.ToLower(f.Path) switch { case name == q: return 1000 case strings.HasPrefix(name, q): return 800 case strings.Contains(name, q): return 600 case strings.Contains(path, q): return 400 default: return 0 } } func sortSearchResults(files []FileInfo, q string) { sort.SliceStable(files, func(i, j int) bool { si := searchMatchScore(files[i], q) sj := searchMatchScore(files[j], q) if si != sj { return si > sj } if len(files[i].Path) != len(files[j].Path) { return len(files[i].Path) < len(files[j].Path) } return strings.ToLower(files[i].Name) < strings.ToLower(files[j].Name) }) }