diff --git a/services/graph/pkg/service/v0/searchquery.go b/services/graph/pkg/service/v0/searchquery.go index 8f244ce3d3..eb80ef9d52 100644 --- a/services/graph/pkg/service/v0/searchquery.go +++ b/services/graph/pkg/service/v0/searchquery.go @@ -365,6 +365,7 @@ func searchEntityToDriveItem(e *searchmsg.Entity) *libregraph.DriveItem { } di.ParentReference = ref } + di.RemoteItem = searchEntityToRemoteItem(e) di.Audio = searchAudioToLibregraph(e.GetAudio()) di.Image = searchImageToLibregraph(e.GetImage()) di.Photo = searchPhotoToLibregraph(e.GetPhoto()) @@ -372,6 +373,30 @@ func searchEntityToDriveItem(e *searchmsg.Entity) *libregraph.DriveItem { return di } +// searchEntityToRemoteItem describes a hit that lives in a space shared with the +// caller: the item id in the owner's drive and the mountpoint it is reached +// through. Absent for hits from the caller's own spaces. +func searchEntityToRemoteItem(e *searchmsg.Entity) *libregraph.RemoteItem { + id := e.GetRemoteItemId() + if id == nil { + return nil + } + + item := libregraph.NewRemoteItem() + item.SetId(storagespace.FormatResourceID(&storageprovider.ResourceId{ + StorageId: id.GetStorageId(), + SpaceId: id.GetSpaceId(), + OpaqueId: id.GetOpaqueId(), + })) + + if root := e.GetShareRootName(); root != "" { + item.SetPath(root) + item.SetName(path.Base(root)) + } + + return item +} + func searchAudioToLibregraph(a *searchmsg.Audio) *libregraph.Audio { if a == nil { return nil diff --git a/services/graph/pkg/service/v0/searchquery_test.go b/services/graph/pkg/service/v0/searchquery_test.go index f16ce2e998..2817f7188d 100644 --- a/services/graph/pkg/service/v0/searchquery_test.go +++ b/services/graph/pkg/service/v0/searchquery_test.go @@ -14,6 +14,7 @@ import ( "go-micro.dev/v4/client" "github.com/opencloud-eu/opencloud/pkg/log" + searchmsg "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/messages/search/v0" searchsvc "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/services/search/v0" ) @@ -47,6 +48,35 @@ func postSearchQuery(g Graph, body string) *httptest.ResponseRecorder { func int32Ptr(v int32) *int32 { return &v } +// searchHitRemoteItem decodes the remoteItem of the first hit, nil when absent. +func searchHitRemoteItem(rr *httptest.ResponseRecorder) *struct { + Id *string `json:"id"` + Name *string `json:"name"` + Path *string `json:"path"` +} { + var decoded struct { + Value []struct { + HitsContainers []struct { + Hits []struct { + Resource struct { + RemoteItem *struct { + Id *string `json:"id"` + Name *string `json:"name"` + Path *string `json:"path"` + } `json:"remoteItem"` + } `json:"resource"` + } `json:"hits"` + } `json:"hitsContainers"` + } `json:"value"` + } + Expect(json.Unmarshal(rr.Body.Bytes(), &decoded)).To(Succeed()) + Expect(decoded.Value).To(HaveLen(1)) + Expect(decoded.Value[0].HitsContainers).To(HaveLen(1)) + Expect(decoded.Value[0].HitsContainers[0].Hits).To(HaveLen(1)) + + return decoded.Value[0].HitsContainers[0].Hits[0].Resource.RemoteItem +} + var _ = ginkgo.Describe("SearchQuery", func() { ginkgo.It("forwards aggregations to the search service and groups results by request", func() { var captured *searchsvc.SearchRequest @@ -107,6 +137,53 @@ var _ = ginkgo.Describe("SearchQuery", func() { Expect(aggs[0].Buckets).To(HaveLen(2)) }) + ginkgo.It("describes a hit from a shared space as a remote item", func() { + g := graphWithSearch(stubSearchService{ + search: func(_ *searchsvc.SearchRequest) (*searchsvc.SearchResponse, error) { + return &searchsvc.SearchResponse{ + TotalMatches: 1, + Matches: []*searchmsg.Match{{ + Entity: &searchmsg.Entity{ + Id: &searchmsg.ResourceID{StorageId: "1", SpaceId: "2", OpaqueId: "3"}, + Name: "contract.pdf", + ShareRootName: "/Project X", + RemoteItemId: &searchmsg.ResourceID{StorageId: "4", SpaceId: "5", OpaqueId: "6"}, + }, + }}, + }, nil + }, + }) + + rr := postSearchQuery(g, `{"requests": [{"entityTypes": ["driveItem"], "query": {"queryString": "contract"}}]}`) + Expect(rr.Code).To(Equal(http.StatusOK)) + + remote := searchHitRemoteItem(rr) + Expect(remote).ToNot(BeNil()) + Expect(remote.Id).To(HaveValue(Equal("4$5!6"))) + Expect(remote.Path).To(HaveValue(Equal("/Project X"))) + Expect(remote.Name).To(HaveValue(Equal("Project X")), "the mountpoint name the caller sees") + }) + + ginkgo.It("leaves the remote item out for hits from the caller's own spaces", func() { + g := graphWithSearch(stubSearchService{ + search: func(_ *searchsvc.SearchRequest) (*searchsvc.SearchResponse, error) { + return &searchsvc.SearchResponse{ + TotalMatches: 1, + Matches: []*searchmsg.Match{{ + Entity: &searchmsg.Entity{ + Id: &searchmsg.ResourceID{StorageId: "1", SpaceId: "2", OpaqueId: "3"}, + Name: "notes.txt", + }, + }}, + }, nil + }, + }) + + rr := postSearchQuery(g, `{"requests": [{"entityTypes": ["driveItem"], "query": {"queryString": "notes"}}]}`) + Expect(rr.Code).To(Equal(http.StatusOK)) + Expect(searchHitRemoteItem(rr)).To(BeNil()) + }) + ginkgo.DescribeTable("clampPagination keeps from/size within valid bounds", func(from, size *int32, wantFrom, wantSize int32) { gotFrom, gotSize := clampPagination(from, size)