feat(search): describe hits from shared spaces as remote items

This commit is contained in:
Dominik Schmidt committed 2026-09-03 10:30:38 +02:00
1 parent e38a735e20
commit 71ca404695
2 files changed
+102

No files matched your search

@@ -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
@@ -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)