Repository: cs3org/reva

Issue:
Spaces incompatibility
The current implementation of spaces breaks standard WebDAV navigation.

When requesting a path like `/webdav/cephfs` I get the following hrefs:
`cephmount-%24F5SW64ZPOVZWK4RPM4XWO33OPJQWY2DV/AMS02`, which they are not a prefix of `/webdav/cephfs`.

This must not be the case.

From ownCloud Web it works because the client requests like `ephmount-%24F5SW64ZPOVZWK4RPM4XWO33OPJQWY2DV/AMS02`.

This behaviour makes Reva not compatible with standard path-based tools, like davfs, etc ... 

Hints from issue comments:

Solution patch:
diff --git a/changelog/unreleased/fix-spaces-dav.md b/changelog/unreleased/fix-spaces-dav.md
new file mode 100644
index 00000000..efab66cd
--- /dev/null
+++ b/changelog/unreleased/fix-spaces-dav.md
@@ -0,0 +1,5 @@
+Bugfix: Bring back proper DAV support
+
+Spaces broke proper DAV support, because returned hrefs in the PROPFIND always contained space IDs, even if these were not present in the incoming request. This is fixed now, by writing the href based in the incoming URL
+
+https://github.com/cs3org/reva/pull/5409
diff --git a/internal/http/services/owncloud/ocdav/copy.go b/internal/http/services/owncloud/ocdav/copy.go
index c56fc9b4..28ecb28c 100644
--- a/internal/http/services/owncloud/ocdav/copy.go
+++ b/internal/http/services/owncloud/ocdav/copy.go
@@ -32,8 +32,6 @@ import (
 	typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
 	"github.com/cs3org/reva/v3/internal/http/services/datagateway"
 	"github.com/cs3org/reva/v3/pkg/appctx"
-	"github.com/cs3org/reva/v3/pkg/rhttp/router"
-	"github.com/cs3org/reva/v3/pkg/spaces"
 	"github.com/rs/zerolog"
 )
 
@@ -63,7 +61,7 @@ func (s *svc) handlePathCopy(w http.ResponseWriter, r *http.Request, ns string)
 	}
 
 	// Local copy: in this case Destination is mandatory
-	dst, err := extractDestination(r)
+	dst, err := extractDestination(r, ns)
 	if err != nil {
 		appctx.GetLogger(ctx).Warn().Msg("HTTP COPY: failed to extract destination")
 		w.WriteHeader(http.StatusBadRequest)
@@ -82,18 +80,6 @@ func (s *svc) handlePathCopy(w http.ResponseWriter, r *http.Request, ns string)
 		}
 	}
 
-	// For the destination, we still need to handle this ourselves
-	if s.c.SpacesEnabled && ns != "/public" {
-		dstSpaceID, dstRelPath := router.ShiftPath(dst)
-		_, spaceRoot, ok := spaces.DecodeStorageSpaceID(dstSpaceID)
-		if !ok {
-			appctx.GetLogger(ctx).Warn().Str("dstSpaceID", dstSpaceID).Msg("handlePathCopy: Failed to parse destination space ID")
-		}
-		dst = path.Join(spaceRoot, dstRelPath)
-	} else {
-		dst = path.Join(ns, dst)
-	}
-
 	srcRef := &provider.Reference{
 		Path: src,
 	}
diff --git a/internal/http/services/owncloud/ocdav/dav.go b/internal/http/services/owncloud/ocdav/dav.go
index 889b4983..d1cc67f1 100644
--- a/internal/http/services/owncloud/ocdav/dav.go
+++ b/internal/http/services/owncloud/ocdav/dav.go
@@ -60,6 +60,10 @@ const (
 	ErrFileNotFoundInRoot = "ERR_FILE_NOT_FOUND_IN_ROOT"
 )
 
+const (
+	ctxKeyIncomingURL = "ctxKeyIncomingURL"
+)
+
 func (h *DavHandler) init(c *Config) error {
 	h.AvatarsHandler = new(AvatarsHandler)
 	if err := h.AvatarsHandler.init(c); err != nil {
diff --git a/internal/http/services/owncloud/ocdav/move.go b/internal/http/services/owncloud/ocdav/move.go
index 08e2d63b..189eca85 100644
--- a/internal/http/services/owncloud/ocdav/move.go
+++ b/internal/http/services/owncloud/ocdav/move.go
@@ -37,7 +37,7 @@ import (
 func (s *svc) handlePathMove(w http.ResponseWriter, r *http.Request, ns string) {
 	ctx := r.Context()
 	srcPath := path.Join(ns, r.URL.Path)
-	dstPath, err := extractDestination(r)
+	dstPath, err := extractDestination(r, ns)
 	if err != nil {
 		w.WriteHeader(http.StatusBadRequest)
 		return
@@ -69,50 +69,6 @@ func (s *svc) handlePathMove(w http.ResponseWriter, r *http.Request, ns string)
 	s.handleMove(ctx, w, r, src, dst, intermediateDirRefFunc, sublog)
 }
 
-func (s *svc) handleSpacesMove(w http.ResponseWriter, r *http.Request, srcSpaceID string) {
-	ctx := r.Context()
-	dst, err := extractDestination(r)
-	if err != nil {
-		w.WriteHeader(http.StatusBadRequest)
-		return
-	}
-
-	sublog := appctx.GetLogger(ctx).With().Str("spaceid", srcSpaceID).Str("path", r.URL.Path).Logger()
-	// retrieve a specific storage space
-	srcRef, status, err := s.lookUpStorageSpaceReference(ctx, srcSpaceID, r.URL.Path)
-	if err != nil {
-		sublog.Error().Err(err).Msg("error sending a grpc request")
-		w.WriteHeader(http.StatusInternalServerError)
-		return
-	}
-
-	if status.Code != rpc.Code_CODE_OK {
-		HandleErrorStatus(&sublog, w, status)
-		return
-	}
-
-	dstSpaceID, dstRelPath := router.ShiftPath(dst)
-
-	// retrieve a specific storage space
-	dstRef, status, err := s.lookUpStorageSpaceReference(ctx, dstSpaceID, dstRelPath)
-	if err != nil {
-		sublog.Error().Err(err).Msg("error sending a grpc request")
-		w.WriteHeader(http.StatusInternalServerError)
-		return
-	}
-
-	if status.Code != rpc.Code_CODE_OK {
-		HandleErrorStatus(&sublog, w, status)
-		return
-	}
-
-	intermediateDirRefFunc := func() (*provider.Reference, *rpc.Status, error) {
-		intermediateDir := path.Dir(dstRelPath)
-		return s.lookUpStorageSpaceReference(ctx, dstSpaceID, intermediateDir)
-	}
-	s.handleMove(ctx, w, r, srcRef, dstRef, intermediateDirRefFunc, sublog)
-}
-
 func (s *svc) handleMove(ctx context.Context, w http.ResponseWriter, r *http.Request, src, dst *provider.Reference, intermediateDirRef intermediateDirRefFunc, log zerolog.Logger) {
 	overwrite := r.Header.Get(HeaderOverwrite)
 	log.Debug().Str("overwrite", overwrite).Msg("move")
diff --git a/internal/http/services/owncloud/ocdav/ocdav.go b/internal/http/services/owncloud/ocdav/ocdav.go
index bc2d6c6a..d038ab7f 100644
--- a/internal/http/services/owncloud/ocdav/ocdav.go
+++ b/internal/http/services/owncloud/ocdav/ocdav.go
@@ -35,6 +35,7 @@ import (
 	storageProvider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
 	"github.com/cs3org/reva/v3/pkg/appctx"
 	"github.com/cs3org/reva/v3/pkg/myofficefiles"
+	"github.com/cs3org/reva/v3/pkg/spaces"
 	"github.com/cs3org/reva/v3/pkg/trace"
 	"github.com/cs3org/reva/v3/pkg/utils"
 
@@ -131,7 +132,6 @@ type Config struct {
 	DisabledOpenInAppPaths       []string                  `mapstructure:"disabled_open_in_app_paths"`
 	Notifications                map[string]any            `docs:"nil; settings for the notification helper" mapstructure:"notifications"`
 	MyOfficeFilesAllowedProjects []string                  `mapstructure:"my_office_files_projects"`
-	SpacesEnabled                bool                      `mapstructure:"spaces_enabled"`
 }
 
 func (c *Config) ApplyDefaults() {
@@ -242,6 +242,10 @@ func (s *svc) Handler() http.Handler {
 		// always starts with /
 		base := path.Join("/", s.Prefix())
 
+		// We store the actual incoming URL
+		ctx = context.WithValue(ctx, ctxKeyIncomingURL, r.URL.Path)
+		r = r.WithContext(ctx)
+
 		var head string
 		head, r.URL.Path = router.ShiftPath(r.URL.Path)
 		log.Debug().Str("head", head).Str("tail", r.URL.Path).Msg("http routing")
@@ -356,7 +360,7 @@ func addAccessHeaders(w http.ResponseWriter, r *http.Request) {
 	}
 }
 
-func extractDestination(r *http.Request) (string, error) {
+func extractDestination(r *http.Request, ns string) (string, error) {
 	dstHeader := r.Header.Get(HeaderDestination)
 	if dstHeader == "" {
 		return "", errors.Wrap(errInvalidValue, "destination header is empty")
@@ -370,8 +374,18 @@ func extractDestination(r *http.Request) (string, error) {
 	// 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
 	destination := strings.TrimPrefix(dstURL.Path, baseURI)
-	return destination, nil
 
+	// If the destination is in a spaces format, we replace with the space path
+	dstSpaceID, dstRelPath := router.ShiftPath(destination)
+	_, spaceRoot, ok := spaces.DecodeStorageSpaceID(dstSpaceID)
+	if ok && ns != "/public" {
+		destination = path.Join(spaceRoot, dstRelPath)
+	} else {
+		// If it is non-spaces, we join the namespace
+		destination = path.Join(ns, destination)
+	}
+
+	return destination, nil
 }
 
 // replaceAllStringSubmatchFunc is taken from 'Go: Replace String with Regular Expression Callback'
diff --git a/internal/http/services/owncloud/ocdav/propfind.go b/internal/http/services/owncloud/ocdav/propfind.go
index 686172f5..9c05e076 100644
--- a/internal/http/services/owncloud/ocdav/propfind.go
+++ b/internal/http/services/owncloud/ocdav/propfind.go
@@ -46,7 +46,6 @@ import (
 	"github.com/pkg/errors"
 
 	"github.com/cs3org/reva/v3/pkg/publicshare"
-	"github.com/cs3org/reva/v3/pkg/rhttp/router"
 	"github.com/cs3org/reva/v3/pkg/share"
 	"github.com/cs3org/reva/v3/pkg/utils"
 	"github.com/cs3org/reva/v3/pkg/utils/resourceid"
@@ -168,8 +167,12 @@ func (s *svc) propfindResponse(ctx context.Context, w http.ResponseWriter, r *ht
 	} else {
 		log.Error().Err(err).Msg("propfindResponse: couldn't list user shares")
 	}
+	href := r.URL.Path
+	if ctxPath := ctx.Value(ctxKeyIncomingURL); ctxPath != nil {
+		href = ctxPath.(string)
+	}
 
-	propRes, err := s.multistatusResponse(ctx, &pf, resourceInfos, namespace, usershares, linkshares)
+	propRes, err := s.multistatusResponse(ctx, &pf, resourceInfos, parentInfo, namespace, href, usershares, linkshares)
 	if err != nil {
 		log.Error().Err(err).Msg("error formatting propfind")
 		w.WriteHeader(http.StatusInternalServerError)
@@ -435,10 +438,10 @@ func readPropfind(r io.Reader) (pf propfindXML, status int, err error) {
 	return pf, 0, nil
 }
 
-func (s *svc) multistatusResponse(ctx context.Context, pf *propfindXML, mds []*provider.ResourceInfo, ns string, usershares, linkshares map[string]struct{}) (string, error) {
+func (s *svc) multistatusResponse(ctx context.Context, pf *propfindXML, mds []*provider.ResourceInfo, parent *provider.ResourceInfo, ns, href string, usershares, linkshares map[string]struct{}) (string, error) {
 	responses := make([]*responseXML, 0, len(mds))
 	for i := range mds {
-		res, err := s.mdToPropResponse(ctx, pf, mds[i], ns, usershares, linkshares)
+		res, err := s.mdToPropResponse(ctx, pf, mds[i], parent, ns, href, usershares, linkshares)
 		if err != nil {
 			return "", err
 		}
@@ -487,48 +490,6 @@ func (s *svc) newPropRaw(key, val string) *propertyXML {
 	}
 }
 
-// Compute the URL of the resource in the spaces format:
-// baseURI + /<space_id>/relative/path/to/space
-// The space_id MUST be set on `md`.
-// The path of the space root it is calculated from `md.Id.SpaceId`
-// Note that the path on `md.Path` must also be set, and must be a path relative to the space root.
-func spaceHref(ctx context.Context, baseURI string, md *provider.ResourceInfo) (string, error) {
-	if ocm, _ := ctx.Value(ctxOCM).(bool); ocm {
-		// /<token>/ was injected in front of the OCM path for the routing to work, we now remove it (see internal/http/services/owncloud/ocdav/dav.go)
-		_, md.Path = router.ShiftPath(md.Path)
-	}
-
-	if linkToken, _ := ctx.Value(ctxPublicLink).(string); linkToken != "" {
-		_, relativePath := router.ShiftPath(md.Path)
-		_, relativePath = router.ShiftPath(relativePath)
-		return path.Join(baseURI, linkToken, relativePath), nil
-	}
-
-	if md.Id == nil || md.Id.SpaceId == "" {
-		return "", errors.New("Space ID must be set to calculate Href")
-	}
-
-	storageSpaceID := spaces.ConcatStorageSpaceID(md.Id.StorageId, md.Id.SpaceId)
-	_, spacePath, ok := spaces.DecodeStorageSpaceID(storageSpaceID)
-	if !ok {
-		return "", errors.New("Failed to decode space ID")
-	}
-
-	relativePath, err := filepath.Rel(spacePath, md.Path)
-	if err != nil {
-		return "", errors.Wrapf(err, "failed to calculate path relative to space root: %v", spacePath)
-	}
-
-	// When requesting for versions, the request URL is baseURI=/remote.php/dav/meta/<resource-id>
-	// When listing other resources, its value is baseURI=/remote.php/dav/spaces.
-	// Because of this, a different response is expected, without the storageSpaceID.
-	if md.Id.StorageId == "versions" {
-		return path.Join(baseURI, relativePath), nil
-	}
-
-	return path.Join(baseURI, storageSpaceID, relativePath), nil
-}
-
 func appendSlash(path string) string {
 	if path == "" {
 		return "/"
@@ -553,43 +514,29 @@ func (s *svc) isOpenable(path string) bool {
 // mdToPropResponse converts the CS3 metadata into a webdav PropResponse
 // ns is the CS3 namespace that needs to be removed from the CS3 path before
 // prefixing it with the baseURI.
-func (s *svc) mdToPropResponse(ctx context.Context, pf *propfindXML, md *provider.ResourceInfo, ns string, usershares, linkshares map[string]struct{}) (*responseXML, error) {
+func (s *svc) mdToPropResponse(ctx context.Context, pf *propfindXML, md *provider.ResourceInfo, parent *provider.ResourceInfo, ns, ref string, usershares, linkshares map[string]struct{}) (*responseXML, error) {
 	sublog := appctx.GetLogger(ctx).With().Str("ns", ns).Logger()
 
-	spacesEnabled := s.c.SpacesEnabled
-
 	baseURI := ctx.Value(ctxKeyBaseURI).(string)
-	var ref string
-	var err error
-	if spacesEnabled {
-		ref, err = spaceHref(ctx, baseURI, md)
-		if err != nil {
-			pxml := propstatXML{
-				Status: "HTTP/1.1 400 Bad Request",
-				Prop:   []*propertyXML{},
-			}
-
-			return &responseXML{
-				Href:     encodePath(ref),
-				Propstat: []propstatXML{pxml},
-			}, err
 
+	href := ref
+	if parent != nil {
+		relativePath, err := filepath.Rel(parent.Path, md.Path)
+		if err != nil {
+			return nil, errors.Wrapf(err, "failed to calculate path relative to parent: %v", md.Path)
 		}
-	} else {
-		md.Path = strings.TrimPrefix(md.Path, ns)
-
-		if ocm, _ := ctx.Value(ctxOCM).(bool); ocm {
-			// /<token>/ was injected in front of the OCM path for the routing to work, we now remove it (see internal/http/services/owncloud/ocdav/dav.go)
-			_, md.Path = router.ShiftPath(md.Path)
+		href, err = url.JoinPath(ref, relativePath)
+		if err != nil {
+			return nil, errors.Wrapf(err, "failed to join relative path with ref: %v", md.Path)
 		}
-		ref = path.Join(baseURI, md.Path)
 	}
+
 	if md.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER {
 		ref += "/"
 	}
 
 	response := responseXML{
-		Href:     encodePath(ref),
+		Href:     href,
 		Propstat: []propstatXML{},
 	}
 
@@ -648,17 +595,11 @@ func (s *svc) mdToPropResponse(ctx context.Context, pf *propfindXML, md *provide
 	if pf.Allprop != nil {
 		// return all known properties
 		if md.Id != nil {
-			if spacesEnabled {
-				id := spaces.EncodeResourceID(md.Id)
-				propstatOK.Prop = append(propstatOK.Prop,
-					s.newProp("oc:id", id),
-					s.newProp("oc:fileid", id))
-			} else {
-				id := spaces.ResourceIdToString(md.Id)
-				propstatOK.Prop = append(propstatOK.Prop,
-					s.newProp("oc:id", id),
-					s.newProp("oc:fileid", id))
-			}
+			id := spaces.EncodeResourceID(md.Id)
+			propstatOK.Prop = append(propstatOK.Prop,
+				s.newProp("oc:id", id),
+				s.newProp("oc:fileid", id))
+
 		}
 
 		if md.ParentId != nil {
@@ -761,7 +702,7 @@ func (s *svc) mdToPropResponse(ctx context.Context, pf *propfindXML, md *provide
 				case "fileid": // phoenix only
 					if md.Id == nil {
 						propstatNotFound.Prop = append(propstatNotFound.Prop, s.newProp("oc:fileid", ""))
-					} else if spacesEnabled {
+					} else {
 						// If our client uses spaces, we try to use the spaces-encoded file id (storage$base32(spacePath)!inode)
 						fileId, err := spaces.EncodeResourceInfo(md)
 						if err != nil {
@@ -770,10 +711,7 @@ func (s *svc) mdToPropResponse(ctx context.Context, pf *propfindXML, md *provide
 						} else {
 							propstatOK.Prop = append(propstatOK.Prop, s.newProp("oc:fileid", fileId))
 						}
-					} else {
-						propstatOK.Prop = append(propstatOK.Prop, s.newProp("oc:fileid", spaces.ResourceIdToString(md.Id)))
 					}
-
 				case "id": // desktop client only
 					if md.Id != nil {
 						propstatOK.Prop = append(propstatOK.Prop, s.newProp("oc:id", spaces.EncodeResourceID(md.Id)))
@@ -782,11 +720,7 @@ func (s *svc) mdToPropResponse(ctx context.Context, pf *propfindXML, md *provide
 					}
 				case "file-parent":
 					if md.ParentId != nil {
-						if spacesEnabled {
-							propstatOK.Prop = append(propstatOK.Prop, s.newProp("oc:file-parent", spaces.EncodeResourceID(md.ParentId)))
-						} else {
-							propstatOK.Prop = append(propstatOK.Prop, s.newProp("oc:file-parent", spaces.ResourceIdToString(md.ParentId)))
-						}
+						propstatOK.Prop = append(propstatOK.Prop, s.newProp("oc:file-parent", spaces.EncodeResourceID(md.ParentId)))
 					} else {
 						propstatNotFound.Prop = append(propstatNotFound.Prop, s.newProp("oc:file-parent", ""))
 					}
diff --git a/internal/http/services/owncloud/ocdav/publicfile.go b/internal/http/services/owncloud/ocdav/publicfile.go
index c3724220..9864e454 100644
--- a/internal/http/services/owncloud/ocdav/publicfile.go
+++ b/internal/http/services/owncloud/ocdav/publicfile.go
@@ -20,6 +20,7 @@ package ocdav
 
 import (
 	"net/http"
+	"net/url"
 	"path"
 
 	rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
@@ -177,7 +178,18 @@ func (s *svc) handlePropfindOnToken(w http.ResponseWriter, r *http.Request, ns s
 	}
 	infos := s.getPublicFileInfos(onContainer, depth == "0", tokenStatInfo)
 
-	propRes, err := s.multistatusResponse(ctx, &pf, infos, ns, nil, nil)
+	linkToken, _ := ctx.Value(ctxPublicLink).(string)
+	baseURI := ctx.Value(ctxKeyBaseURI).(string)
+
+	_, relativePath := router.ShiftPath(pathRes.Path)
+	_, relativePath = router.ShiftPath(relativePath)
+	href, err := url.JoinPath(baseURI, linkToken, relativePath)
+	if err != nil {
+		sublog.Error().Err(err).Msg("error formatting propfind")
+		w.WriteHeader(http.StatusInternalServerError)
+		return
+	}
+	propRes, err := s.multistatusResponse(ctx, &pf, infos, nil, ns, href, nil, nil)
 	if err != nil {
 		sublog.Error().Err(err).Msg("error formatting propfind")
 		w.WriteHeader(http.StatusInternalServerError)
diff --git a/internal/http/services/owncloud/ocdav/put.go b/internal/http/services/owncloud/ocdav/put.go
index 1cec39ee..68fd5ff7 100644
--- a/internal/http/services/owncloud/ocdav/put.go
+++ b/internal/http/services/owncloud/ocdav/put.go
@@ -128,8 +128,6 @@ func (s *svc) handlePathPut(w http.ResponseWriter, r *http.Request, ns string) {
 }
 
 func (s *svc) handlePut(ctx context.Context, w http.ResponseWriter, r *http.Request, ref *provider.Reference, log zerolog.Logger) {
-	spacesEnabled := s.c.SpacesEnabled
-
 	if !checkPreconditions(w, r, log) {
 		// checkPreconditions handles error returns
 		return
@@ -368,12 +366,9 @@ func (s *svc) handlePut(ctx context.Context, w http.ResponseWriter, r *http.Requ
 
 	w.Header().Add(HeaderContentType, newInfo.MimeType)
 	w.Header().Set(HeaderETag, newInfo.Etag)
-	if spacesEnabled {
-		newInfoID, _ := spaces.EncodeResourceInfo(newInfo)
-		w.Header().Set(HeaderOCFileID, newInfoID)
-	} else {
-		w.Header().Set(HeaderOCFileID, spaces.ResourceIdToString(newInfo.Id))
-	}
+	newInfoID, _ := spaces.EncodeResourceInfo(newInfo)
+	w.Header().Set(HeaderOCFileID, newInfoID)
+
 	w.Header().Set(HeaderOCETag, newInfo.Etag)
 	t := utils.TSToTime(newInfo.Mtime).UTC()
 	lastModifiedString := t.Format(time.RFC1123Z)
diff --git a/internal/http/services/owncloud/ocdav/report.go b/internal/http/services/owncloud/ocdav/report.go
index ea8efec2..001e15b2 100644
--- a/internal/http/services/owncloud/ocdav/report.go
+++ b/internal/http/services/owncloud/ocdav/report.go
@@ -22,6 +22,7 @@ import (
 	"encoding/xml"
 	"io"
 	"net/http"
+	"net/url"
 	"strings"
 
 	rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
@@ -139,7 +140,14 @@ func (s *svc) doFilterFiles(w http.ResponseWriter, r *http.Request, ff *reportFi
 		}
 	}
 
-	responsesXML, err := s.multistatusResponse(ctx, &propfindXML{Prop: ff.Prop}, resourceInfos, namespace, nil, nil)
+	baseURI := ctx.Value(ctxKeyBaseURI).(string)
+	href, err := url.JoinPath(baseURI, r.URL.Path)
+	if err != nil {
+		log.Error().Err(err).Msg("error formatting propfind")
+		w.WriteHeader(http.StatusInternalServerError)
+		return
+	}
+	responsesXML, err := s.multistatusResponse(ctx, &propfindXML{Prop: ff.Prop}, resourceInfos, nil, namespace, href, nil, nil)
 	if err != nil {
 		log.Error().Err(err).Msg("error formatting propfind")
 		w.WriteHeader(http.StatusInternalServerError)
diff --git a/internal/http/services/owncloud/ocdav/trashbin.go b/internal/http/services/owncloud/ocdav/trashbin.go
index aadc5d22..185a8ef7 100644
--- a/internal/http/services/owncloud/ocdav/trashbin.go
+++ b/internal/http/services/owncloud/ocdav/trashbin.go
@@ -83,7 +83,7 @@ func (h *TrashbinHandler) handleTrashbinSpaces(s *svc, w http.ResponseWriter, r
 	if key != "" && r.Method == MethodMove {
 		// find path in url relative to trash base
 		// TODO make request.php optional in destination header
-		dst, err := extractDestination(r)
+		dst, err := extractDestination(r, "")
 		if err != nil {
 			w.WriteHeader(http.StatusBadRequest)
 			return
@@ -201,7 +201,7 @@ func (h *TrashbinHandler) Handler(s *svc) http.Handler {
 			r = r.WithContext(ctx)
 
 			// TODO make request.php optional in destination header
-			dst, err := extractDestination(r)
+			dst, err := extractDestination(r, "")
 			if err != nil {
 				w.WriteHeader(http.StatusBadRequest)
 				return
diff --git a/internal/http/services/owncloud/ocdav/versions.go b/internal/http/services/owncloud/ocdav/versions.go
index 89360748..85d7f215 100644
--- a/internal/http/services/owncloud/ocdav/versions.go
+++ b/internal/http/services/owncloud/ocdav/versions.go
@@ -23,6 +23,7 @@ import (
 	"fmt"
 	"io"
 	"net/http"
+	"net/url"
 	"path"
 	"path/filepath"
 
@@ -165,18 +166,12 @@ func (h *VersionsHandler) doListVersions(w http.ResponseWriter, r *http.Request,
 	// add version dir . entry, derived from file info
 	infos = append(infos, info)
 
-	var spacePath string
-	var ok bool
-	if s.c.SpacesEnabled {
-		storageSpaceID := spaces.ConcatStorageSpaceID(rid.StorageId, rid.SpaceId)
-		_, spacePath, ok = spaces.DecodeStorageSpaceID(storageSpaceID)
-		if !ok {
-			sublog.Error().Msg("error decoding storage space id")
-			w.WriteHeader(http.StatusInternalServerError)
-			return
-		}
-	} else {
-		spacePath = ""
+	storageSpaceID := spaces.ConcatStorageSpaceID(rid.StorageId, rid.SpaceId)
+	_, spacePath, ok := spaces.DecodeStorageSpaceID(storageSpaceID)
+	if !ok {
+		sublog.Error().Msg("error decoding storage space id")
+		w.WriteHeader(http.StatusInternalServerError)
+		return
 	}
 
 	for i := range versions {
@@ -204,7 +199,19 @@ func (h *VersionsHandler) doListVersions(w http.ResponseWriter, r *http.Request,
 		infos = append(infos, vi)
 	}
 
-	propRes, err := s.multistatusResponse(ctx, &pf, infos, "", nil, nil)
+	baseURI := ctx.Value(ctxKeyBaseURI).(string)
+	URLPath := r.URL.Path
+	if ctxPath := ctx.Value(ctxKeyIncomingURL); ctxPath != nil {
+		URLPath = ctxPath.(string)
+	}
+	href, err := url.JoinPath(baseURI, URLPath)
+	if err != nil {
+		sublog.Error().Err(err).Msg("error formatting propfind")
+		w.WriteHeader(http.StatusInternalServerError)
+		return
+	}
+
+	propRes, err := s.multistatusResponse(ctx, &pf, infos, nil, "", href, nil, nil)
 	if err != nil {
 		sublog.Error().Err(err).Msg("error formatting propfind")
 		w.WriteHeader(http.StatusInternalServerError)
diff --git a/pkg/eosclient/eosgrpc/file_ops.go b/pkg/eosclient/eosgrpc/file_ops.go
index 452ed9d5..df0e21d5 100644
--- a/pkg/eosclient/eosgrpc/file_ops.go
+++ b/pkg/eosclient/eosgrpc/file_ops.go
@@ -421,8 +421,6 @@ func (c *Client) list(ctx context.Context, auth eosclient.Authorization, dpath s
 		mylst = append(mylst, myitem)
 	}
 
-	log.Info().Any("resp-list", mylst).Msg("FindRequest raw response")
-
 	for _, fi := range mylst {
 		if fi.SysACL == nil {
 			fi.SysACL = &acl.ACLs{



Test patch:
diff --git a/internal/http/services/owncloud/ocdav/ocdav_test.go b/internal/http/services/owncloud/ocdav/ocdav_test.go
index 90fed1cb..c62b1a27 100644
--- a/internal/http/services/owncloud/ocdav/ocdav_test.go
+++ b/internal/http/services/owncloud/ocdav/ocdav_test.go
@@ -57,7 +57,7 @@ func TestExtractDestination(t *testing.T) {
 	request.Header.Set(HeaderDestination, "https://example.org/remote.php/dav/dst")
 
 	ctx := context.WithValue(context.Background(), ctxKeyBaseURI, "/remote.php/dav")
-	destination, err := extractDestination(request.WithContext(ctx))
+	destination, err := extractDestination(request.WithContext(ctx), "")
 	if err != nil {
 		t.Errorf("Expected err to be nil got %s", err)
 	}
@@ -70,7 +70,7 @@ func TestExtractDestination(t *testing.T) {
 func TestExtractDestinationWithoutHeader(t *testing.T) {
 	request := httptest.NewRequest(http.MethodGet, "https://example.org/remote.php/dav/src", nil)
 
-	_, err := extractDestination(request)
+	_, err := extractDestination(request, "")
 	if err == nil {
 		t.Errorf("Expected err to be nil got %s", err)
 	}
@@ -83,7 +83,7 @@ func TestExtractDestinationWithoutHeader(t *testing.T) {
 func TestExtractDestinationWithInvalidDestination(t *testing.T) {
 	request := httptest.NewRequest(http.MethodGet, "https://example.org/remote.php/dav/src", nil)
 	request.Header.Set(HeaderDestination, "://example.org/remote.php/dav/dst")
-	_, err := extractDestination(request)
+	_, err := extractDestination(request, "")
 	if err == nil {
 		t.Errorf("Expected err to be nil got %s", err)
 	}