Skip to content

Commit

Permalink
Implement listing favorites via the dav report API (#2086)
Browse files Browse the repository at this point in the history
  • Loading branch information
David Christofas authored Oct 5, 2021
1 parent 27531e0 commit 4676db3
Show file tree
Hide file tree
Showing 12 changed files with 414 additions and 21 deletions.
7 changes: 7 additions & 0 deletions changelog/unreleased/list-favorites.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Enhancement: Implement listing favorites via the dav report API

Added filter-files to the dav REPORT API. This enables the listing of
favorites.

https://github.com/cs3org/reva/pull/2071
https://github.com/cs3org/reva/pull/2086
11 changes: 7 additions & 4 deletions internal/http/services/owncloud/ocdav/ocdav.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import (
"github.com/cs3org/reva/pkg/rhttp/global"
"github.com/cs3org/reva/pkg/rhttp/router"
"github.com/cs3org/reva/pkg/sharedconf"
"github.com/cs3org/reva/pkg/storage/favorite"
"github.com/cs3org/reva/pkg/storage/utils/templates"
"github.com/mitchellh/mapstructure"
"github.com/pkg/errors"
Expand Down Expand Up @@ -110,10 +111,11 @@ func (c *Config) init() {
}

type svc struct {
c *Config
webDavHandler *WebDavHandler
davHandler *DavHandler
client *http.Client
c *Config
webDavHandler *WebDavHandler
davHandler *DavHandler
favoritesManager favorite.Manager
client *http.Client
}

// New returns a new ocdav
Expand All @@ -133,6 +135,7 @@ func New(m map[string]interface{}, log *zerolog.Logger) (global.Service, error)
rhttp.Timeout(time.Duration(conf.Timeout*int64(time.Second))),
rhttp.Insecure(conf.Insecure),
),
favoritesManager: favorite.NewInMemoryManager(),
}
// initialize handlers and set default configs
if err := s.webDavHandler.init(conf.WebdavNamespace, true); err != nil {
Expand Down
4 changes: 2 additions & 2 deletions internal/http/services/owncloud/ocdav/propfind.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ func (s *svc) handleSpacesPropfind(w http.ResponseWriter, r *http.Request, space
}

func (s *svc) propfindResponse(ctx context.Context, w http.ResponseWriter, r *http.Request, namespace string, pf propfindXML, parentInfo *provider.ResourceInfo, resourceInfos []*provider.ResourceInfo, log zerolog.Logger) {
propRes, err := s.formatPropfind(ctx, &pf, resourceInfos, namespace)
propRes, err := s.multistatusResponse(ctx, &pf, resourceInfos, namespace)
if err != nil {
log.Error().Err(err).Msg("error formatting propfind")
w.WriteHeader(http.StatusInternalServerError)
Expand Down Expand Up @@ -342,7 +342,7 @@ func readPropfind(r io.Reader) (pf propfindXML, status int, err error) {
return pf, 0, nil
}

func (s *svc) formatPropfind(ctx context.Context, pf *propfindXML, mds []*provider.ResourceInfo, ns string) (string, error) {
func (s *svc) multistatusResponse(ctx context.Context, pf *propfindXML, mds []*provider.ResourceInfo, ns string) (string, error) {
responses := make([]*responseXML, 0, len(mds))
for i := range mds {
res, err := s.mdToPropResponse(ctx, pf, mds[i], ns)
Expand Down
28 changes: 28 additions & 0 deletions internal/http/services/owncloud/ocdav/proppatch.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/cs3org/reva/pkg/appctx"
ctxpkg "github.com/cs3org/reva/pkg/ctx"
rtrace "github.com/cs3org/reva/pkg/trace"
"github.com/pkg/errors"
"github.com/rs/zerolog"
Expand Down Expand Up @@ -232,6 +233,19 @@ func (s *svc) handleProppatch(ctx context.Context, w http.ResponseWriter, r *htt
HandleErrorStatus(&log, w, res.Status)
return nil, nil, false
}
if key == "http://owncloud.org/ns/favorite" {
statRes, err := c.Stat(ctx, &provider.StatRequest{Ref: ref})
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return nil, nil, false
}
currentUser := ctxpkg.ContextMustGetUser(ctx)
err = s.favoritesManager.UnsetFavorite(ctx, currentUser.Id, statRes.Info.Id)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return nil, nil, false
}
}
removedProps = append(removedProps, propNameXML)
} else {
sreq.ArbitraryMetadata.Metadata[key] = value
Expand Down Expand Up @@ -259,6 +273,20 @@ func (s *svc) handleProppatch(ctx context.Context, w http.ResponseWriter, r *htt

acceptedProps = append(acceptedProps, propNameXML)
delete(sreq.ArbitraryMetadata.Metadata, key)

if key == "http://owncloud.org/ns/favorite" {
statRes, err := c.Stat(ctx, &provider.StatRequest{Ref: ref})
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return nil, nil, false
}
currentUser := ctxpkg.ContextMustGetUser(ctx)
err = s.favoritesManager.SetFavorite(ctx, currentUser.Id, statRes.Info.Id)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return nil, nil, false
}
}
}
}
// FIXME: in case of error, need to set all properties back to the original state,
Expand Down
2 changes: 1 addition & 1 deletion internal/http/services/owncloud/ocdav/publicfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ func (s *svc) handlePropfindOnToken(w http.ResponseWriter, r *http.Request, ns s

infos := s.getPublicFileInfos(onContainer, depth == "0", tokenStatInfo)

propRes, err := s.formatPropfind(ctx, &pf, infos, ns)
propRes, err := s.multistatusResponse(ctx, &pf, infos, ns)
if err != nil {
sublog.Error().Err(err).Msg("error formatting propfind")
w.WriteHeader(http.StatusInternalServerError)
Expand Down
97 changes: 96 additions & 1 deletion internal/http/services/owncloud/ocdav/report.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,18 @@ import (
"encoding/xml"
"io"
"net/http"
"strings"

rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/cs3org/reva/pkg/appctx"
ctxpkg "github.com/cs3org/reva/pkg/ctx"
)

const (
elementNameSearchFiles = "search-files"
elementNameFilterFiles = "filter-files"
)

func (s *svc) handleReport(w http.ResponseWriter, r *http.Request, ns string) {
Expand All @@ -42,6 +52,11 @@ func (s *svc) handleReport(w http.ResponseWriter, r *http.Request, ns string) {
return
}

if rep.FilterFiles != nil {
s.doFilterFiles(w, r, rep.FilterFiles, ns)
return
}

// TODO(jfd): implement report

w.WriteHeader(http.StatusNotImplemented)
Expand All @@ -59,9 +74,70 @@ func (s *svc) doSearchFiles(w http.ResponseWriter, r *http.Request, sf *reportSe
w.WriteHeader(http.StatusNotImplemented)
}

func (s *svc) doFilterFiles(w http.ResponseWriter, r *http.Request, ff *reportFilterFiles, namespace string) {
ctx := r.Context()
log := appctx.GetLogger(ctx)

if ff.Rules.Favorite {
// List the users favorite resources.
currentUser := ctxpkg.ContextMustGetUser(ctx)
favorites, err := s.favoritesManager.ListFavorites(ctx, currentUser.Id)
if err != nil {
log.Error().Err(err).Msg("error getting favorites")
w.WriteHeader(http.StatusInternalServerError)
return
}

client, err := s.getClient()
if err != nil {
log.Error().Err(err).Msg("error getting gateway client")
w.WriteHeader(http.StatusInternalServerError)
return
}

infos := make([]*provider.ResourceInfo, 0, len(favorites))
for i := range favorites {
statRes, err := client.Stat(ctx, &providerv1beta1.StatRequest{Ref: &providerv1beta1.Reference{ResourceId: favorites[i]}})
if err != nil {
log.Error().Err(err).Msg("error getting resource info")
continue
}
if statRes.Status.Code != rpcv1beta1.Code_CODE_OK {
log.Error().Interface("stat_response", statRes).Msg("error getting resource info")
continue
}

// The paths we receive have the format /user/<username>/<filepath>
// We only want the `<filepath>` part. Thus we remove the /user/<username>/ part.
parts := strings.SplitN(statRes.Info.Path, "/", 4)
if len(parts) != 4 {
log.Error().Str("path", statRes.Info.Path).Msg("path doesn't have the expected format")
continue
}
statRes.Info.Path = parts[3]

infos = append(infos, statRes.Info)
}

responsesXML, err := s.multistatusResponse(ctx, &propfindXML{Prop: ff.Prop}, infos, namespace)
if err != nil {
log.Error().Err(err).Msg("error formatting propfind")
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Header().Set(HeaderDav, "1, 3, extended-mkcol")
w.Header().Set(HeaderContentType, "application/xml; charset=utf-8")
w.WriteHeader(http.StatusMultiStatus)
if _, err := w.Write([]byte(responsesXML)); err != nil {
log.Err(err).Msg("error writing response")
}
}
}

type report struct {
SearchFiles *reportSearchFiles
// FilterFiles TODO add this for tag based search
FilterFiles *reportFilterFiles `xml:"filter-files"`
}
type reportSearchFiles struct {
XMLName xml.Name `xml:"search-files"`
Expand All @@ -75,6 +151,18 @@ type reportSearchFilesSearch struct {
Offset int `xml:"offset"`
}

type reportFilterFiles struct {
XMLName xml.Name `xml:"filter-files"`
Lang string `xml:"xml:lang,attr,omitempty"`
Prop propfindProps `xml:"DAV: prop"`
Rules reportFilterFilesRules `xml:"filter-rules"`
}

type reportFilterFilesRules struct {
Favorite bool `xml:"favorite"`
SystemTag int `xml:"systemtag"`
}

func readReport(r io.Reader) (rep *report, status int, err error) {
decoder := xml.NewDecoder(r)
rep = &report{}
Expand All @@ -89,13 +177,20 @@ func readReport(r io.Reader) (rep *report, status int, err error) {
}

if v, ok := t.(xml.StartElement); ok {
if v.Name.Local == "search-files" {
if v.Name.Local == elementNameSearchFiles {
var repSF reportSearchFiles
err = decoder.DecodeElement(&repSF, &v)
if err != nil {
return nil, http.StatusBadRequest, err
}
rep.SearchFiles = &repSF
} else if v.Name.Local == elementNameFilterFiles {
var repFF reportFilterFiles
err = decoder.DecodeElement(&repFF, &v)
if err != nil {
return nil, http.StatusBadRequest, err
}
rep.FilterFiles = &repFF
}
}
}
Expand Down
63 changes: 63 additions & 0 deletions internal/http/services/owncloud/ocdav/report_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.

package ocdav

import (
"strings"
"testing"
)

func TestUnmarshallReportFilterFiles(t *testing.T) {
ffXML := `<oc:filter-files xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
<d:prop>
<d:getlastmodified />
<d:getetag />
<d:getcontenttype />
<d:resourcetype />
<oc:fileid />
<oc:permissions />
<oc:size />
<d:getcontentlength />
<oc:tags />
<oc:favorite />
<d:lockdiscovery />
<oc:comments-unread />
<oc:owner-display-name />
<oc:share-types />
</d:prop>
<oc:filter-rules>
<oc:favorite>1</oc:favorite>
</oc:filter-rules>
</oc:filter-files>`

reader := strings.NewReader(ffXML)

report, status, err := readReport(reader)
if status != 0 || err != nil {
t.Error("Failed to unmarshal filter-files xml")
}

if report.FilterFiles == nil {
t.Error("Failed to unmarshal filter-files xml. FilterFiles is nil")
}

if report.FilterFiles.Rules.Favorite == false {
t.Error("Failed to correctly unmarshal filter-rules. Favorite is expected to be true.")
}
}
2 changes: 1 addition & 1 deletion internal/http/services/owncloud/ocdav/versions.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ func (h *VersionsHandler) doListVersions(w http.ResponseWriter, r *http.Request,
infos = append(infos, vi)
}

propRes, err := s.formatPropfind(ctx, &pf, infos, "")
propRes, err := s.multistatusResponse(ctx, &pf, infos, "")
if err != nil {
sublog.Error().Err(err).Msg("error formatting propfind")
w.WriteHeader(http.StatusInternalServerError)
Expand Down
Loading

0 comments on commit 4676db3

Please sign in to comment.