diff --git a/go.mod b/go.mod
index 271b78b785..271c64bbf2 100644
--- a/go.mod
+++ b/go.mod
@@ -43,6 +43,7 @@ require (
github.com/gookit/config/v2 v2.2.9
github.com/gorilla/mux v1.8.1
github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0
+ github.com/iancoleman/strcase v0.3.0
github.com/invopop/validation v0.8.0
github.com/jellydator/ttlcache/v2 v2.11.1
github.com/jellydator/ttlcache/v3 v3.4.1
@@ -245,7 +246,6 @@ require (
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/hashicorp/yamux v0.1.2 // indirect
github.com/huandu/xstrings v1.5.0 // indirect
- github.com/iancoleman/strcase v0.3.0 // indirect
github.com/imdario/mergo v0.3.16 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
diff --git a/services/webdav/pkg/service/v0/search.go b/services/webdav/pkg/service/v0/search.go
index e2f6a3f4a4..c4b062b081 100644
--- a/services/webdav/pkg/service/v0/search.go
+++ b/services/webdav/pkg/service/v0/search.go
@@ -12,11 +12,16 @@ import (
"slices"
"strconv"
"strings"
+ "time"
userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
+ "github.com/iancoleman/strcase"
merrors "go-micro.dev/v4/errors"
"go-micro.dev/v4/metadata"
+ "google.golang.org/protobuf/proto"
+ "google.golang.org/protobuf/reflect/protoreflect"
+ "google.golang.org/protobuf/types/known/timestamppb"
revactx "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
@@ -262,6 +267,14 @@ func matchToPropResponse(ctx context.Context, davPrefix, publicURL string, match
propstatOK.Prop = append(propstatOK.Prop, prop.Escaped("oc:favorite", "1"))
}
+ propstatOK.Prop = appendFacet(propstatOK.Prop, "audio", match.Entity.Audio)
+ propstatOK.Prop = appendFacet(propstatOK.Prop, "location", match.Entity.Location)
+ propstatOK.Prop = appendFacet(propstatOK.Prop, "image", match.Entity.Image)
+ propstatOK.Prop = appendFacet(propstatOK.Prop, "photo", match.Entity.Photo)
+ propstatOK.Prop = appendFacet(propstatOK.Prop, "video", match.Entity.Video)
+ propstatOK.Prop = appendFacet(propstatOK.Prop, "motion-photo", match.Entity.MotionPhoto)
+ propstatOK.Prop = appendFacet(propstatOK.Prop, "live-photo", match.Entity.LivePhoto)
+
if len(propstatOK.Prop) > 0 {
response.Propstat = append(response.Propstat, propstatOK)
}
@@ -269,6 +282,45 @@ func matchToPropResponse(ctx context.Context, davPrefix, publicURL string, match
return &response, nil
}
+// appendFacet renders a facet in the xml shape PROPFIND uses for it.
+func appendFacet(props []prop.PropertyXML, name string, facet proto.Message) []prop.PropertyXML {
+ m := facet.ProtoReflect()
+ if !m.IsValid() {
+ return props
+ }
+
+ content := strings.Builder{}
+ fields := m.Descriptor().Fields()
+ for i := 0; i < fields.Len(); i++ {
+ fd := fields.Get(i)
+ if !m.Has(fd) {
+ continue
+ }
+ key := "oc:" + strcase.ToKebab(string(fd.Name()))
+ content.WriteString("<" + key + ">")
+ content.WriteString(prop.Escape(facetValue(fd, m.Get(fd))))
+ content.WriteString("" + key + ">")
+ }
+ if content.Len() == 0 {
+ return props
+ }
+ return append(props, prop.Raw("oc:"+name, content.String()))
+}
+
+func facetValue(fd protoreflect.FieldDescriptor, v protoreflect.Value) string {
+ switch fd.Kind() {
+ case protoreflect.FloatKind:
+ return strconv.FormatFloat(v.Float(), 'f', -1, 32)
+ case protoreflect.DoubleKind:
+ return strconv.FormatFloat(v.Float(), 'f', -1, 64)
+ case protoreflect.MessageKind:
+ if ts, ok := v.Message().Interface().(*timestamppb.Timestamp); ok {
+ return ts.AsTime().Format(time.RFC3339)
+ }
+ }
+ return v.String()
+}
+
func hasPreview(md *provider.ResourceInfo, appendToOK func(p ...prop.PropertyXML)) {
_, match := thumbnail.SupportedMimeTypes[md.MimeType]
if match {
diff --git a/services/webdav/pkg/service/v0/search_test.go b/services/webdav/pkg/service/v0/search_test.go
index 5bfa2782d8..facfeeffb3 100644
--- a/services/webdav/pkg/service/v0/search_test.go
+++ b/services/webdav/pkg/service/v0/search_test.go
@@ -1,10 +1,17 @@
package svc
import (
+ "context"
"testing"
+ "time"
+ userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
+ "google.golang.org/protobuf/proto"
+ "google.golang.org/protobuf/types/known/timestamppb"
+
+ searchmsg "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/messages/search/v0"
)
func TestSearch(t *testing.T) {
@@ -33,3 +40,21 @@ var _ = Describe("SpacesSearchRegex", func() {
Entry("unrelated path", "/dav/files/123", "", false),
)
})
+
+var _ = Describe("multistatusResponse", func() {
+ It("carries the facets of a hit in the PROPFIND shape", func() {
+ id := &searchmsg.ResourceID{StorageId: "storage", SpaceId: "space", OpaqueId: "file"}
+ res, err := multistatusResponse(context.Background(), "", "", []*searchmsg.Match{{Entity: &searchmsg.Entity{
+ Ref: &searchmsg.Reference{ResourceId: id},
+ Id: id,
+ Audio: &searchmsg.Audio{},
+ Photo: &searchmsg.Photo{FNumber: proto.Float32(1.8), TakenDateTime: timestamppb.New(time.Date(2018, 1, 1, 12, 34, 56, 0, time.UTC))},
+ LivePhoto: &searchmsg.LivePhoto{ContentId: proto.String("a&b"), StillImageTimeUs: proto.Int64(1233333)},
+ }}}, &userv1beta1.User{Id: &userv1beta1.UserId{}})
+ Expect(err).ToNot(HaveOccurred())
+
+ Expect(string(res)).To(ContainSubstring("1.82018-01-01T12:34:56Z"))
+ Expect(string(res)).To(ContainSubstring("a&b1233333"))
+ Expect(string(res)).ToNot(ContainSubstring("oc:audio"))
+ })
+})