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("") + } + 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")) + }) +})