- 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.
208 lines
4.1 KiB
Go
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)
|
|
})
|
|
}
|