Skip to content

Commit

Permalink
Webdav trash-bin API for spaces (#2628)
Browse files Browse the repository at this point in the history
* implement propfinds for spaces trash-bin

* implement webdav delete for spaces trash-bin

* refactor overwrite header parsing

* implement restore for spaces trashbin

* fix restore for nested spaces

There can be nested spaces e.g. the 'Shares Jail' inside a users space
  • Loading branch information
David Christofas authored Mar 15, 2022
1 parent 991d38a commit a24b53d
Show file tree
Hide file tree
Showing 11 changed files with 268 additions and 291 deletions.
5 changes: 5 additions & 0 deletions changelog/unreleased/spaces-trashbin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Enhancement: Add spaces aware trash-bin API

Added the webdav trash-bin endpoint for spaces.

https://github.com/cs3org/reva/pull/2628
33 changes: 12 additions & 21 deletions internal/http/services/owncloud/ocdav/copy.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import (
"path"
"path/filepath"
"strconv"
"strings"

gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
Expand Down Expand Up @@ -56,7 +55,10 @@ func (s *svc) handlePathCopy(w http.ResponseWriter, r *http.Request, ns string)
defer span.End()

src := path.Join(ns, r.URL.Path)
dst, err := extractDestination(r)

dh := r.Header.Get(net.HeaderDestination)
baseURI := r.Context().Value(net.CtxKeyBaseURI).(string)
dst, err := net.ParseDestination(baseURI, dh)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
Expand Down Expand Up @@ -275,7 +277,9 @@ func (s *svc) handleSpacesCopy(w http.ResponseWriter, r *http.Request, spaceID s
ctx, span := rtrace.Provider.Tracer("reva").Start(r.Context(), "spaces_copy")
defer span.End()

dst, err := extractDestination(r)
dh := r.Header.Get(net.HeaderDestination)
baseURI := r.Context().Value(net.CtxKeyBaseURI).(string)
dst, err := net.ParseDestination(baseURI, dh)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
Expand Down Expand Up @@ -480,7 +484,8 @@ func (s *svc) executeSpacesCopy(ctx context.Context, w http.ResponseWriter, clie
}

func (s *svc) prepareCopy(ctx context.Context, w http.ResponseWriter, r *http.Request, srcRef, dstRef *provider.Reference, log *zerolog.Logger) *copy {
overwrite, err := extractOverwrite(w, r)
oh := r.Header.Get(net.HeaderOverwrite)
overwrite, err := net.ParseOverwrite(oh)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
m := fmt.Sprintf("Overwrite header is set to incorrect value %v", overwrite)
Expand All @@ -504,7 +509,7 @@ func (s *svc) prepareCopy(ctx context.Context, w http.ResponseWriter, r *http.Re
depth = net.DepthInfinity
}

log.Debug().Str("overwrite", overwrite).Str("depth", depth.String()).Msg("copy")
log.Debug().Bool("overwrite", overwrite).Str("depth", depth.String()).Msg("copy")

client, err := s.getClient()
if err != nil {
Expand Down Expand Up @@ -548,8 +553,8 @@ func (s *svc) prepareCopy(ctx context.Context, w http.ResponseWriter, r *http.Re
if dstStatRes.Status.Code == rpc.Code_CODE_OK {
successCode = http.StatusNoContent // 204 if target already existed, see https://tools.ietf.org/html/rfc4918#section-9.8.5

if overwrite == "F" {
log.Warn().Str("overwrite", overwrite).Msg("dst already exists")
if !overwrite {
log.Warn().Bool("overwrite", overwrite).Msg("dst already exists")
w.WriteHeader(http.StatusPreconditionFailed)
m := fmt.Sprintf("Could not overwrite Resource %v", dstRef.Path)
b, err := errors.Marshal(http.StatusPreconditionFailed, m, "")
Expand Down Expand Up @@ -598,17 +603,3 @@ func (s *svc) prepareCopy(ctx context.Context, w http.ResponseWriter, r *http.Re

return &copy{source: srcRef, sourceInfo: srcStatRes.Info, depth: depth, successCode: successCode, destination: dstRef}
}

func extractOverwrite(w http.ResponseWriter, r *http.Request) (string, error) {
overwrite := r.Header.Get(net.HeaderOverwrite)
overwrite = strings.ToUpper(overwrite)
if overwrite == "" {
overwrite = "T"
}

if overwrite != "T" && overwrite != "F" {
return "", errInvalidValue
}

return overwrite, nil
}
15 changes: 9 additions & 6 deletions internal/http/services/owncloud/ocdav/dav.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import (
gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/errors"
"github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/net"
Expand All @@ -39,6 +38,10 @@ import (
"google.golang.org/grpc/metadata"
)

const (
_trashbinPath = "trash-bin"
)

type tokenStatInfoKey struct{}

// DavHandler routes to the different sub handlers
Expand Down Expand Up @@ -172,7 +175,7 @@ func (h *DavHandler) Handler(s *svc) http.Handler {
base := path.Join(ctx.Value(net.CtxKeyBaseURI).(string), "spaces")
ctx := context.WithValue(ctx, net.CtxKeyBaseURI, base)
r = r.WithContext(ctx)
h.SpacesHandler.Handler(s).ServeHTTP(w, r)
h.SpacesHandler.Handler(s, h.TrashbinHandler).ServeHTTP(w, r)
case "public-files":
base := path.Join(ctx.Value(net.CtxKeyBaseURI).(string), "public-files")
ctx = context.WithValue(ctx, net.CtxKeyBaseURI, base)
Expand Down Expand Up @@ -203,9 +206,9 @@ func (h *DavHandler) Handler(s *svc) http.Handler {
case err != nil:
w.WriteHeader(http.StatusInternalServerError)
return
case res.Status.Code == rpcv1beta1.Code_CODE_PERMISSION_DENIED:
case res.Status.Code == rpc.Code_CODE_PERMISSION_DENIED:
fallthrough
case res.Status.Code == rpcv1beta1.Code_CODE_UNAUTHENTICATED:
case res.Status.Code == rpc.Code_CODE_UNAUTHENTICATED:
w.WriteHeader(http.StatusUnauthorized)
if hasValidBasicAuthHeader {
b, err := errors.Marshal(http.StatusUnauthorized, "Username or password was incorrect", "")
Expand All @@ -215,10 +218,10 @@ func (h *DavHandler) Handler(s *svc) http.Handler {
b, err := errors.Marshal(http.StatusUnauthorized, "No 'Authorization: Basic' header found", "")
errors.HandleWebdavError(log, w, b, err)
return
case res.Status.Code == rpcv1beta1.Code_CODE_NOT_FOUND:
case res.Status.Code == rpc.Code_CODE_NOT_FOUND:
w.WriteHeader(http.StatusNotFound)
return
case res.Status.Code != rpcv1beta1.Code_CODE_OK:
case res.Status.Code != rpc.Code_CODE_OK:
w.WriteHeader(http.StatusInternalServerError)
return
}
Expand Down
25 changes: 12 additions & 13 deletions internal/http/services/owncloud/ocdav/move.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import (
"fmt"
"net/http"
"path"
"strings"

rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
Expand All @@ -43,7 +42,9 @@ func (s *svc) handlePathMove(w http.ResponseWriter, r *http.Request, ns string)
defer span.End()

srcPath := path.Join(ns, r.URL.Path)
dstPath, err := extractDestination(r)
dh := r.Header.Get(net.HeaderDestination)
baseURI := r.Context().Value(net.CtxKeyBaseURI).(string)
dstPath, err := net.ParseDestination(baseURI, dh)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
Expand Down Expand Up @@ -99,7 +100,9 @@ func (s *svc) handleSpacesMove(w http.ResponseWriter, r *http.Request, srcSpaceI
ctx, span := rtrace.Provider.Tracer("ocdav").Start(r.Context(), "spaces_move")
defer span.End()

dst, err := extractDestination(r)
dh := r.Header.Get(net.HeaderDestination)
baseURI := r.Context().Value(net.CtxKeyBaseURI).(string)
dst, err := net.ParseDestination(baseURI, dh)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
Expand Down Expand Up @@ -145,15 +148,11 @@ func (s *svc) handleSpacesMove(w http.ResponseWriter, r *http.Request, srcSpaceI
}

func (s *svc) handleMove(ctx context.Context, w http.ResponseWriter, r *http.Request, src, dst *provider.Reference, log zerolog.Logger) {
overwrite := r.Header.Get(net.HeaderOverwrite)
log.Debug().Str("overwrite", overwrite).Msg("move")
oh := r.Header.Get(net.HeaderOverwrite)
log.Debug().Str("overwrite", oh).Msg("move")

overwrite = strings.ToUpper(overwrite)
if overwrite == "" {
overwrite = "T"
}

if overwrite != "T" && overwrite != "F" {
overwrite, err := net.ParseOverwrite(oh)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
Expand Down Expand Up @@ -201,8 +200,8 @@ func (s *svc) handleMove(ctx context.Context, w http.ResponseWriter, r *http.Req
if dstStatRes.Status.Code == rpc.Code_CODE_OK {
successCode = http.StatusNoContent // 204 if target already existed, see https://tools.ietf.org/html/rfc4918#section-9.9.4

if overwrite == "F" {
log.Warn().Str("overwrite", overwrite).Msg("dst already exists")
if !overwrite {
log.Warn().Bool("overwrite", overwrite).Msg("dst already exists")
w.WriteHeader(http.StatusPreconditionFailed) // 412, see https://tools.ietf.org/html/rfc4918#section-9.9.4
return
}
Expand Down
44 changes: 43 additions & 1 deletion internal/http/services/owncloud/ocdav/net/net.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,16 @@ package net

import (
"fmt"
"net/url"
"regexp"
"strings"

"github.com/pkg/errors"
)

var (
// ErrInvalidHeaderValue defines an error which can occure when trying to parse a header value.
ErrInvalidHeaderValue = errors.New("invalid value")
)

type ctxKey int
Expand Down Expand Up @@ -110,6 +118,40 @@ func ParseDepth(s string) (Depth, error) {
case DepthInfinity.String():
return DepthInfinity, nil
default:
return "", fmt.Errorf("invalid depth: %s", s)
return "", errors.Wrapf(ErrInvalidHeaderValue, "invalid depth: %s", s)
}
}

// ParseOverwrite parses the overwrite header value defined in https://datatracker.ietf.org/doc/html/rfc4918#section-10.6
// Valid values are "T" and "F". An empty string will be parse to true.
func ParseOverwrite(s string) (bool, error) {
if s == "" {
s = "T"
}
if s != "T" && s != "F" {
return false, errors.Wrapf(ErrInvalidHeaderValue, "invalid overwrite: %s", s)
}
return s == "T", nil
}

// ParseDestination parses the destination header value defined in https://datatracker.ietf.org/doc/html/rfc4918#section-10.3
// The returned path will be relative to the given baseURI.
func ParseDestination(baseURI, s string) (string, error) {
if s == "" {
return "", errors.Wrap(ErrInvalidHeaderValue, "destination header is empty")
}
dstURL, err := url.ParseRequestURI(s)
if err != nil {
return "", errors.Wrap(ErrInvalidHeaderValue, err.Error())
}

// TODO check if path is on same storage, return 502 on problems, see https://tools.ietf.org/html/rfc4918#section-9.9.4
// TODO make request.php optional in destination header
// Strip the base URI from the destination. The destination might contain redirection prefixes which need to be handled
urlSplit := strings.Split(dstURL.Path, baseURI)
if len(urlSplit) != 2 {
return "", errors.Wrap(ErrInvalidHeaderValue, "destination path does not contain base URI")
}

return urlSplit[1], nil
}
23 changes: 23 additions & 0 deletions internal/http/services/owncloud/ocdav/net/net_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,4 +90,27 @@ var _ = Describe("Net", func() {
Expect(medianDuration).To(BeNumerically("<", 10*time.Millisecond))
})
})

DescribeTable("TestParseOverwrite",
func(v string, expectSuccess bool, expectedValue bool) {
parsed, err := net.ParseOverwrite(v)
Expect(err == nil).To(Equal(expectSuccess))
Expect(parsed).To(Equal(expectedValue))
},
Entry("default", "", true, true),
Entry("T", "T", true, true),
Entry("F", "F", true, false),
Entry("invalid", "invalid", false, false))

DescribeTable("TestParseDestination",
func(baseURI, v string, expectSuccess bool, expectedValue string) {
parsed, err := net.ParseDestination(baseURI, v)
Expect(err == nil).To(Equal(expectSuccess))
Expect(parsed).To(Equal(expectedValue))
},
Entry("invalid1", "", "", false, ""),
Entry("invalid2", "baseURI", "", false, ""),
Entry("invalid3", "", "/dest/path", false, ""),
Entry("invalid4", "/foo", "/dest/path", false, ""),
Entry("valid", "/foo", "https://example.com/foo/dest/path", true, "/dest/path"))
})
25 changes: 0 additions & 25 deletions internal/http/services/owncloud/ocdav/ocdav.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ package ocdav
import (
"context"
"net/http"
"net/url"
"path"
"strings"
"time"
Expand All @@ -42,13 +41,10 @@ import (
"github.com/cs3org/reva/v2/pkg/storage/favorite/registry"
"github.com/cs3org/reva/v2/pkg/storage/utils/templates"
"github.com/mitchellh/mapstructure"
"github.com/pkg/errors"
"github.com/rs/zerolog"
)

var (
errInvalidValue = errors.New("invalid value")

nameRules = [...]nameRule{
nameNotEmpty{},
nameDoesNotContain{chars: "\f\r\n\\"},
Expand Down Expand Up @@ -339,24 +335,3 @@ func addAccessHeaders(w http.ResponseWriter, r *http.Request) {
headers.Set("Strict-Transport-Security", "max-age=63072000")
}
}

func extractDestination(r *http.Request) (string, error) {
dstHeader := r.Header.Get(net.HeaderDestination)
if dstHeader == "" {
return "", errors.Wrap(errInvalidValue, "destination header is empty")
}
dstURL, err := url.ParseRequestURI(dstHeader)
if err != nil {
return "", errors.Wrap(errInvalidValue, err.Error())
}

baseURI := r.Context().Value(net.CtxKeyBaseURI).(string)
// TODO check if path is on same storage, return 502 on problems, see https://tools.ietf.org/html/rfc4918#section-9.9.4
// Strip the base URI from the destination. The destination might contain redirection prefixes which need to be handled
urlSplit := strings.Split(dstURL.Path, baseURI)
if len(urlSplit) != 2 {
return "", errors.Wrap(errInvalidValue, "destination path does not contain base URI")
}

return urlSplit[1], nil
}
Loading

0 comments on commit a24b53d

Please sign in to comment.