Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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
10 changes: 10 additions & 0 deletions pkg/conversions/strings.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package conversions

import (
"strconv"
"strings"
)

Expand All @@ -14,3 +15,12 @@ func StringToSliceString(src string, sep string) []string {

return parts
}

// StringToInt32 parses s as a base-10 int32, returning fallback on any error.
func StringToInt32(s string, fallback int32) int32 {
v, err := strconv.ParseInt(s, 10, 32)
if err != nil {
return fallback
}
return int32(v)
}
567 changes: 328 additions & 239 deletions protogen/gen/opencloud/messages/search/v0/search.pb.go

Large diffs are not rendered by default.

17 changes: 17 additions & 0 deletions protogen/gen/opencloud/services/search/v0/search.swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,9 @@
},
"livePhoto": {
"$ref": "#/definitions/v0LivePhoto"
},
"preview": {
"$ref": "#/definitions/v0Preview"
}
}
},
Expand Down Expand Up @@ -478,6 +481,20 @@
}
}
},
"v0Preview": {
"type": "object",
"properties": {
"width": {
"type": "integer",
"format": "int32"
},
"height": {
"type": "integer",
"format": "int32"
}
},
"description": "Preview carries the dimensions of an embedded preview (e.g. audio cover art).\nIts presence signals that a preview is available for the resource."
},
"v0Reference": {
"type": "object",
"properties": {
Expand Down
8 changes: 8 additions & 0 deletions protogen/proto/opencloud/messages/search/v0/search.proto
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@ message Image {
optional int32 height = 2;
}

// Preview carries the dimensions of an embedded preview (e.g. audio cover art).
// Its presence signals that a preview is available for the resource.
message Preview {
optional int32 width = 1;
optional int32 height = 2;
}

message GeoCoordinates {
optional double altitude = 1;
optional double latitude = 2;
Expand Down Expand Up @@ -110,6 +117,7 @@ message Entity {
MotionPhoto motionPhoto = 21;
Video video = 22;
LivePhoto livePhoto = 23;
Preview preview = 24;
}

message Match {
Expand Down
32 changes: 28 additions & 4 deletions services/graph/pkg/service/v0/thumbnails.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
libregraph "github.com/opencloud-eu/libre-graph-api-go"
"github.com/opencloud-eu/reva/v2/pkg/storagespace"

"github.com/opencloud-eu/opencloud/pkg/conversions"
"github.com/opencloud-eu/opencloud/services/thumbnails/pkg/thumbnail"
)

Expand All @@ -34,24 +35,47 @@ func setDriveItemThumbnails(item *libregraph.DriveItem, res *provider.ResourceIn
}
}

// previewThumbnailSet returns nil when the thumbnailer cannot render the resource.
// previewThumbnailSet returns nil when no preview can be produced for the resource.
func previewThumbnailSet(res *provider.ResourceInfo, baseURL string) *libregraph.ThumbnailSet {
if !thumbnail.IsMimeTypeSupported(res.GetMimeType()) {
if !thumbnail.HasPreview(res) {
return nil
}
return thumbnailSetFor(baseURL, storagespace.FormatResourceID(res.GetId()))

itemID := storagespace.FormatResourceID(res.GetId())
set := thumbnailSetFor(baseURL, itemID)
// only the source carries exact dimensions, the boxes above are requests
if w, h := previewSourceDimensions(res); w > 0 && h > 0 {
url := fmt.Sprintf("%s&x=%d&y=%d", previewBaseURL(baseURL, itemID), w, h)
set.Source = &libregraph.Thumbnail{Url: &url, Width: &w, Height: &h}
}
return set
}

// previewSourceDimensions: audio cover from oc.preview, images from the image facet.
func previewSourceDimensions(res *provider.ResourceInfo) (int32, int32) {
if w, h := thumbnail.PreviewDimensions(res); w > 0 && h > 0 {
return w, h
}
meta := res.GetArbitraryMetadata().GetMetadata()
w := conversions.StringToInt32(meta["libre.graph.image.width"], 0)
h := conversions.StringToInt32(meta["libre.graph.image.height"], 0)
return w, h
}

// thumbnailSetFor builds the urls of the WebDAV preview endpoint.
func thumbnailSetFor(baseURL, itemID string) *libregraph.ThumbnailSet {
base := fmt.Sprintf("%s/dav/spaces/%s?scalingup=0&preview=1&processor=thumbnail", baseURL, itemID)
base := previewBaseURL(baseURL, itemID)
return &libregraph.ThumbnailSet{
Small: previewThumbnail(base, thumbnailBoxSmall),
Medium: previewThumbnail(base, thumbnailBoxMedium),
Large: previewThumbnail(base, thumbnailBoxLarge),
}
}

func previewBaseURL(baseURL, itemID string) string {
return fmt.Sprintf("%s/dav/spaces/%s?scalingup=0&preview=1&processor=thumbnail", baseURL, itemID)
}

func previewThumbnail(base string, box int32) *libregraph.Thumbnail {
url := fmt.Sprintf("%s&x=%d&y=%d", base, box, box)
return &libregraph.Thumbnail{Url: &url}
Expand Down
22 changes: 12 additions & 10 deletions services/search/pkg/bleve/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,16 +125,18 @@ 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),
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),
Preview: hitToFacet[searchMessage.Preview](hit.Fields, "preview"),

Audio: hitToFacet[searchMessage.Audio](hit.Fields, "audio"),
Image: hitToFacet[searchMessage.Image](hit.Fields, "image"),
Location: hitToFacet[searchMessage.GeoCoordinates](hit.Fields, "location"),
Expand Down
32 changes: 32 additions & 0 deletions services/search/pkg/bleve/testdata/mapping.golden.json
Original file line number Diff line number Diff line change
Expand Up @@ -1053,6 +1053,38 @@
}
}
},
"preview": {
"enabled": true,
"dynamic": true,
"properties": {
"height": {
"enabled": true,
"dynamic": true,
"fields": [
{
"type": "number",
"store": true,
"index": true,
"include_in_all": true,
"docvalues": true
}
]
},
"width": {
"enabled": true,
"dynamic": true,
"fields": [
{
"type": "number",
"store": true,
"index": true,
"include_in_all": true,
"docvalues": true
}
]
}
}
},
"video": {
"enabled": true,
"dynamic": true,
Expand Down
33 changes: 25 additions & 8 deletions services/search/pkg/content/content.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,16 @@ 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,omitempty"`
MimeType string `json:"MimeType"`
Tags []string `json:"Tags"`
Favorites []string `json:"Favorites"`
Title string `json:"Title"`
Name string `json:"Name"`
Content string `json:"Content"`
Size uint64 `json:"Size"`
Mtime *time.Time `json:"Mtime,omitempty"`
MimeType string `json:"MimeType"`
Tags []string `json:"Tags"`
Favorites []string `json:"Favorites"`
Preview *Preview `json:"preview,omitempty"`

Audio *libregraph.Audio `json:"audio,omitempty"`
Image *libregraph.Image `json:"image,omitempty"`
Location *libregraph.GeoCoordinates `json:"location,omitempty"`
Expand All @@ -32,6 +34,21 @@ type Document struct {
LivePhoto *libregraph.LivePhoto `json:"livePhoto,omitempty"`
}

// Preview holds the dimensions of an embedded preview (e.g. audio cover art).
// Internal signal, not a Graph facet; its presence marks that a preview exists.
type Preview struct {
Width int32 `json:"width"`
Height int32 `json:"height"`
}

// ToMap flows Preview through the shared facet flattening, under oc.preview.
func (p Preview) ToMap() (map[string]interface{}, error) {
return map[string]interface{}{
"width": p.Width,
"height": p.Height,
}, nil
}

func CleanString(content, langCode string) string {
return strings.TrimSpace(stopwords.CleanString(content, langCode, true))
}
47 changes: 18 additions & 29 deletions services/search/pkg/content/tika.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,11 @@ func (t Tika) Extract(ctx context.Context, ri *provider.ResourceInfo) (Document,
if err != nil {
return doc, err
}
if len(metas) == 0 {
return doc, nil
}

// Title and content aggregate across the container and embedded resources.
for _, meta := range metas {
title, err := getFirstValue(meta, "dc:title")
if err != nil {
Expand All @@ -98,42 +102,27 @@ func (t Tika) Extract(ctx context.Context, ri *provider.ResourceInfo) (Document,
} else if content, err := getFirstValue(meta, "X-TIKA:content"); err == nil {
doc.Content = strings.TrimSpace(fmt.Sprintf("%s %s", doc.Content, content))
}

// keep facets from earlier entries, an embedded resource's meta
// (e.g. cover art) must not reset them
if v := t.getLocation(meta); v != nil {
doc.Location = v
}
if v := t.getImage(meta); v != nil {
doc.Image = v
}
if v := t.getPhoto(meta); v != nil {
doc.Photo = v
}
if v := t.getAudio(meta); v != nil {
doc.Audio = v
}
if v := t.getLivePhoto(meta); v != nil {
doc.LivePhoto = v
}
}

if len(metas) > 0 {
// the video facet says the file is a video, so it comes from the file
// itself: the clip tika extracts from a motion photo must not make its
// image look like one
doc.Video = t.getVideo(metas[0])
}

// a motion photo is the xmp on the file itself plus the video tika extracted
// from it. The xmp alone proves nothing: a share can keep it and strip the
// appended video.
if len(metas) > 0 {
if i := slices.IndexFunc(metas[1:], isVideo); i >= 0 {
doc.MotionPhoto = t.getMotionPhoto(metas[0], metas[i+1])
}
if i := slices.IndexFunc(metas[1:], isVideo); i >= 0 {
doc.MotionPhoto = t.getMotionPhoto(metas[0], metas[i+1])
}

doc.Preview = getPreview(ri.GetMimeType(), metas)

// Facets come from the container (first entry) only; embedded resources like
// audio cover art must not leak in (the cover becomes the preview instead).
container := metas[0]
doc.Location = t.getLocation(container)
doc.Image = t.getImage(container)
doc.Photo = t.getPhoto(container)
doc.Audio = t.getAudio(container)
doc.Video = t.getVideo(container)
doc.LivePhoto = t.getLivePhoto(container)

if langCode := t.detectLanguage(ctx, doc.Content); langCode != "" && t.CleanStopWords {
doc.Content = CleanString(doc.Content, langCode)
}
Expand Down
47 changes: 47 additions & 0 deletions services/search/pkg/content/tika_preview.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package content

import (
"strconv"
"strings"

"github.com/opencloud-eu/opencloud/services/thumbnails/pkg/thumbnail"
)

// frontCoverDescription is the picture type Tika reports (as dc:description) for
// the front cover, matching the thumbnailer's cover selection.
const frontCoverDescription = "Cover (front)"

// getPreview returns the dimensions of an audio file's embedded cover art from
// Tika's recursive metadata, preferring the front cover (dc:description) and
// falling back to the first image, matching the thumbnailer's selection. It only
// runs for EmbeddedPreviewMimeTypes.
func getPreview(mimeType string, metas []map[string][]string) *Preview {
if _, ok := thumbnail.EmbeddedPreviewMimeTypes[mimeType]; !ok {
return nil
}
var first *Preview
for _, meta := range metas {
ct, err := getFirstValue(meta, "Content-Type")
if err != nil || !strings.HasPrefix(ct, "image/") {
continue
}
w, wErr := getFirstValue(meta, "tiff:ImageWidth")
h, hErr := getFirstValue(meta, "tiff:ImageLength")
if wErr != nil || hErr != nil {
continue
}
width, wErr := strconv.ParseInt(w, 10, 32)
height, hErr := strconv.ParseInt(h, 10, 32)
if wErr != nil || hErr != nil || width <= 0 || height <= 0 {
continue
}
preview := &Preview{Width: int32(width), Height: int32(height)}
if desc, _ := getFirstValue(meta, "dc:description"); desc == frontCoverDescription {
return preview
}
if first == nil {
first = preview
}
}
return first
}
Loading