ultisuite-backend/internal/nextcloud/search.go
R3D347HR4Y 556d5f416d Enhance API and configuration for contact discovery and public sharing
- Introduced new endpoints for contact discovery, including scanning, listing, and managing discovered contacts.
- Implemented retry logic for handling missing DAV credentials during contact operations.
- Added public share functionality for drive API, allowing users to manage public shares, including upload, delete, and rename operations.
- Updated Nextcloud configuration to support public share links and improved error handling for public share permissions.
- Enhanced logging and validation across contact and drive APIs for better error tracking and user feedback.
- Added tests for new contact matching and ranking functionalities to ensure accuracy and reliability.
2026-06-06 20:27:02 +02:00

208 lines
4.1 KiB
Go

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)
})
}