Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
275 changes: 189 additions & 86 deletions protogen/gen/opencloud/messages/search/v0/search.pb.go

Large diffs are not rendered by default.

36 changes: 36 additions & 0 deletions protogen/gen/opencloud/messages/search/v0/search.pb.web.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions protogen/gen/opencloud/services/search/v0/search.swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,9 @@
"items": {
"type": "string"
}
},
"motionPhoto": {
"$ref": "#/definitions/v0MotionPhoto"
}
}
},
Expand Down Expand Up @@ -352,6 +355,23 @@
}
}
},
"v0MotionPhoto": {
"type": "object",
"properties": {
"version": {
"type": "integer",
"format": "int32"
},
"presentationTimestampUs": {
"type": "string",
"format": "int64"
},
"videoSize": {
"type": "string",
"format": "int64"
}
}
},
"v0Photo": {
"type": "object",
"properties": {
Expand Down
7 changes: 7 additions & 0 deletions protogen/proto/opencloud/messages/search/v0/search.proto
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ message Photo {
optional google.protobuf.Timestamp takenDateTime = 9;
}

message MotionPhoto {
optional int32 version = 1;
optional int64 presentationTimestampUs = 2;
optional int64 videoSize = 3;
}

message Entity {
Reference ref = 1;
ResourceID id = 2;
Expand All @@ -80,6 +86,7 @@ message Entity {
Image image = 18;
Photo photo = 19;
repeated string favorites = 20;
MotionPhoto motionPhoto = 21;
}

message Match {
Expand Down
1 change: 1 addition & 0 deletions services/graph/pkg/service/v0/driveitems.go
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,7 @@ func cs3ResourceToDriveItem(logger *log.Logger, publicBaseURL *url.URL, res *sto
driveItem.Image = metadataToFacet[libregraph.Image](metadata, "image")
driveItem.Location = metadataToFacet[libregraph.GeoCoordinates](metadata, "location")
driveItem.Photo = metadataToFacet[libregraph.Photo](metadata, "photo")
driveItem.LibreGraphMotionPhoto = metadataToFacet[libregraph.MotionPhoto](metadata, "motionPhoto")
}

return driveItem, nil
Expand Down
29 changes: 15 additions & 14 deletions services/search/pkg/bleve/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,20 +127,21 @@ func (b *Backend) Search(_ context.Context, sir *searchService.SearchIndexReques
ResourceId: resourceIDtoSearchID(rootID),
Path: getFieldValue[string](hit.Fields, "Path"),
},
Id: resourceIDtoSearchID(rID),
Name: getFieldValue[string](hit.Fields, "Name"),
ParentId: resourceIDtoSearchID(pID),
Size: uint64(getFieldValue[float64](hit.Fields, "Size")),
Type: uint64(getFieldValue[float64](hit.Fields, "Type")),
MimeType: getFieldValue[string](hit.Fields, "MimeType"),
Deleted: getFieldValue[bool](hit.Fields, "Deleted"),
Tags: getFieldSliceValue[string](hit.Fields, "Tags"),
Favorites: getFieldSliceValue[string](hit.Fields, "Favorites"),
Highlights: getFragmentValue(hit.Fragments, "Content", 0),
Audio: hitToFacet[searchMessage.Audio](hit.Fields, "audio"),
Image: hitToFacet[searchMessage.Image](hit.Fields, "image"),
Location: hitToFacet[searchMessage.GeoCoordinates](hit.Fields, "location"),
Photo: hitToFacet[searchMessage.Photo](hit.Fields, "photo"),
Id: resourceIDtoSearchID(rID),
Name: getFieldValue[string](hit.Fields, "Name"),
ParentId: resourceIDtoSearchID(pID),
Size: uint64(getFieldValue[float64](hit.Fields, "Size")),
Type: uint64(getFieldValue[float64](hit.Fields, "Type")),
MimeType: getFieldValue[string](hit.Fields, "MimeType"),
Deleted: getFieldValue[bool](hit.Fields, "Deleted"),
Tags: getFieldSliceValue[string](hit.Fields, "Tags"),
Favorites: getFieldSliceValue[string](hit.Fields, "Favorites"),
Highlights: getFragmentValue(hit.Fragments, "Content", 0),
Audio: hitToFacet[searchMessage.Audio](hit.Fields, "audio"),
Image: hitToFacet[searchMessage.Image](hit.Fields, "image"),
Location: hitToFacet[searchMessage.GeoCoordinates](hit.Fields, "location"),
Photo: hitToFacet[searchMessage.Photo](hit.Fields, "photo"),
MotionPhoto: hitToFacet[searchMessage.MotionPhoto](hit.Fields, "motionPhoto"),
},
}

Expand Down
25 changes: 13 additions & 12 deletions services/search/pkg/content/content.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,19 @@ func init() {
// Document wraps all resource meta fields,
// it is used as a content extraction result.
type Document struct {
Title string `json:"Title"`
Name string `json:"Name"`
Content string `json:"Content"`
Size uint64 `json:"Size"`
Mtime *time.Time `json:"Mtime"`
MimeType string `json:"MimeType"`
Tags []string `json:"Tags"`
Favorites []string `json:"Favorites"`
Audio *libregraph.Audio `json:"audio,omitempty"`
Image *libregraph.Image `json:"image,omitempty"`
Location *libregraph.GeoCoordinates `json:"location,omitempty"`
Photo *libregraph.Photo `json:"photo,omitempty"`
Title string `json:"Title"`
Name string `json:"Name"`
Content string `json:"Content"`
Size uint64 `json:"Size"`
Mtime *time.Time `json:"Mtime"`
MimeType string `json:"MimeType"`
Tags []string `json:"Tags"`
Favorites []string `json:"Favorites"`
Audio *libregraph.Audio `json:"audio,omitempty"`
Image *libregraph.Image `json:"image,omitempty"`
Location *libregraph.GeoCoordinates `json:"location,omitempty"`
Photo *libregraph.Photo `json:"photo,omitempty"`
MotionPhoto *libregraph.MotionPhoto `json:"motionPhoto,omitempty"`
}

func CleanString(content, langCode string) string {
Expand Down
80 changes: 67 additions & 13 deletions services/search/pkg/content/cs3.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,37 +33,45 @@ func newCS3Retriever(gatewaySelector pool.Selectable[gateway.GatewayAPIClient],
}
}

// Retrieve downloads the file from a cs3 service
// The caller MUST make sure to close the returned ReadCloser
func (s cs3) Retrieve(ctx context.Context, rID *provider.ResourceId) (io.ReadCloser, error) {
at, ok := contextGet(ctx, revactx.TokenHeader)
// initiateDownload resolves the download endpoint, transfer token and auth token
// for rID through the cs3 gateway.
func (s cs3) initiateDownload(ctx context.Context, rID *provider.ResourceId) (endpoint, transferToken, authToken string, err error) {
authToken, ok := contextGet(ctx, revactx.TokenHeader)
if !ok {
return nil, fmt.Errorf("context without %s", revactx.TokenHeader)
return "", "", "", fmt.Errorf("context without %s", revactx.TokenHeader)
}

gatewayClient, err := s.gatewaySelector.Next()
if err != nil {
s.logger.Error().Err(err).Msg("could not get reva gatewayClient")
return nil, err
return "", "", "", err
}

res, err := gatewayClient.InitiateFileDownload(ctx, &provider.InitiateFileDownloadRequest{Ref: &provider.Reference{ResourceId: rID, Path: "."}})
if err != nil {
return nil, err
return "", "", "", err
}
if res.Status.Code != rpc.Code_CODE_OK {
return nil, fmt.Errorf("could not load resoure: %s", res.Status.Message)
return "", "", "", fmt.Errorf("could not load resoure: %s", res.Status.Message)
}

var ep, tt string
for _, p := range res.Protocols {
if p.Protocol == "spaces" {
ep, tt = p.DownloadEndpoint, p.Token
break
return p.DownloadEndpoint, p.Token, authToken, nil
}
}
if (ep == "" || tt == "") && len(res.Protocols) > 0 {
ep, tt = res.Protocols[0].DownloadEndpoint, res.Protocols[0].Token
if len(res.Protocols) > 0 {
return res.Protocols[0].DownloadEndpoint, res.Protocols[0].Token, authToken, nil
}
return "", "", "", fmt.Errorf("no download protocol found")
}

// Retrieve downloads the file from a cs3 service
// The caller MUST make sure to close the returned ReadCloser
func (s cs3) Retrieve(ctx context.Context, rID *provider.ResourceId) (io.ReadCloser, error) {
ep, tt, at, err := s.initiateDownload(ctx, rID)
if err != nil {
return nil, err
}

req, err := http.NewRequest(http.MethodGet, ep, nil)
Expand All @@ -85,3 +93,49 @@ func (s cs3) Retrieve(ctx context.Context, rID *provider.ResourceId) (io.ReadClo

return cres.Body, nil
}

// RetrieveRange downloads length bytes starting at offset from a cs3 service.
// The caller MUST make sure to close the returned ReadCloser.
// It relies on HTTP range support of the download endpoint. If the endpoint
// ignores the Range header and returns the full file (200 instead of 206), the
// leading offset bytes are discarded so the returned reader is always positioned
// at offset.
func (s cs3) RetrieveRange(ctx context.Context, rID *provider.ResourceId, offset, length int64) (io.ReadCloser, error) {
ep, tt, at, err := s.initiateDownload(ctx, rID)
if err != nil {
return nil, err
}

req, err := http.NewRequest(http.MethodGet, ep, nil)
if err != nil {
return nil, err
}

req.Header.Set(revactx.TokenHeader, at)
req.Header.Set("X-Reva-Transfer", tt)
// A single range keeps the response a plain 206 with a Content-Range header,
// never a multipart/byteranges body.
req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", offset, offset+length-1))

cres, err := s.httpClient.Do(req)
if err != nil {
return nil, err
}

switch cres.StatusCode {
case http.StatusPartialContent:
// Range honored: the body already starts at offset.
return cres.Body, nil
case http.StatusOK:
// Range ignored: the body is the whole file. Skip to offset so the
// caller always reads from the requested position.
if _, err := io.CopyN(io.Discard, cres.Body, offset); err != nil {
_ = cres.Body.Close()
return nil, fmt.Errorf("could not skip to offset %d: %w", offset, err)
}
return cres.Body, nil
default:
_ = cres.Body.Close()
return nil, fmt.Errorf("could not download range. Request returned with statuscode %d ", cres.StatusCode)
}
}
23 changes: 8 additions & 15 deletions services/search/pkg/content/extractor.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package content

import (
"context"
"errors"
"fmt"

provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
Expand All @@ -13,19 +12,13 @@ type Extractor interface {
Extract(ctx context.Context, ri *provider.ResourceInfo) (Document, error)
}

func getFirstValue(m map[string][]string, key string) (string, error) {
if m == nil {
return "", errors.New("undefined map")
// getFirstValue returns the first metadata value present among keys, trying them
// in order. It errors when the map is nil or none of the keys holds a value.
func getFirstValue(m map[string][]string, keys ...string) (string, error) {
for _, key := range keys {
if v, ok := m[key]; ok && len(v) > 0 {
return v[0], nil
}
}

v, ok := m[key]
if !ok {
return "", fmt.Errorf("unknown key: %v", key)
}

if len(m) == 0 {
return "", fmt.Errorf("no values for: %v", key)
}

return v[0], nil
return "", fmt.Errorf("no value for keys: %v", keys)
}
Loading
Loading