diff --git a/changelog/unreleased/trash-listing-bykey-fixes.md b/changelog/unreleased/trash-listing-bykey-fixes.md
new file mode 100644
index 0000000000..dd649013af
--- /dev/null
+++ b/changelog/unreleased/trash-listing-bykey-fixes.md
@@ -0,0 +1,9 @@
+Bugfix: Allow listing directory trash items by key
+
+The storageprovider now passes on the key without inventing a `/` as the relative path when it was not present at the end of the key. This allows differentiating requests that want to get the trash item of a folder itself (where the relative path is empty) or listing the children of a folder in the trash (where the relative path at least starts with a `/`).
+
+We also fixed the `/dav/spaces` endpoint to not invent a `/` at the end of URLs to allow clients to actually make these different requests.
+
+As a byproduct we now return the size of trashed items.
+
+https://github.com/cs3org/reva/pull/4818
diff --git a/internal/grpc/services/storageprovider/storageprovider.go b/internal/grpc/services/storageprovider/storageprovider.go
index 763148e8e7..3637980c0a 100644
--- a/internal/grpc/services/storageprovider/storageprovider.go
+++ b/internal/grpc/services/storageprovider/storageprovider.go
@@ -27,6 +27,7 @@ import (
"path"
"sort"
"strconv"
+ "strings"
"time"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
@@ -880,8 +881,11 @@ func (s *Service) ListRecycleStream(req *provider.ListRecycleStreamRequest, ss p
ctx := ss.Context()
log := appctx.GetLogger(ctx)
- key, itemPath := router.ShiftPath(req.Key)
- items, err := s.Storage.ListRecycle(ctx, req.Ref, key, itemPath)
+ // if no slash is present in the key, do not pass a relative path to the storage
+ // when a path is passed to the storage, it will list the contents of the directory
+ key, relativePath := splitKeyAndPath(req.GetKey())
+ items, err := s.Storage.ListRecycle(ctx, req.Ref, key, relativePath)
+
if err != nil {
var st *rpc.Status
switch err.(type) {
@@ -924,8 +928,10 @@ func (s *Service) ListRecycleStream(req *provider.ListRecycleStreamRequest, ss p
}
func (s *Service) ListRecycle(ctx context.Context, req *provider.ListRecycleRequest) (*provider.ListRecycleResponse, error) {
- key, itemPath := router.ShiftPath(req.Key)
- items, err := s.Storage.ListRecycle(ctx, req.Ref, key, itemPath)
+ // if no slash is present in the key, do not pass a relative path to the storage
+ // when a path is passed to the storage, it will list the contents of the directory
+ key, relativePath := splitKeyAndPath(req.GetKey())
+ items, err := s.Storage.ListRecycle(ctx, req.Ref, key, relativePath)
if err != nil {
var st *rpc.Status
switch err.(type) {
@@ -962,8 +968,8 @@ func (s *Service) RestoreRecycleItem(ctx context.Context, req *provider.RestoreR
ctx = ctxpkg.ContextSetLockID(ctx, req.LockId)
// TODO(labkode): CRITICAL: fill recycle info with storage provider.
- key, itemPath := router.ShiftPath(req.Key)
- err := s.Storage.RestoreRecycleItem(ctx, req.Ref, key, itemPath, req.RestoreRef)
+ key, relativePath := splitKeyAndPath(req.GetKey())
+ err := s.Storage.RestoreRecycleItem(ctx, req.Ref, key, relativePath, req.RestoreRef)
res := &provider.RestoreRecycleItemResponse{
Status: status.NewStatusFromErrType(ctx, "restore recycle item", err),
@@ -980,9 +986,9 @@ func (s *Service) PurgeRecycle(ctx context.Context, req *provider.PurgeRecycleRe
}
// if a key was sent as opaque id purge only that item
- key, itemPath := router.ShiftPath(req.Key)
+ key, relativePath := splitKeyAndPath(req.GetKey())
if key != "" {
- if err := s.Storage.PurgeRecycleItem(ctx, req.Ref, key, itemPath); err != nil {
+ if err := s.Storage.PurgeRecycleItem(ctx, req.Ref, key, relativePath); err != nil {
st := status.NewStatusFromErrType(ctx, "error purging recycle item", err)
appctx.GetLogger(ctx).
Error().
@@ -1313,3 +1319,12 @@ func canLockPublicShare(ctx context.Context) bool {
psr := utils.ReadPlainFromOpaque(u.Opaque, "public-share-role")
return psr == "" || psr == conversions.RoleEditor
}
+
+// splitKeyAndPath splits a key into a root and a relative path
+func splitKeyAndPath(key string) (string, string) {
+ root, relativePath := router.ShiftPath(key)
+ if relativePath == "/" && !strings.HasSuffix(key, "/") {
+ relativePath = ""
+ }
+ return root, relativePath
+}
diff --git a/internal/http/services/owncloud/ocdav/dav.go b/internal/http/services/owncloud/ocdav/dav.go
index ba82e15439..7aff215a34 100644
--- a/internal/http/services/owncloud/ocdav/dav.go
+++ b/internal/http/services/owncloud/ocdav/dav.go
@@ -291,7 +291,7 @@ func (h *DavHandler) Handler(s *svc) http.Handler {
http.Redirect(w, r, rUrl, http.StatusTemporaryRedirect)
return
}
- log.Debug().Str("token", token).Interface("status", res.Status).Msg("resource id not found")
+ log.Debug().Str("token", token).Interface("status", psRes.Status).Msg("resource id not found")
w.WriteHeader(http.StatusNotFound)
return
}
diff --git a/internal/http/services/owncloud/ocdav/spaces.go b/internal/http/services/owncloud/ocdav/spaces.go
index 37797d60ad..2439a98251 100644
--- a/internal/http/services/owncloud/ocdav/spaces.go
+++ b/internal/http/services/owncloud/ocdav/spaces.go
@@ -21,6 +21,7 @@ package ocdav
import (
"net/http"
"path"
+ "strings"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/config"
@@ -132,8 +133,7 @@ func (h *SpacesHandler) handleSpacesTrashbin(w http.ResponseWriter, r *http.Requ
ctx := r.Context()
log := appctx.GetLogger(ctx)
- var spaceID string
- spaceID, r.URL.Path = router.ShiftPath(r.URL.Path)
+ spaceID, key := splitSpaceAndKey(r.URL.Path)
if spaceID == "" {
// listing is disabled, no auth will change that
w.WriteHeader(http.StatusMethodNotAllowed)
@@ -146,12 +146,9 @@ func (h *SpacesHandler) handleSpacesTrashbin(w http.ResponseWriter, r *http.Requ
return
}
- var key string
- key, r.URL.Path = router.ShiftPath(r.URL.Path)
-
switch r.Method {
case MethodPropfind:
- trashbinHandler.listTrashbin(w, r, s, &ref, path.Join(_trashbinPath, spaceID), key, r.URL.Path)
+ trashbinHandler.listTrashbin(w, r, s, &ref, path.Join(_trashbinPath, spaceID), key)
case MethodMove:
if key == "" {
http.Error(w, "501 Not implemented", http.StatusNotImplemented)
@@ -167,15 +164,25 @@ func (h *SpacesHandler) handleSpacesTrashbin(w http.ResponseWriter, r *http.Requ
w.WriteHeader(http.StatusBadRequest)
return
}
- log.Debug().Str("key", key).Str("path", r.URL.Path).Str("dst", dst).Msg("spaces restore")
+ log.Debug().Str("key", key).Str("dst", dst).Msg("spaces restore")
dstRef := proto.Clone(&ref).(*provider.Reference)
dstRef.Path = utils.MakeRelativePath(dst)
- trashbinHandler.restore(w, r, s, &ref, dstRef, key, r.URL.Path)
+ trashbinHandler.restore(w, r, s, &ref, dstRef, key)
case http.MethodDelete:
- trashbinHandler.delete(w, r, s, &ref, key, r.URL.Path)
+ trashbinHandler.delete(w, r, s, &ref, key)
default:
http.Error(w, "501 Not implemented", http.StatusNotImplemented)
}
}
+
+func splitSpaceAndKey(p string) (space, key string) {
+ p = strings.TrimPrefix(p, "/")
+ parts := strings.SplitN(p, "/", 2)
+ space = parts[0]
+ if len(parts) > 1 {
+ key = parts[1]
+ }
+ return
+}
diff --git a/internal/http/services/owncloud/ocdav/trashbin.go b/internal/http/services/owncloud/ocdav/trashbin.go
index ef742c54df..5e3015e3ef 100644
--- a/internal/http/services/owncloud/ocdav/trashbin.go
+++ b/internal/http/services/owncloud/ocdav/trashbin.go
@@ -43,7 +43,6 @@ import (
"github.com/cs3org/reva/v2/pkg/appctx"
ctxpkg "github.com/cs3org/reva/v2/pkg/ctx"
rstatus "github.com/cs3org/reva/v2/pkg/rgrpc/status"
- "github.com/cs3org/reva/v2/pkg/rhttp/router"
"github.com/cs3org/reva/v2/pkg/utils"
semconv "go.opentelemetry.io/otel/semconv/v1.20.0"
)
@@ -74,7 +73,7 @@ func (h *TrashbinHandler) Handler(s *svc) http.Handler {
}
var username string
- username, r.URL.Path = router.ShiftPath(r.URL.Path)
+ username, r.URL.Path = splitSpaceAndKey(r.URL.Path)
if username == "" {
// listing is disabled, no auth will change that
w.WriteHeader(http.StatusMethodNotAllowed)
@@ -131,13 +130,12 @@ func (h *TrashbinHandler) Handler(s *svc) http.Handler {
}
ref := spacelookup.MakeRelativeReference(space, ".", false)
- // key will be a base64 encoded cs3 path, it uniquely identifies a trash item & storage
- var key string
- key, r.URL.Path = router.ShiftPath(r.URL.Path)
+ // key will be a base64 encoded cs3 path, it uniquely identifies a trash item with an opaque id and an optional path
+ key := r.URL.Path
switch r.Method {
case MethodPropfind:
- h.listTrashbin(w, r, s, ref, user.Username, key, r.URL.Path)
+ h.listTrashbin(w, r, s, ref, user.Username, key)
case MethodMove:
if key == "" {
http.Error(w, "501 Not implemented", http.StatusNotImplemented)
@@ -172,50 +170,55 @@ func (h *TrashbinHandler) Handler(s *svc) http.Handler {
dstRef := spacelookup.MakeRelativeReference(space, p, false)
log.Debug().Str("key", key).Str("dst", dst).Msg("restore")
- h.restore(w, r, s, ref, dstRef, key, r.URL.Path)
+ h.restore(w, r, s, ref, dstRef, key)
case http.MethodDelete:
- h.delete(w, r, s, ref, key, r.URL.Path)
+ h.delete(w, r, s, ref, key)
default:
http.Error(w, "501 Not implemented", http.StatusNotImplemented)
}
})
}
-func (h *TrashbinHandler) listTrashbin(w http.ResponseWriter, r *http.Request, s *svc, ref *provider.Reference, refBase, key, itemPath string) {
+func (h *TrashbinHandler) getDepth(r *http.Request) (net.Depth, error) {
+ dh := r.Header.Get(net.HeaderDepth)
+ depth, err := net.ParseDepth(dh)
+ if err != nil || depth == net.DepthInfinity && !h.allowPropfindDepthInfinitiy {
+ return "", errors.ErrInvalidDepth
+ }
+ return depth, nil
+}
+
+func (h *TrashbinHandler) listTrashbin(w http.ResponseWriter, r *http.Request, s *svc, ref *provider.Reference, refBase, key string) {
ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), "list_trashbin")
defer span.End()
sublog := appctx.GetLogger(ctx).With().Logger()
- dh := r.Header.Get(net.HeaderDepth)
- depth, err := net.ParseDepth(dh)
+ depth, err := h.getDepth(r)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "Invalid Depth header value")
span.SetAttributes(semconv.HTTPStatusCodeKey.Int(http.StatusBadRequest))
- sublog.Debug().Str("depth", dh).Msg(err.Error())
+ sublog.Debug().Str("depth", r.Header.Get(net.HeaderDepth)).Msg(err.Error())
w.WriteHeader(http.StatusBadRequest)
- m := fmt.Sprintf("Invalid Depth header value: %v", dh)
+ m := fmt.Sprintf("Invalid Depth header value: %v", r.Header.Get(net.HeaderDepth))
b, err := errors.Marshal(http.StatusBadRequest, m, "", "")
errors.HandleWebdavError(&sublog, w, b, err)
return
}
- if depth == net.DepthInfinity && !h.allowPropfindDepthInfinitiy {
- span.RecordError(errors.ErrInvalidDepth)
- span.SetStatus(codes.Error, "DEPTH: infinity is not supported")
- span.SetAttributes(semconv.HTTPStatusCodeKey.Int(http.StatusBadRequest))
- sublog.Debug().Str("depth", dh).Msg(errors.ErrInvalidDepth.Error())
- w.WriteHeader(http.StatusBadRequest)
- m := fmt.Sprintf("Invalid Depth header value: %v", dh)
- b, err := errors.Marshal(http.StatusBadRequest, m, "", "")
- errors.HandleWebdavError(&sublog, w, b, err)
+ pf, status, err := propfind.ReadPropfind(r.Body)
+ if err != nil {
+ sublog.Debug().Err(err).Msg("error reading propfind request")
+ w.WriteHeader(status)
return
}
- if depth == net.DepthZero {
- rootHref := path.Join(refBase, key, itemPath)
- propRes, err := h.formatTrashPropfind(ctx, s, ref.ResourceId.SpaceId, refBase, rootHref, nil, nil)
+ if key == "" && depth == net.DepthZero {
+ // we are listing the trash root, but without children
+ // so we just fake a root element without actually querying the gateway
+ rootHref := path.Join(refBase, key)
+ propRes, err := h.formatTrashPropfind(ctx, s, ref.ResourceId.SpaceId, refBase, rootHref, &pf, nil, true)
if err != nil {
sublog.Error().Err(err).Msg("error formatting propfind")
w.WriteHeader(http.StatusInternalServerError)
@@ -232,11 +235,9 @@ func (h *TrashbinHandler) listTrashbin(w http.ResponseWriter, r *http.Request, s
return
}
- pf, status, err := propfind.ReadPropfind(r.Body)
- if err != nil {
- sublog.Debug().Err(err).Msg("error reading propfind request")
- w.WriteHeader(status)
- return
+ if depth == net.DepthOne && key != "" && !strings.HasSuffix(key, "/") {
+ // when a key is provided and the depth is 1 we need to append a / to the key to list the children
+ key += "/"
}
client, err := s.gatewaySelector.Next()
@@ -246,7 +247,7 @@ func (h *TrashbinHandler) listTrashbin(w http.ResponseWriter, r *http.Request, s
return
}
// ask gateway for recycle items
- getRecycleRes, err := client.ListRecycle(ctx, &provider.ListRecycleRequest{Ref: ref, Key: path.Join(key, itemPath)})
+ getRecycleRes, err := client.ListRecycle(ctx, &provider.ListRecycleRequest{Ref: ref, Key: key})
if err != nil {
sublog.Error().Err(err).Msg("error calling ListRecycle")
w.WriteHeader(http.StatusInternalServerError)
@@ -304,8 +305,8 @@ func (h *TrashbinHandler) listTrashbin(w http.ResponseWriter, r *http.Request, s
}
}
- rootHref := path.Join(refBase, key, itemPath)
- propRes, err := h.formatTrashPropfind(ctx, s, ref.ResourceId.SpaceId, refBase, rootHref, &pf, items)
+ rootHref := path.Join(refBase, key)
+ propRes, err := h.formatTrashPropfind(ctx, s, ref.ResourceId.SpaceId, refBase, rootHref, &pf, items, depth != net.DepthZero)
if err != nil {
sublog.Error().Err(err).Msg("error formatting propfind")
w.WriteHeader(http.StatusInternalServerError)
@@ -321,29 +322,30 @@ func (h *TrashbinHandler) listTrashbin(w http.ResponseWriter, r *http.Request, s
}
}
-func (h *TrashbinHandler) formatTrashPropfind(ctx context.Context, s *svc, spaceID, refBase, rootHref string, pf *propfind.XML, items []*provider.RecycleItem) ([]byte, error) {
+func (h *TrashbinHandler) formatTrashPropfind(ctx context.Context, s *svc, spaceID, refBase, rootHref string, pf *propfind.XML, items []*provider.RecycleItem, fakeRoot bool) ([]byte, error) {
responses := make([]*propfind.ResponseXML, 0, len(items)+1)
- // add trashbin dir . entry
- responses = append(responses, &propfind.ResponseXML{
- Href: net.EncodePath(path.Join(ctx.Value(net.CtxKeyBaseURI).(string), rootHref) + "/"), // url encode response.Href TODO
- Propstat: []propfind.PropstatXML{
- {
- Status: "HTTP/1.1 200 OK",
- Prop: []prop.PropertyXML{
- prop.Raw("d:resourcetype", ""),
+ if fakeRoot {
+ responses = append(responses, &propfind.ResponseXML{
+ Href: net.EncodePath(path.Join(ctx.Value(net.CtxKeyBaseURI).(string), rootHref) + "/"), // url encode response.Href TODO
+ Propstat: []propfind.PropstatXML{
+ {
+ Status: "HTTP/1.1 200 OK",
+ Prop: []prop.PropertyXML{
+ prop.Raw("d:resourcetype", ""),
+ },
},
- },
- {
- Status: "HTTP/1.1 404 Not Found",
- Prop: []prop.PropertyXML{
- prop.NotFound("oc:trashbin-original-filename"),
- prop.NotFound("oc:trashbin-original-location"),
- prop.NotFound("oc:trashbin-delete-datetime"),
- prop.NotFound("d:getcontentlength"),
+ {
+ Status: "HTTP/1.1 404 Not Found",
+ Prop: []prop.PropertyXML{
+ prop.NotFound("oc:trashbin-original-filename"),
+ prop.NotFound("oc:trashbin-original-location"),
+ prop.NotFound("oc:trashbin-delete-datetime"),
+ prop.NotFound("d:getcontentlength"),
+ },
},
},
- },
- })
+ })
+ }
for i := range items {
res, err := h.itemToPropResponse(ctx, s, spaceID, refBase, pf, items[i])
@@ -401,7 +403,7 @@ func (h *TrashbinHandler) itemToPropResponse(ctx context.Context, s *svc, spaceI
propstatOK.Prop = append(propstatOK.Prop, prop.Escaped("oc:trashbin-delete-datetime", dTime))
if item.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER {
propstatOK.Prop = append(propstatOK.Prop, prop.Raw("d:resourcetype", ""))
- // TODO(jfd): decide if we can and want to list oc:size for folders
+ propstatOK.Prop = append(propstatOK.Prop, prop.Raw("oc:size", size))
} else {
propstatOK.Prop = append(propstatOK.Prop,
prop.Escaped("d:resourcetype", ""),
@@ -426,7 +428,7 @@ func (h *TrashbinHandler) itemToPropResponse(ctx context.Context, s *svc, spaceI
switch pf.Prop[i].Local {
case "oc:size":
if item.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER {
- propstatOK.Prop = append(propstatOK.Prop, prop.Escaped("d:getcontentlength", size))
+ propstatOK.Prop = append(propstatOK.Prop, prop.Escaped("oc:size", size))
} else {
propstatNotFound.Prop = append(propstatNotFound.Prop, prop.NotFound("oc:size"))
}
@@ -480,7 +482,7 @@ func (h *TrashbinHandler) itemToPropResponse(ctx context.Context, s *svc, spaceI
return &response, nil
}
-func (h *TrashbinHandler) restore(w http.ResponseWriter, r *http.Request, s *svc, ref, dst *provider.Reference, key, itemPath string) {
+func (h *TrashbinHandler) restore(w http.ResponseWriter, r *http.Request, s *svc, ref, dst *provider.Reference, key string) {
ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), "restore")
defer span.End()
@@ -566,7 +568,7 @@ func (h *TrashbinHandler) restore(w http.ResponseWriter, r *http.Request, s *svc
req := &provider.RestoreRecycleItemRequest{
Ref: ref,
- Key: path.Join(key, itemPath),
+ Key: key,
RestoreRef: dst,
}
@@ -608,16 +610,15 @@ func (h *TrashbinHandler) restore(w http.ResponseWriter, r *http.Request, s *svc
}
// delete has only a key
-func (h *TrashbinHandler) delete(w http.ResponseWriter, r *http.Request, s *svc, ref *provider.Reference, key, itemPath string) {
+func (h *TrashbinHandler) delete(w http.ResponseWriter, r *http.Request, s *svc, ref *provider.Reference, key string) {
ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), "erase")
defer span.End()
- sublog := appctx.GetLogger(ctx).With().Interface("reference", ref).Str("key", key).Str("item_path", itemPath).Logger()
+ sublog := appctx.GetLogger(ctx).With().Interface("reference", ref).Str("key", key).Logger()
- trashPath := path.Join(key, itemPath)
req := &provider.PurgeRecycleRequest{
Ref: ref,
- Key: trashPath,
+ Key: key,
}
client, err := s.gatewaySelector.Next()
@@ -638,7 +639,7 @@ func (h *TrashbinHandler) delete(w http.ResponseWriter, r *http.Request, s *svc,
case rpc.Code_CODE_NOT_FOUND:
sublog.Debug().Interface("status", res.Status).Msg("resource not found")
w.WriteHeader(http.StatusConflict)
- m := fmt.Sprintf("path %s not found", trashPath)
+ m := fmt.Sprintf("key %s not found", key)
b, err := errors.Marshal(http.StatusConflict, m, "", "")
errors.HandleWebdavError(&sublog, w, b, err)
case rpc.Code_CODE_PERMISSION_DENIED:
diff --git a/pkg/storage/fs/nextcloud/nextcloud_server_mock.go b/pkg/storage/fs/nextcloud/nextcloud_server_mock.go
index 4998c5a5b7..97507df71b 100644
--- a/pkg/storage/fs/nextcloud/nextcloud_server_mock.go
+++ b/pkg/storage/fs/nextcloud/nextcloud_server_mock.go
@@ -129,8 +129,8 @@ var responses = map[string]Response{
`POST /apps/sciencemesh/~f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c/api/storage/ListGrants {"path":"/subdir"} GRANT-UPDATED`: {200, `[{"grantee":{"type":1,"Id":{"UserId":{"idp":"some-idp","opaque_id":"some-opaque-id","type":1}}},"permissions":{"add_grant":true,"create_container":true,"delete":true,"get_path":true,"get_quota":true,"initiate_file_download":true,"initiate_file_upload":true,"list_grants":true,"list_container":true,"list_file_versions":true,"list_recycle":true,"move":true,"remove_grant":true,"purge_recycle":true,"restore_file_version":true,"restore_recycle_item":true,"stat":true,"update_grant":true,"deny_grant":true}}]`, serverStateEmpty},
`POST /apps/sciencemesh/~f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c/api/storage/ListGrants {"path":"/subdir"} GRANT-REMOVED`: {200, `[]`, serverStateEmpty},
- `POST /apps/sciencemesh/~f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c/api/storage/ListRecycle {"key":"","path":"/"} EMPTY`: {200, `[]`, serverStateEmpty},
- `POST /apps/sciencemesh/~f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c/api/storage/ListRecycle {"key":"","path":"/"} RECYCLE`: {200, `[{"opaque":{},"key":"some-deleted-version","ref":{"resource_id":{},"path":"/subdir"},"size":12345,"deletion_time":{"seconds":1234567890}}]`, serverStateRecycle},
+ `POST /apps/sciencemesh/~f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c/api/storage/ListRecycle {"key":"","path":""} EMPTY`: {200, `[]`, serverStateEmpty},
+ `POST /apps/sciencemesh/~f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c/api/storage/ListRecycle {"key":"","path":""} RECYCLE`: {200, `[{"opaque":{},"key":"some-deleted-version","ref":{"resource_id":{},"path":"/subdir"},"size":12345,"deletion_time":{"seconds":1234567890}}]`, serverStateRecycle},
`POST /apps/sciencemesh/~f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c/api/storage/ListRevisions {"path":"/versionedFile"} EMPTY`: {200, `[{"opaque":{"map":{"some":{"value":"ZGF0YQ=="}}},"key":"version-12","size":1,"mtime":1234567890,"etag":"deadb00f"}]`, serverStateEmpty},
`POST /apps/sciencemesh/~f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c/api/storage/ListRevisions {"path":"/versionedFile"} FILE-RESTORED`: {200, `[{"opaque":{"map":{"some":{"value":"ZGF0YQ=="}}},"key":"version-12","size":1,"mtime":1234567890,"etag":"deadb00f"},{"opaque":{"map":{"different":{"value":"c3R1ZmY="}}},"key":"asdf","size":2,"mtime":1234567890,"etag":"deadbeef"}]`, serverStateFileRestored},
@@ -139,9 +139,9 @@ var responses = map[string]Response{
`POST /apps/sciencemesh/~f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c/api/storage/RemoveGrant {"path":"/subdir"} GRANT-ADDED`: {200, ``, serverStateGrantRemoved},
- `POST /apps/sciencemesh/~f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c/api/storage/RestoreRecycleItem null`: {200, ``, serverStateSubdir},
- `POST /apps/sciencemesh/~f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c/api/storage/RestoreRecycleItem {"key":"some-deleted-version","path":"/","restoreRef":{"path":"/subdirRestored"}}`: {200, ``, serverStateFileRestored},
- `POST /apps/sciencemesh/~f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c/api/storage/RestoreRecycleItem {"key":"some-deleted-version","path":"/","restoreRef":null}`: {200, ``, serverStateFileRestored},
+ `POST /apps/sciencemesh/~f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c/api/storage/RestoreRecycleItem null`: {200, ``, serverStateSubdir},
+ `POST /apps/sciencemesh/~f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c/api/storage/RestoreRecycleItem {"key":"some-deleted-version","path":"","restoreRef":{"path":"/subdirRestored"}}`: {200, ``, serverStateFileRestored},
+ `POST /apps/sciencemesh/~f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c/api/storage/RestoreRecycleItem {"key":"some-deleted-version","path":"","restoreRef":null}`: {200, ``, serverStateFileRestored},
`POST /apps/sciencemesh/~f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c/api/storage/RestoreRevision {"ref":{"path":"/versionedFile"},"key":"version-12"}`: {200, ``, serverStateFileRestored},
diff --git a/pkg/storage/utils/decomposedfs/recycle.go b/pkg/storage/utils/decomposedfs/recycle.go
index aa0ead067b..fdc99b806b 100644
--- a/pkg/storage/utils/decomposedfs/recycle.go
+++ b/pkg/storage/utils/decomposedfs/recycle.go
@@ -23,7 +23,6 @@ import (
iofs "io/fs"
"os"
"path/filepath"
- "strconv"
"strings"
"time"
@@ -55,6 +54,9 @@ func (fs *Decomposedfs) ListRecycle(ctx context.Context, ref *provider.Reference
if ref == nil || ref.ResourceId == nil || ref.ResourceId.OpaqueId == "" {
return nil, errtypes.BadRequest("spaceid required")
}
+ if key == "" && relativePath != "" {
+ return nil, errtypes.BadRequest("key is required when navigating with a path")
+ }
spaceID := ref.ResourceId.OpaqueId
sublog := appctx.GetLogger(ctx).With().Str("spaceid", spaceID).Str("key", key).Str("relative_path", relativePath).Logger()
@@ -75,7 +77,7 @@ func (fs *Decomposedfs) ListRecycle(ctx context.Context, ref *provider.Reference
return nil, errtypes.NotFound(key)
}
- if key == "" && relativePath == "/" {
+ if key == "" && relativePath == "" {
return fs.listTrashRoot(ctx, spaceID)
}
@@ -113,16 +115,25 @@ func (fs *Decomposedfs) ListRecycle(ctx context.Context, ref *provider.Reference
sublog.Error().Err(err).Msg("could not parse time format, ignoring")
}
- nodeType := fs.lu.TypeFromPath(ctx, originalPath)
- if nodeType != provider.ResourceType_RESOURCE_TYPE_CONTAINER {
+ var size int64
+ if relativePath == "" {
// this is the case when we want to directly list a file in the trashbin
- blobsize, err := strconv.ParseInt(string(attrs[prefixes.BlobsizeAttr]), 10, 64)
- if err != nil {
- return items, err
+ nodeType := fs.lu.TypeFromPath(ctx, originalPath)
+ switch nodeType {
+ case provider.ResourceType_RESOURCE_TYPE_FILE:
+ size, err = fs.lu.ReadBlobSizeAttr(ctx, originalPath)
+ if err != nil {
+ return items, err
+ }
+ case provider.ResourceType_RESOURCE_TYPE_CONTAINER:
+ size, err = fs.lu.MetadataBackend().GetInt64(ctx, originalPath, prefixes.TreesizeAttr)
+ if err != nil {
+ return items, err
+ }
}
item := &provider.RecycleItem{
- Type: nodeType,
- Size: uint64(blobsize),
+ Type: fs.lu.TypeFromPath(ctx, originalPath),
+ Size: uint64(size),
Key: filepath.Join(key, relativePath),
DeletionTime: deletionTime,
Ref: &provider.Reference{
@@ -134,9 +145,6 @@ func (fs *Decomposedfs) ListRecycle(ctx context.Context, ref *provider.Reference
}
// we have to read the names and stat the path to follow the symlinks
- if err != nil {
- return nil, err
- }
childrenPath := filepath.Join(originalPath, relativePath)
childrenDir, err := os.Open(childrenPath)
if err != nil {
@@ -154,9 +162,10 @@ func (fs *Decomposedfs) ListRecycle(ctx context.Context, ref *provider.Reference
continue
}
- size := int64(0)
+ // reset size
+ size = 0
- nodeType = fs.lu.TypeFromPath(ctx, resolvedChildPath)
+ nodeType := fs.lu.TypeFromPath(ctx, resolvedChildPath)
switch nodeType {
case provider.ResourceType_RESOURCE_TYPE_FILE:
size, err = fs.lu.ReadBlobSizeAttr(ctx, resolvedChildPath)
@@ -165,12 +174,7 @@ func (fs *Decomposedfs) ListRecycle(ctx context.Context, ref *provider.Reference
continue
}
case provider.ResourceType_RESOURCE_TYPE_CONTAINER:
- attr, err := fs.lu.MetadataBackend().Get(ctx, resolvedChildPath, prefixes.TreesizeAttr)
- if err != nil {
- sublog.Error().Err(err).Str("name", name).Msg("invalid tree size, skipping")
- continue
- }
- size, err = strconv.ParseInt(string(attr), 10, 64)
+ size, err = fs.lu.MetadataBackend().GetInt64(ctx, resolvedChildPath, prefixes.TreesizeAttr)
if err != nil {
sublog.Error().Err(err).Str("name", name).Msg("invalid tree size, skipping")
continue
@@ -217,7 +221,7 @@ func readTrashLink(path string) (string, string, string, error) {
func (fs *Decomposedfs) listTrashRoot(ctx context.Context, spaceID string) ([]*provider.RecycleItem, error) {
log := appctx.GetLogger(ctx)
trashRoot := fs.getRecycleRoot(spaceID)
-
+ items := []*provider.RecycleItem{}
subTrees, err := filepath.Glob(trashRoot + "/*")
if err != nil {
return nil, err
@@ -256,6 +260,7 @@ func (fs *Decomposedfs) listTrashRoot(ctx context.Context, spaceID string) ([]*p
}
for _, itemPath := range matches {
+ // TODO can we encode this in the path instead of reading the link?
nodePath, nodeID, timeSuffix, err := readTrashLink(itemPath)
if err != nil {
log.Error().Err(err).Str("trashRoot", trashRoot).Str("item", itemPath).Msg("error reading trash link, skipping")
@@ -300,6 +305,7 @@ func (fs *Decomposedfs) listTrashRoot(ctx context.Context, spaceID string) ([]*p
} else {
log.Error().Str("trashRoot", trashRoot).Str("item", itemPath).Str("spaceid", spaceID).Str("nodeid", nodeID).Str("dtime", timeSuffix).Msg("could not read origin path")
}
+
select {
case results <- item:
case <-ctx.Done():
@@ -318,7 +324,6 @@ func (fs *Decomposedfs) listTrashRoot(ctx context.Context, spaceID string) ([]*p
}()
// Collect results
- items := []*provider.RecycleItem{}
for ri := range results {
items = append(items, ri)
}
@@ -414,7 +419,7 @@ func (fs *Decomposedfs) EmptyRecycle(ctx context.Context, ref *provider.Referenc
return errtypes.BadRequest("spaceid must be set")
}
- items, err := fs.ListRecycle(ctx, ref, "", "/")
+ items, err := fs.ListRecycle(ctx, ref, "", "")
if err != nil {
return err
}
diff --git a/pkg/storage/utils/decomposedfs/recycle_test.go b/pkg/storage/utils/decomposedfs/recycle_test.go
index 5cfe9ec5e7..b883d39955 100644
--- a/pkg/storage/utils/decomposedfs/recycle_test.go
+++ b/pkg/storage/utils/decomposedfs/recycle_test.go
@@ -73,7 +73,7 @@ var _ = Describe("Recycle", func() {
})
It("they are stored in the same trashbin", func() {
- items, err := env.Fs.ListRecycle(env.Ctx, &provider.Reference{ResourceId: env.SpaceRootRes}, "", "/")
+ items, err := env.Fs.ListRecycle(env.Ctx, &provider.Reference{ResourceId: env.SpaceRootRes}, "", "")
Expect(err).ToNot(HaveOccurred())
Expect(len(items)).To(Equal(2))
})
@@ -88,17 +88,17 @@ var _ = Describe("Recycle", func() {
// mock call to blobstore
env.Blobstore.On("Delete", mock.Anything).Return(nil).Times(2)
- items, err := env.Fs.ListRecycle(env.Ctx, &provider.Reference{ResourceId: env.SpaceRootRes}, "", "/")
+ items, err := env.Fs.ListRecycle(env.Ctx, &provider.Reference{ResourceId: env.SpaceRootRes}, "", "")
Expect(err).ToNot(HaveOccurred())
Expect(len(items)).To(Equal(2))
- err = env.Fs.PurgeRecycleItem(env.Ctx, &provider.Reference{ResourceId: env.SpaceRootRes}, items[0].Key, "/")
+ err = env.Fs.PurgeRecycleItem(env.Ctx, &provider.Reference{ResourceId: env.SpaceRootRes}, items[0].Key, "")
Expect(err).ToNot(HaveOccurred())
- err = env.Fs.PurgeRecycleItem(env.Ctx, &provider.Reference{ResourceId: env.SpaceRootRes}, items[1].Key, "/")
+ err = env.Fs.PurgeRecycleItem(env.Ctx, &provider.Reference{ResourceId: env.SpaceRootRes}, items[1].Key, "")
Expect(err).ToNot(HaveOccurred())
- items, err = env.Fs.ListRecycle(env.Ctx, &provider.Reference{ResourceId: env.SpaceRootRes}, "", "/")
+ items, err = env.Fs.ListRecycle(env.Ctx, &provider.Reference{ResourceId: env.SpaceRootRes}, "", "")
Expect(err).ToNot(HaveOccurred())
Expect(len(items)).To(Equal(0))
})
@@ -106,14 +106,14 @@ var _ = Describe("Recycle", func() {
It("they can be restored", func() {
env.Blobstore.On("Delete", mock.Anything).Return(nil).Times(2)
- items, err := env.Fs.ListRecycle(env.Ctx, &provider.Reference{ResourceId: env.SpaceRootRes}, "", "/")
+ items, err := env.Fs.ListRecycle(env.Ctx, &provider.Reference{ResourceId: env.SpaceRootRes}, "", "")
Expect(err).ToNot(HaveOccurred())
Expect(len(items)).To(Equal(2))
- err = env.Fs.RestoreRecycleItem(env.Ctx, &provider.Reference{ResourceId: env.SpaceRootRes}, items[0].Key, "/", nil)
+ err = env.Fs.RestoreRecycleItem(env.Ctx, &provider.Reference{ResourceId: env.SpaceRootRes}, items[0].Key, "", nil)
Expect(err).ToNot(HaveOccurred())
- items, err = env.Fs.ListRecycle(env.Ctx, &provider.Reference{ResourceId: env.SpaceRootRes}, "", "/")
+ items, err = env.Fs.ListRecycle(env.Ctx, &provider.Reference{ResourceId: env.SpaceRootRes}, "", "")
Expect(err).ToNot(HaveOccurred())
Expect(len(items)).To(Equal(1))
})
@@ -167,10 +167,10 @@ var _ = Describe("Recycle", func() {
})
It("they are stored in the same trashbin (for both users)", func() {
- itemsA, err := env.Fs.ListRecycle(env.Ctx, &provider.Reference{ResourceId: env.SpaceRootRes}, "", "/")
+ itemsA, err := env.Fs.ListRecycle(env.Ctx, &provider.Reference{ResourceId: env.SpaceRootRes}, "", "")
Expect(err).ToNot(HaveOccurred())
Expect(len(itemsA)).To(Equal(2))
- itemsB, err := env.Fs.ListRecycle(env.Ctx, &provider.Reference{ResourceId: env.SpaceRootRes}, "", "/")
+ itemsB, err := env.Fs.ListRecycle(env.Ctx, &provider.Reference{ResourceId: env.SpaceRootRes}, "", "")
Expect(err).ToNot(HaveOccurred())
Expect(len(itemsB)).To(Equal(2))
Expect(itemsA).To(ConsistOf(itemsB))
@@ -179,7 +179,7 @@ var _ = Describe("Recycle", func() {
It("they can be permanently deleted by the other user", func() {
env.Blobstore.On("Delete", mock.Anything).Return(nil).Times(2)
- items, err := env.Fs.ListRecycle(env.Ctx, &provider.Reference{ResourceId: env.SpaceRootRes}, "", "/")
+ items, err := env.Fs.ListRecycle(env.Ctx, &provider.Reference{ResourceId: env.SpaceRootRes}, "", "")
Expect(err).ToNot(HaveOccurred())
Expect(len(items)).To(Equal(2))
@@ -194,13 +194,13 @@ var _ = Describe("Recycle", func() {
ctx2 = env.Ctx
}
- err = env.Fs.PurgeRecycleItem(ctx1, &provider.Reference{ResourceId: env.SpaceRootRes}, items[0].Key, "/")
+ err = env.Fs.PurgeRecycleItem(ctx1, &provider.Reference{ResourceId: env.SpaceRootRes}, items[0].Key, "")
Expect(err).ToNot(HaveOccurred())
- err = env.Fs.PurgeRecycleItem(ctx2, &provider.Reference{ResourceId: env.SpaceRootRes}, items[1].Key, "/")
+ err = env.Fs.PurgeRecycleItem(ctx2, &provider.Reference{ResourceId: env.SpaceRootRes}, items[1].Key, "")
Expect(err).ToNot(HaveOccurred())
- items, err = env.Fs.ListRecycle(env.Ctx, &provider.Reference{ResourceId: env.SpaceRootRes}, "", "/")
+ items, err = env.Fs.ListRecycle(env.Ctx, &provider.Reference{ResourceId: env.SpaceRootRes}, "", "")
Expect(err).ToNot(HaveOccurred())
Expect(len(items)).To(Equal(0))
})
@@ -208,7 +208,7 @@ var _ = Describe("Recycle", func() {
It("they can be restored by the other user", func() {
env.Blobstore.On("Delete", mock.Anything).Return(nil).Times(2)
- items, err := env.Fs.ListRecycle(env.Ctx, &provider.Reference{ResourceId: env.SpaceRootRes}, "", "/")
+ items, err := env.Fs.ListRecycle(env.Ctx, &provider.Reference{ResourceId: env.SpaceRootRes}, "", "")
Expect(err).ToNot(HaveOccurred())
Expect(len(items)).To(Equal(2))
@@ -223,13 +223,13 @@ var _ = Describe("Recycle", func() {
ctx2 = env.Ctx
}
- err = env.Fs.RestoreRecycleItem(ctx1, &provider.Reference{ResourceId: env.SpaceRootRes}, items[0].Key, "/", nil)
+ err = env.Fs.RestoreRecycleItem(ctx1, &provider.Reference{ResourceId: env.SpaceRootRes}, items[0].Key, "", nil)
Expect(err).ToNot(HaveOccurred())
- err = env.Fs.RestoreRecycleItem(ctx2, &provider.Reference{ResourceId: env.SpaceRootRes}, items[1].Key, "/", nil)
+ err = env.Fs.RestoreRecycleItem(ctx2, &provider.Reference{ResourceId: env.SpaceRootRes}, items[1].Key, "", nil)
Expect(err).ToNot(HaveOccurred())
- items, err = env.Fs.ListRecycle(env.Ctx, &provider.Reference{ResourceId: env.SpaceRootRes}, "", "/")
+ items, err = env.Fs.ListRecycle(env.Ctx, &provider.Reference{ResourceId: env.SpaceRootRes}, "", "")
Expect(err).ToNot(HaveOccurred())
Expect(len(items)).To(Equal(0))
})
@@ -269,12 +269,12 @@ var _ = Describe("Recycle", func() {
})
It("they are stored in different trashbins", func() {
- items, err := env.Fs.ListRecycle(env.Ctx, &provider.Reference{ResourceId: env.SpaceRootRes}, "", "/")
+ items, err := env.Fs.ListRecycle(env.Ctx, &provider.Reference{ResourceId: env.SpaceRootRes}, "", "")
Expect(err).ToNot(HaveOccurred())
Expect(len(items)).To(Equal(1))
recycled1 := items[0]
- items, err = env.Fs.ListRecycle(env.Ctx, &provider.Reference{ResourceId: projectID}, "", "/")
+ items, err = env.Fs.ListRecycle(env.Ctx, &provider.Reference{ResourceId: projectID}, "", "")
Expect(err).ToNot(HaveOccurred())
Expect(len(items)).To(Equal(1))
recycled2 := items[0]
@@ -283,7 +283,7 @@ var _ = Describe("Recycle", func() {
})
It("they can excess the spaces quota if restored", func() {
- items, err := env.Fs.ListRecycle(env.Ctx, &provider.Reference{ResourceId: projectID}, "", "/")
+ items, err := env.Fs.ListRecycle(env.Ctx, &provider.Reference{ResourceId: projectID}, "", "")
Expect(err).ToNot(HaveOccurred())
Expect(len(items)).To(Equal(1))
@@ -291,7 +291,7 @@ var _ = Describe("Recycle", func() {
_, err = env.CreateTestFile("largefile", "largefile-blobid", projectID.OpaqueId, projectID.SpaceId, 2000)
Expect(err).ToNot(HaveOccurred())
- err = env.Fs.RestoreRecycleItem(env.Ctx, &provider.Reference{ResourceId: projectID}, items[0].Key, "/", nil)
+ err = env.Fs.RestoreRecycleItem(env.Ctx, &provider.Reference{ResourceId: projectID}, items[0].Key, "", nil)
Expect(err).ToNot(HaveOccurred())
max, used, remaining, err := env.Fs.GetQuota(env.Ctx, &provider.Reference{ResourceId: projectID})
@@ -340,7 +340,7 @@ var _ = Describe("Recycle", func() {
})
Expect(err).ToNot(HaveOccurred())
- items, err := env.Fs.ListRecycle(ctx, &provider.Reference{ResourceId: env.SpaceRootRes}, "", "/")
+ items, err := env.Fs.ListRecycle(ctx, &provider.Reference{ResourceId: env.SpaceRootRes}, "", "")
Expect(err).ToNot(HaveOccurred())
Expect(len(items)).To(Equal(1))
})
@@ -353,7 +353,7 @@ var _ = Describe("Recycle", func() {
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("permission denied"))
- items, err := env.Fs.ListRecycle(ctx, &provider.Reference{ResourceId: env.SpaceRootRes}, "", "/")
+ items, err := env.Fs.ListRecycle(ctx, &provider.Reference{ResourceId: env.SpaceRootRes}, "", "")
Expect(err).ToNot(HaveOccurred())
Expect(len(items)).To(Equal(0))
})
@@ -365,11 +365,11 @@ var _ = Describe("Recycle", func() {
})
Expect(err).ToNot(HaveOccurred())
- items, err := env.Fs.ListRecycle(ctx, &provider.Reference{ResourceId: env.SpaceRootRes}, "", "/")
+ items, err := env.Fs.ListRecycle(ctx, &provider.Reference{ResourceId: env.SpaceRootRes}, "", "")
Expect(err).ToNot(HaveOccurred())
Expect(len(items)).To(Equal(1))
- err = env.Fs.PurgeRecycleItem(ctx, &provider.Reference{ResourceId: env.SpaceRootRes}, items[0].Key, "/")
+ err = env.Fs.PurgeRecycleItem(ctx, &provider.Reference{ResourceId: env.SpaceRootRes}, items[0].Key, "")
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("permission denied"))
})
@@ -381,11 +381,11 @@ var _ = Describe("Recycle", func() {
})
Expect(err).ToNot(HaveOccurred())
- items, err := env.Fs.ListRecycle(ctx, &provider.Reference{ResourceId: env.SpaceRootRes}, "", "/")
+ items, err := env.Fs.ListRecycle(ctx, &provider.Reference{ResourceId: env.SpaceRootRes}, "", "")
Expect(err).ToNot(HaveOccurred())
Expect(len(items)).To(Equal(1))
- err = env.Fs.RestoreRecycleItem(ctx, &provider.Reference{ResourceId: env.SpaceRootRes}, items[0].Key, "/", nil)
+ err = env.Fs.RestoreRecycleItem(ctx, &provider.Reference{ResourceId: env.SpaceRootRes}, items[0].Key, "", nil)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("permission denied"))
})
@@ -432,19 +432,19 @@ var _ = Describe("Recycle", func() {
})
Expect(err).ToNot(HaveOccurred())
- _, err = env.Fs.ListRecycle(ctx, &provider.Reference{ResourceId: env.SpaceRootRes}, "", "/")
+ _, err = env.Fs.ListRecycle(ctx, &provider.Reference{ResourceId: env.SpaceRootRes}, "", "")
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("not found"))
- items, err := env.Fs.ListRecycle(env.Ctx, &provider.Reference{ResourceId: env.SpaceRootRes}, "", "/")
+ items, err := env.Fs.ListRecycle(env.Ctx, &provider.Reference{ResourceId: env.SpaceRootRes}, "", "")
Expect(err).ToNot(HaveOccurred())
Expect(len(items)).To(Equal(1))
- err = env.Fs.PurgeRecycleItem(ctx, &provider.Reference{ResourceId: env.SpaceRootRes}, items[0].Key, "/")
+ err = env.Fs.PurgeRecycleItem(ctx, &provider.Reference{ResourceId: env.SpaceRootRes}, items[0].Key, "")
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("not found"))
- err = env.Fs.RestoreRecycleItem(ctx, &provider.Reference{ResourceId: env.SpaceRootRes}, items[0].Key, "/", nil)
+ err = env.Fs.RestoreRecycleItem(ctx, &provider.Reference{ResourceId: env.SpaceRootRes}, items[0].Key, "", nil)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("not found"))
})
diff --git a/pkg/storage/utils/decomposedfs/tree/tree.go b/pkg/storage/utils/decomposedfs/tree/tree.go
index dd68c1b550..de070c874f 100644
--- a/pkg/storage/utils/decomposedfs/tree/tree.go
+++ b/pkg/storage/utils/decomposedfs/tree/tree.go
@@ -584,10 +584,8 @@ func (t *Tree) RestoreRecycleItemFunc(ctx context.Context, spaceid, key, trashPa
attrs := node.Attributes{}
attrs.SetString(prefixes.NameAttr, targetNode.Name)
- if trashPath != "" {
- // set ParentidAttr to restorePath's node parent id
- attrs.SetString(prefixes.ParentidAttr, targetNode.ParentID)
- }
+ // set ParentidAttr to restorePath's node parent id
+ attrs.SetString(prefixes.ParentidAttr, targetNode.ParentID)
if err = recycleNode.SetXattrsWithContext(ctx, attrs, true); err != nil {
return errors.Wrap(err, "Decomposedfs: could not update recycle node")