mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-01-27 23:47:33 -05:00
Merge pull request #6713 from 2403905/OCIS-3705
Provide Search filter for locations
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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, ""
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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:"foo" scope:<uuid>/folder/subfolder`,
|
||||
`+Name:*file* +Tags:"foo"`,
|
||||
`<uuid>/folder/subfolder`,
|
||||
),
|
||||
Entry("When scope is at the end of the line 2",
|
||||
`+Name:*file* +Tags:"foo" scope:<uuid>/folder`,
|
||||
`+Name:*file* +Tags:"foo"`,
|
||||
`<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:"foo" scope: <uuid>/folder/subfolder`,
|
||||
`+Name:*file* +Tags:"foo"`,
|
||||
`<uuid>/folder/subfolder`,
|
||||
),
|
||||
Entry("When scope is in the middle of the line",
|
||||
`+Name:*file* scope:<uuid>/folder/subfolder +Tags:"foo"`,
|
||||
`+Name:*file* +Tags:"foo"`,
|
||||
`<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:"foo"`,
|
||||
`+Name:*file* +Tags:"foo"`,
|
||||
``,
|
||||
),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user