diff --git a/changelog/unreleased/enhancement-add-search-filter-for-location.md b/changelog/unreleased/enhancement-add-search-filter-for-location.md new file mode 100644 index 0000000000..c1eceb6d4c --- /dev/null +++ b/changelog/unreleased/enhancement-add-search-filter-for-location.md @@ -0,0 +1,8 @@ +Enhancement: Provide Search filter for locations + +The search result REPORT response now can be restricted the by the current folder via api (recursive) +The scope needed for "current folder" (default is to search all available spaces) - part of the oc:pattern:"scope: +/Test" + +https://github.com/owncloud/ocis/pull/6713 +OCIS-3705 diff --git a/services/search/pkg/search/search.go b/services/search/pkg/search/search.go index b4a1ad749c..af4fd54997 100644 --- a/services/search/pkg/search/search.go +++ b/services/search/pkg/search/search.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "regexp" "strings" gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" @@ -13,6 +14,7 @@ import ( ctxpkg "github.com/cs3org/reva/v2/pkg/ctx" "github.com/cs3org/reva/v2/pkg/errtypes" "github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool" + "github.com/cs3org/reva/v2/pkg/storagespace" "github.com/cs3org/reva/v2/pkg/utils" "github.com/owncloud/ocis/v2/ocis-pkg/log" searchmsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/search/v0" @@ -20,6 +22,8 @@ import ( "google.golang.org/grpc/metadata" ) +var scopeRegex = regexp.MustCompile(`scope:\s*([^" "\n\r]*)`) + // ResolveReference makes sure the path is relative to the space root func ResolveReference(ctx context.Context, ref *provider.Reference, ri *provider.ResourceInfo, gatewaySelector pool.Selectable[gateway.GatewayAPIClient]) (*provider.Reference, error) { if ref.GetResourceId().GetOpaqueId() == ref.GetResourceId().GetSpaceId() { @@ -155,3 +159,28 @@ func convertToWebDAVPermissions(isShared, isMountpoint, isDir bool, p *provider. } return b.String() } + +func extractScope(path string) (*searchmsg.Reference, error) { + ref, err := storagespace.ParseReference(path) + if err != nil { + return nil, err + } + return &searchmsg.Reference{ + ResourceId: &searchmsg.ResourceID{ + StorageId: ref.ResourceId.StorageId, + SpaceId: ref.ResourceId.SpaceId, + OpaqueId: ref.ResourceId.OpaqueId, + }, + Path: ref.GetPath(), + }, nil +} + +// ParseScope extract a scope value from the query string and returns search, scope strings +func ParseScope(query string) (string, string) { + match := scopeRegex.FindStringSubmatch(query) + if len(match) >= 2 { + cut := match[0] + return strings.TrimSpace(strings.ReplaceAll(query, cut, "")), strings.TrimSpace(match[1]) + } + return query, "" +} diff --git a/services/search/pkg/search/service.go b/services/search/pkg/search/service.go index f7ae9ca5dc..d17bfb669a 100644 --- a/services/search/pkg/search/service.go +++ b/services/search/pkg/search/service.go @@ -72,10 +72,31 @@ func NewService(gatewaySelector pool.Selectable[gateway.GatewayAPIClient], eng e // Search processes a search request and passes it down to the engine. func (s *Service) Search(ctx context.Context, req *searchsvc.SearchRequest) (*searchsvc.SearchResponse, error) { - if req.Query == "" { + s.logger.Debug().Str("query", req.Query).Msg("performing a search") + query, scope := ParseScope(req.Query) + if query == "" { return nil, errtypes.BadRequest("empty query provided") } - s.logger.Debug().Str("query", req.Query).Msg("performing a search") + req.Query = query + var filters []*provider.ListStorageSpacesRequest_Filter + if len(scope) > 0 { + scopeRef, err := extractScope(scope) + if err != nil { + return nil, err + } + if req.Ref == nil { + req.Ref = scopeRef + } + req.Ref.Path = scopeRef.GetPath() + if scopeRef.GetResourceId().OpaqueId != "" { + filters = []*provider.ListStorageSpacesRequest_Filter{ + { + Type: provider.ListStorageSpacesRequest_Filter_TYPE_ID, + Term: &provider.ListStorageSpacesRequest_Filter_Id{Id: &provider.StorageSpaceId{OpaqueId: scopeRef.GetResourceId().OpaqueId}}, + }, + } + } + } gatewayClient, err := s.gatewaySelector.Next() if err != nil { @@ -84,18 +105,17 @@ func (s *Service) Search(ctx context.Context, req *searchsvc.SearchRequest) (*se currentUser := revactx.ContextMustGetUser(ctx) - listSpacesRes, err := gatewayClient.ListStorageSpaces(ctx, &provider.ListStorageSpacesRequest{ - Filters: []*provider.ListStorageSpacesRequest_Filter{ - { - Type: provider.ListStorageSpacesRequest_Filter_TYPE_USER, - Term: &provider.ListStorageSpacesRequest_Filter_User{User: currentUser.GetId()}, - }, - { - Type: provider.ListStorageSpacesRequest_Filter_TYPE_SPACE_TYPE, - Term: &provider.ListStorageSpacesRequest_Filter_SpaceType{SpaceType: "+grant"}, - }, + filters = append(filters, []*provider.ListStorageSpacesRequest_Filter{ + { + Type: provider.ListStorageSpacesRequest_Filter_TYPE_USER, + Term: &provider.ListStorageSpacesRequest_Filter_User{User: currentUser.GetId()}, }, - }) + { + Type: provider.ListStorageSpacesRequest_Filter_TYPE_SPACE_TYPE, + Term: &provider.ListStorageSpacesRequest_Filter_SpaceType{SpaceType: "+grant"}, + }, + }...) + listSpacesRes, err := gatewayClient.ListStorageSpaces(ctx, &provider.ListStorageSpacesRequest{Filters: filters}) if err != nil { s.logger.Error().Err(err).Msg("failed to list the user's storage spaces") return nil, err @@ -229,7 +249,7 @@ func (s *Service) searchIndex(ctx context.Context, req *searchsvc.SearchRequest, rootName string permissions *provider.ResourcePermissions ) - mountpointPrefix := "" + mountpointPrefix := req.GetRef().GetPath() switch space.SpaceType { case "mountpoint": return nil, errSkipSpace // mountpoint spaces are only "links" to the shared spaces. we have to search the shared "grant" space instead diff --git a/services/search/pkg/search/service_test.go b/services/search/pkg/search/service_test.go index 50600ecff3..3c340003f7 100644 --- a/services/search/pkg/search/service_test.go +++ b/services/search/pkg/search/service_test.go @@ -391,3 +391,51 @@ var _ = Describe("Searchprovider", func() { }) }) }) + +var _ = DescribeTable("Parse Scope", + func(pattern, wantSearch, wantScope string) { + gotSearch, gotScope := search.ParseScope(pattern) + Expect(gotSearch).To(Equal(wantSearch)) + Expect(gotScope).To(Equal(wantScope)) + }, + Entry("When scope is at the end of the line", + `+Name:*file* +Tags:"foo" scope:/folder/subfolder`, + `+Name:*file* +Tags:"foo"`, + `/folder/subfolder`, + ), + Entry("When scope is at the end of the line 2", + `+Name:*file* +Tags:"foo" scope:/folder`, + `+Name:*file* +Tags:"foo"`, + `/folder`, + ), + Entry("When scope is at the end of the line 3", + `file scope:/folder/subfolder`, + `file`, + `/folder/subfolder`, + ), + Entry("When scope is at the end of the line with a space", + `+Name:*file* +Tags:"foo" scope: /folder/subfolder`, + `+Name:*file* +Tags:"foo"`, + `/folder/subfolder`, + ), + Entry("When scope is in the middle of the line", + `+Name:*file* scope:/folder/subfolder +Tags:"foo"`, + `+Name:*file* +Tags:"foo"`, + `/folder/subfolder`, + ), + Entry("When scope is at the end of the line", + `scope:/folder/subfolder +Name:*file*`, + `+Name:*file*`, + `/folder/subfolder`, + ), + Entry("When scope is at the begging of the line", + `scope:/folder/subfolder file`, + `file`, + `/folder/subfolder`, + ), + Entry("When no scope", + `+Name:*file* +Tags:"foo"`, + `+Name:*file* +Tags:"foo"`, + ``, + ), +)