Merge pull request #6713 from 2403905/OCIS-3705

Provide Search filter for locations
This commit is contained in:
Roman Perekhod
2023-07-12 21:28:51 +03:00
committed by GitHub
5 changed files with 246 additions and 34 deletions

View File

@@ -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:<uuid>
/Test"
https://github.com/owncloud/ocis/pull/6713
OCIS-3705

View File

@@ -162,8 +162,10 @@ func (b *Bleve) Search(_ context.Context, sir *searchService.SearchIndexRequest)
}
matches := make([]*searchMessage.Match, 0, len(res.Hits))
totalMatches := res.Total
for _, hit := range res.Hits {
if sir.Ref != nil && !strings.HasPrefix(getFieldValue[string](hit.Fields, "Path"), utils.MakeRelativePath(path.Join(sir.Ref.Path, "/"))) {
totalMatches--
continue
}
@@ -206,7 +208,7 @@ func (b *Bleve) Search(_ context.Context, sir *searchService.SearchIndexRequest)
return &searchService.SearchIndexResponse{
Matches: matches,
TotalMatches: int32(res.Total),
TotalMatches: int32(totalMatches),
}, nil
}

View File

@@ -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) (*provider.Reference, error) {
ref, err := storagespace.ParseReference(path)
if err != nil {
return nil, err
}
return &provider.Reference{
ResourceId: &provider.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, ""
}

View File

@@ -26,13 +26,18 @@ import (
"github.com/owncloud/ocis/v2/services/search/pkg/content"
"github.com/owncloud/ocis/v2/services/search/pkg/engine"
"golang.org/x/sync/errgroup"
"google.golang.org/protobuf/types/known/fieldmaskpb"
)
//go:generate mockery --name=Searcher
const (
_spaceStateTrashed = "trashed"
_slowQueryDuration = 500 * time.Millisecond
_spaceStateTrashed = "trashed"
_spaceTypeMountpoint = "mountpoint"
_spaceTypePersonal = "personal"
_spaceTypeProject = "project"
_spaceTypeGrant = "grant"
_slowQueryDuration = 500 * time.Millisecond
)
// Searcher is the interface to the SearchService
@@ -72,47 +77,87 @@ 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 == "" {
return nil, errtypes.BadRequest("empty query provided")
}
s.logger.Debug().Str("query", req.Query).Msg("performing a search")
gatewayClient, err := s.gatewaySelector.Next()
if err != nil {
return nil, err
}
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"},
// Extract scope from query if set
query, scope := ParseScope(req.Query)
if query == "" {
return nil, errtypes.BadRequest("empty query provided")
}
req.Query = query
if len(scope) > 0 {
// if req.Ref != nil {
// return nil, errtypes.BadRequest("cannot scope a search that is limited to a resource")
// }
scopeRef, err := extractScope(scope)
if err != nil {
return nil, err
}
// Stat the scope to get the resource id
statRes, err := gatewayClient.Stat(ctx, &provider.StatRequest{
Ref: scopeRef,
FieldMask: &fieldmaskpb.FieldMask{Paths: []string{"space"}},
})
if err != nil {
return nil, err
}
// GetPath the scope to get the full path in the space
gpRes, err := gatewayClient.GetPath(ctx, &provider.GetPathRequest{
ResourceId: statRes.GetInfo().GetId(),
})
if err != nil {
return nil, err
}
req.Ref = &searchmsg.Reference{
ResourceId: &searchmsg.ResourceID{
StorageId: statRes.GetInfo().GetSpace().GetRoot().GetStorageId(),
SpaceId: statRes.GetInfo().GetSpace().GetRoot().GetSpaceId(),
OpaqueId: statRes.GetInfo().GetSpace().GetRoot().GetOpaqueId(),
},
Path: gpRes.Path,
}
}
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"},
},
}
// Get the spaces to search
spaces := []*provider.StorageSpace{}
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
}
spaces := []*provider.StorageSpace{}
for _, space := range listSpacesRes.StorageSpaces {
if utils.ReadPlainFromOpaque(space.Opaque, "trashed") == _spaceStateTrashed {
// Do not consider disabled spaces
continue
}
if space.SpaceType != "mountpoint" && req.Ref != nil && (req.Ref.GetResourceId().GetSpaceId() != space.Root.GetSpaceId()) {
// Do not search (non-mountpoint) spaces that do not match the given scope (if a scope is set)
// We still need the mountpoint in order to map the result paths to the according share
continue
}
spaces = append(spaces, space)
}
mountpointMap := map[string]string{}
for _, space := range spaces {
if space.SpaceType != "mountpoint" {
if space.SpaceType != _spaceTypeMountpoint {
continue
}
opaqueMap := sdk.DecodeOpaqueMap(space.Opaque)
@@ -213,8 +258,7 @@ func (s *Service) Search(ctx context.Context, req *searchsvc.SearchRequest) (*se
func (s *Service) searchIndex(ctx context.Context, req *searchsvc.SearchRequest, space *provider.StorageSpace, mountpointID string) (*searchsvc.SearchIndexResponse, error) {
if req.Ref != nil &&
(req.Ref.ResourceId.StorageId != space.Root.StorageId ||
req.Ref.ResourceId.SpaceId != space.Root.SpaceId ||
req.Ref.ResourceId.OpaqueId != space.Root.OpaqueId) {
req.Ref.ResourceId.SpaceId != space.Root.SpaceId) {
return nil, errSkipSpace
}
@@ -230,10 +274,11 @@ func (s *Service) searchIndex(ctx context.Context, req *searchsvc.SearchRequest,
permissions *provider.ResourcePermissions
)
mountpointPrefix := ""
searchPathPrefix := req.Ref.GetPath()
switch space.SpaceType {
case "mountpoint":
case _spaceTypeMountpoint:
return nil, errSkipSpace // mountpoint spaces are only "links" to the shared spaces. we have to search the shared "grant" space instead
case "grant":
case _spaceTypeGrant:
// In case of grant spaces we search the root of the outer space and translate the paths to the according mountpoint
searchRootID.OpaqueId = space.Root.SpaceId
if mountpointID == "" {
@@ -272,6 +317,9 @@ func (s *Service) searchIndex(ctx context.Context, req *searchsvc.SearchRequest,
return nil, errSkipSpace
}
mountpointPrefix = utils.MakeRelativePath(gpRes.Path)
if searchPathPrefix == "" {
searchPathPrefix = mountpointPrefix
}
sid, spid, oid, err := storagespace.SplitID(mountpointID)
if err != nil {
s.logger.Error().Err(err).Str("space", space.Id.OpaqueId).Str("mountpointId", mountpointID).Msg("invalid mountpoint space id")
@@ -285,7 +333,7 @@ func (s *Service) searchIndex(ctx context.Context, req *searchsvc.SearchRequest,
rootName = space.GetRootInfo().GetPath()
permissions = space.GetRootInfo().GetPermissionSet()
s.logger.Debug().Interface("grantSpace", space).Interface("mountpointRootId", mountpointRootID).Msg("searching a grant")
case "personal", "project":
case _spaceTypePersonal, _spaceTypeProject:
permissions = space.GetRootInfo().GetPermissionSet()
}
@@ -293,7 +341,7 @@ func (s *Service) searchIndex(ctx context.Context, req *searchsvc.SearchRequest,
Query: req.Query,
Ref: &searchmsg.Reference{
ResourceId: searchRootID,
Path: mountpointPrefix,
Path: searchPathPrefix,
},
PageSize: req.PageSize,
}

View File

@@ -53,9 +53,10 @@ var _ = Describe("Searchprovider", func() {
},
},
},
Id: &sprovider.StorageSpaceId{OpaqueId: "storageid$personalspace!personalspace"},
Root: &sprovider.ResourceId{StorageId: "storageid", SpaceId: "personalspace", OpaqueId: "personalspace"},
Name: "personalspace",
Id: &sprovider.StorageSpaceId{OpaqueId: "storageid$personalspace!personalspace"},
Root: &sprovider.ResourceId{StorageId: "storageid", SpaceId: "personalspace", OpaqueId: "personalspace"},
Name: "personalspace",
SpaceType: "personal",
}
ri = &sprovider.ResourceInfo{
@@ -94,10 +95,6 @@ var _ = Describe("Searchprovider", func() {
Status: status.NewOK(ctx),
Token: "authtoken",
}, nil)
gatewayClient.On("Stat", mock.Anything, mock.Anything).Return(&sprovider.StatResponse{
Status: status.NewOK(context.Background()),
Info: ri,
}, nil)
gatewayClient.On("GetPath", mock.Anything, mock.MatchedBy(func(req *sprovider.GetPathRequest) bool {
return req.ResourceId.OpaqueId == ri.Id.OpaqueId
})).Return(&sprovider.GetPathResponse{
@@ -123,7 +120,10 @@ var _ = Describe("Searchprovider", func() {
extractor.On("Extract", mock.Anything, mock.Anything, mock.Anything).Return(content.Document{}, nil)
indexClient.On("Upsert", mock.Anything, mock.Anything).Return(nil)
indexClient.On("Search", mock.Anything, mock.Anything).Return(&searchsvc.SearchIndexResponse{}, nil)
gatewayClient.On("Stat", mock.Anything, mock.Anything).Return(&sprovider.StatResponse{
Status: status.NewOK(context.Background()),
Info: ri,
}, nil)
err := s.IndexSpace(&sprovider.StorageSpaceId{OpaqueId: "storageid$spaceid!spaceid"}, user.Id)
Expect(err).ShouldNot(HaveOccurred())
})
@@ -167,6 +167,10 @@ var _ = Describe("Searchprovider", func() {
},
},
}, nil)
gatewayClient.On("Stat", mock.Anything, mock.Anything).Return(&sprovider.StatResponse{
Status: status.NewOK(context.Background()),
Info: ri,
}, nil)
})
It("does not mess with field-based searches", func() {
@@ -195,6 +199,75 @@ var _ = Describe("Searchprovider", func() {
})
})
Context("with a personal space with a filter", func() {
BeforeEach(func() {
gatewayClient.On("ListStorageSpaces", mock.Anything, mock.Anything).Return(&sprovider.ListStorageSpacesResponse{
Status: status.NewOK(ctx),
StorageSpaces: []*sprovider.StorageSpace{personalSpace},
}, nil)
gatewayClient.On("Stat", mock.Anything, mock.Anything).Return(&sprovider.StatResponse{
Status: status.NewOK(context.Background()),
Info: &sprovider.ResourceInfo{
Space: &sprovider.StorageSpace{Root: &sprovider.ResourceId{
StorageId: "storageid",
SpaceId: "personalspace",
OpaqueId: "personalspace",
}},
},
}, nil)
gatewayClient.On("GetPath", mock.Anything, mock.Anything).Return(&sprovider.GetPathResponse{
Status: status.NewOK(ctx),
Path: "/path",
}, nil)
indexClient.On("Search", mock.Anything, mock.Anything).Return(&searchsvc.SearchIndexResponse{
TotalMatches: 1,
Matches: []*searchmsg.Match{
{
Score: 1,
Entity: &searchmsg.Entity{
Ref: &searchmsg.Reference{
ResourceId: &searchmsg.ResourceID{
StorageId: personalSpace.Root.StorageId,
SpaceId: personalSpace.Root.SpaceId,
OpaqueId: personalSpace.Root.OpaqueId,
},
Path: "./path/to/Foo.pdf",
},
Id: &searchmsg.ResourceID{
StorageId: personalSpace.Root.StorageId,
OpaqueId: "foo-id",
},
Name: "Foo.pdf",
},
},
},
}, nil)
})
It("searches the personal user space", func() {
res, err := s.Search(ctx, &searchsvc.SearchRequest{
Query: "foo scope:storageid$personalspace!personalspace/path",
Ref: &searchmsg.Reference{
ResourceId: &searchmsg.ResourceID{
StorageId: "storageid",
SpaceId: "personalspace",
OpaqueId: "personalspace",
},
},
})
Expect(err).ToNot(HaveOccurred())
Expect(res).ToNot(BeNil())
Expect(res.TotalMatches).To(Equal(int32(1)))
Expect(len(res.Matches)).To(Equal(1))
match := res.Matches[0]
Expect(match.Entity.Id.OpaqueId).To(Equal("foo-id"))
Expect(match.Entity.Name).To(Equal("Foo.pdf"))
Expect(match.Entity.Ref.ResourceId.OpaqueId).To(Equal(personalSpace.Root.OpaqueId))
Expect(match.Entity.Ref.Path).To(Equal("./path/to/Foo.pdf"))
})
})
Context("with received shares", func() {
var (
grantSpace *sprovider.StorageSpace
@@ -223,6 +296,10 @@ var _ = Describe("Searchprovider", func() {
},
},
}
gatewayClient.On("Stat", mock.Anything, mock.Anything).Return(&sprovider.StatResponse{
Status: status.NewOK(context.Background()),
Info: ri,
}, nil)
gatewayClient.On("GetPath", mock.Anything, mock.Anything).Return(&sprovider.GetPathResponse{
Status: status.NewOK(ctx),
Path: "/grant/path",
@@ -391,3 +468,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:&quot;foo&quot; scope:<uuid>/folder/subfolder`,
`+Name:*file* +Tags:&quot;foo&quot;`,
`<uuid>/folder/subfolder`,
),
Entry("When scope is at the end of the line 2",
`+Name:*file* +Tags:&quot;foo&quot; scope:<uuid>/folder`,
`+Name:*file* +Tags:&quot;foo&quot;`,
`<uuid>/folder`,
),
Entry("When scope is at the end of the line 3",
`file scope:<uuid>/folder/subfolder`,
`file`,
`<uuid>/folder/subfolder`,
),
Entry("When scope is at the end of the line with a space",
`+Name:*file* +Tags:&quot;foo&quot; scope: <uuid>/folder/subfolder`,
`+Name:*file* +Tags:&quot;foo&quot;`,
`<uuid>/folder/subfolder`,
),
Entry("When scope is in the middle of the line",
`+Name:*file* scope:<uuid>/folder/subfolder +Tags:&quot;foo&quot;`,
`+Name:*file* +Tags:&quot;foo&quot;`,
`<uuid>/folder/subfolder`,
),
Entry("When scope is at the end of the line",
`scope:<uuid>/folder/subfolder +Name:*file*`,
`+Name:*file*`,
`<uuid>/folder/subfolder`,
),
Entry("When scope is at the begging of the line",
`scope:<uuid>/folder/subfolder file`,
`file`,
`<uuid>/folder/subfolder`,
),
Entry("When no scope",
`+Name:*file* +Tags:&quot;foo&quot;`,
`+Name:*file* +Tags:&quot;foo&quot;`,
``,
),
)