From 9e40f2ecd9c15aa80d892a7a0fc4a9e0044c05dc Mon Sep 17 00:00:00 2001 From: David Christofas Date: Fri, 25 Jun 2021 10:56:25 +0200 Subject: [PATCH] implement depth handling for propfinds on the trash-bin --- .../grpc/services/gateway/storageprovider.go | 9 +- .../storageprovider/storageprovider.go | 12 +- .../http/services/owncloud/ocdav/trashbin.go | 62 +++++++- pkg/storage/fs/owncloud/owncloud.go | 2 +- pkg/storage/fs/owncloudsql/owncloudsql.go | 2 +- pkg/storage/fs/s3/s3.go | 2 +- pkg/storage/storage.go | 2 +- pkg/storage/utils/decomposedfs/recycle.go | 134 +++++++++++++++++- pkg/storage/utils/eosfs/eosfs.go | 2 +- pkg/storage/utils/localfs/localfs.go | 2 +- 10 files changed, 208 insertions(+), 21 deletions(-) diff --git a/internal/grpc/services/gateway/storageprovider.go b/internal/grpc/services/gateway/storageprovider.go index 466bb7f3036..cc91b6ba83a 100644 --- a/internal/grpc/services/gateway/storageprovider.go +++ b/internal/grpc/services/gateway/storageprovider.go @@ -31,6 +31,7 @@ import ( rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" registry "github.com/cs3org/go-cs3apis/cs3/storage/registry/v1beta1" + typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" "github.com/cs3org/reva/pkg/appctx" "github.com/cs3org/reva/pkg/errtypes" "github.com/cs3org/reva/pkg/rgrpc/status" @@ -1831,8 +1832,14 @@ func (s *svc) ListRecycle(ctx context.Context, req *gateway.ListRecycleRequest) }, nil } + o := &typesv1beta1.Opaque{ + Map: map[string]*typesv1beta1.OpaqueEntry{ + "refPath": {Decoder: "plain", Value: []byte(req.Ref.Path)}, + }, + } + res, err := c.ListRecycle(ctx, &provider.ListRecycleRequest{ - Opaque: req.Opaque, + Opaque: o, FromTs: req.FromTs, ToTs: req.ToTs, }) diff --git a/internal/grpc/services/storageprovider/storageprovider.go b/internal/grpc/services/storageprovider/storageprovider.go index cc2f296c552..013fed44cf8 100644 --- a/internal/grpc/services/storageprovider/storageprovider.go +++ b/internal/grpc/services/storageprovider/storageprovider.go @@ -754,7 +754,7 @@ func (s *service) ListRecycleStream(req *provider.ListRecycleStreamRequest, ss p ctx := ss.Context() log := appctx.GetLogger(ctx) - items, err := s.storage.ListRecycle(ctx) + items, err := s.storage.ListRecycle(ctx, nil) if err != nil { var st *rpc.Status switch err.(type) { @@ -790,7 +790,15 @@ func (s *service) ListRecycleStream(req *provider.ListRecycleStreamRequest, ss p } func (s *service) ListRecycle(ctx context.Context, req *provider.ListRecycleRequest) (*provider.ListRecycleResponse, error) { - items, err := s.storage.ListRecycle(ctx) + var p string + if v, ok := req.Opaque.Map["refPath"]; ok { + p = string(v.Value) + } + ref, err := s.unwrap(ctx, &provider.Reference{Path: p}) + if err != nil { + return nil, err + } + items, err := s.storage.ListRecycle(ctx, ref) // TODO(labkode): CRITICAL: fill recycle info with storage provider. if err != nil { var st *rpc.Status diff --git a/internal/http/services/owncloud/ocdav/trashbin.go b/internal/http/services/owncloud/ocdav/trashbin.go index d19c0866312..24cf6b21ce2 100644 --- a/internal/http/services/owncloud/ocdav/trashbin.go +++ b/internal/http/services/owncloud/ocdav/trashbin.go @@ -107,8 +107,8 @@ func (h *TrashbinHandler) Handler(s *svc) http.Handler { // return //} - if key == "" && r.Method == "PROPFIND" { - h.listTrashbin(w, r, s, u) + if r.Method == "PROPFIND" { + h.listTrashbin(w, r, s, u, path.Join(key, r.URL.Path)) return } if key != "" && r.Method == "MOVE" { @@ -142,13 +142,25 @@ func (h *TrashbinHandler) Handler(s *svc) http.Handler { }) } -func (h *TrashbinHandler) listTrashbin(w http.ResponseWriter, r *http.Request, s *svc, u *userpb.User) { +func (h *TrashbinHandler) listTrashbin(w http.ResponseWriter, r *http.Request, s *svc, u *userpb.User, key string) { ctx := r.Context() ctx, span := trace.StartSpan(ctx, "listTrashbin") defer span.End() + depth := r.Header.Get("Depth") + if depth == "" { + depth = "1" + } + sublog := appctx.GetLogger(ctx).With().Logger() + // see https://tools.ietf.org/html/rfc4918#section-9.1 + if depth != "0" && depth != "1" && depth != "infinity" { + sublog.Debug().Str("depth", depth).Msgf("invalid Depth header value") + w.WriteHeader(http.StatusBadRequest) + return + } + pf, status, err := readPropfind(r.Body) if err != nil { sublog.Debug().Err(err).Msg("error reading propfind request") @@ -179,7 +191,7 @@ func (h *TrashbinHandler) listTrashbin(w http.ResponseWriter, r *http.Request, s // ask gateway for recycle items // TODO(labkode): add Reference to ListRecycleRequest - getRecycleRes, err := gc.ListRecycle(ctx, &gateway.ListRecycleRequest{Ref: &provider.Reference{Path: getHomeRes.Path}}) + getRecycleRes, err := gc.ListRecycle(ctx, &gateway.ListRecycleRequest{Ref: &provider.Reference{Path: filepath.Join(getHomeRes.Path, key)}}) if err != nil { sublog.Error().Err(err).Msg("error calling ListRecycle") @@ -192,7 +204,47 @@ func (h *TrashbinHandler) listTrashbin(w http.ResponseWriter, r *http.Request, s return } - propRes, err := h.formatTrashPropfind(ctx, s, u, &pf, getRecycleRes.RecycleItems) + items := getRecycleRes.RecycleItems + + if depth == "infinity" { + var stack []string + // check sub-containers in reverse order and add them to the stack + // the reversed order here will produce a more logical sorting of results + for i := len(items) - 1; i >= 0; i-- { + // for i := range res.Infos { + if items[i].Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER { + stack = append(stack, items[i].Key) + } + } + + for len(stack) > 0 { + key := stack[len(stack)-1] + getRecycleRes, err := gc.ListRecycle(ctx, &gateway.ListRecycleRequest{Ref: &provider.Reference{Path: path.Join(getHomeRes.Path, key)}}) + if err != nil { + sublog.Error().Err(err).Msg("error calling ListRecycle") + w.WriteHeader(http.StatusInternalServerError) + return + } + + if getRecycleRes.Status.Code != rpc.Code_CODE_OK { + HandleErrorStatus(&sublog, w, getRecycleRes.Status) + return + } + items = append(items, getRecycleRes.RecycleItems...) + + stack = stack[:len(stack)-1] + // check sub-containers in reverse order and add them to the stack + // the reversed order here will produce a more logical sorting of results + for i := len(getRecycleRes.RecycleItems) - 1; i >= 0; i-- { + // for i := range res.Infos { + if getRecycleRes.RecycleItems[i].Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER { + stack = append(stack, getRecycleRes.RecycleItems[i].Key) + } + } + } + } + + propRes, err := h.formatTrashPropfind(ctx, s, u, &pf, items) if err != nil { sublog.Error().Err(err).Msg("error formatting propfind") w.WriteHeader(http.StatusInternalServerError) diff --git a/pkg/storage/fs/owncloud/owncloud.go b/pkg/storage/fs/owncloud/owncloud.go index bc4fcf00581..3ba6423a358 100644 --- a/pkg/storage/fs/owncloud/owncloud.go +++ b/pkg/storage/fs/owncloud/owncloud.go @@ -2149,7 +2149,7 @@ func (fs *ocfs) convertToRecycleItem(ctx context.Context, rp string, md os.FileI } } -func (fs *ocfs) ListRecycle(ctx context.Context) ([]*provider.RecycleItem, error) { +func (fs *ocfs) ListRecycle(ctx context.Context, ref *provider.Reference) ([]*provider.RecycleItem, error) { // TODO check permission? on what? user must be the owner? rp, err := fs.getRecyclePath(ctx) if err != nil { diff --git a/pkg/storage/fs/owncloudsql/owncloudsql.go b/pkg/storage/fs/owncloudsql/owncloudsql.go index 030d079ce53..b0773e15450 100644 --- a/pkg/storage/fs/owncloudsql/owncloudsql.go +++ b/pkg/storage/fs/owncloudsql/owncloudsql.go @@ -2000,7 +2000,7 @@ func (fs *ocfs) convertToRecycleItem(ctx context.Context, md os.FileInfo) *provi } } -func (fs *ocfs) ListRecycle(ctx context.Context) ([]*provider.RecycleItem, error) { +func (fs *ocfs) ListRecycle(ctx context.Context, ref *provider.Reference) ([]*provider.RecycleItem, error) { // TODO check permission? on what? user must be the owner? rp, err := fs.getRecyclePath(ctx) if err != nil { diff --git a/pkg/storage/fs/s3/s3.go b/pkg/storage/fs/s3/s3.go index 83c1fe1db8b..1ed0a0ea68d 100644 --- a/pkg/storage/fs/s3/s3.go +++ b/pkg/storage/fs/s3/s3.go @@ -654,7 +654,7 @@ func (fs *s3FS) EmptyRecycle(ctx context.Context) error { return errtypes.NotSupported("empty recycle") } -func (fs *s3FS) ListRecycle(ctx context.Context) ([]*provider.RecycleItem, error) { +func (fs *s3FS) ListRecycle(ctx context.Context, ref *provider.Reference) ([]*provider.RecycleItem, error) { return nil, errtypes.NotSupported("list recycle") } diff --git a/pkg/storage/storage.go b/pkg/storage/storage.go index 8481c45a989..687fe9eab35 100644 --- a/pkg/storage/storage.go +++ b/pkg/storage/storage.go @@ -42,7 +42,7 @@ type FS interface { ListRevisions(ctx context.Context, ref *provider.Reference) ([]*provider.FileVersion, error) DownloadRevision(ctx context.Context, ref *provider.Reference, key string) (io.ReadCloser, error) RestoreRevision(ctx context.Context, ref *provider.Reference, key string) error - ListRecycle(ctx context.Context) ([]*provider.RecycleItem, error) + ListRecycle(ctx context.Context, ref *provider.Reference) ([]*provider.RecycleItem, error) RestoreRecycleItem(ctx context.Context, key string, restoreRef *provider.Reference) error PurgeRecycleItem(ctx context.Context, key string) error EmptyRecycle(ctx context.Context) error diff --git a/pkg/storage/utils/decomposedfs/recycle.go b/pkg/storage/utils/decomposedfs/recycle.go index 28f3bfedcc1..70007215e7f 100644 --- a/pkg/storage/utils/decomposedfs/recycle.go +++ b/pkg/storage/utils/decomposedfs/recycle.go @@ -21,6 +21,7 @@ package decomposedfs import ( "context" "os" + "path" "path/filepath" "strings" "time" @@ -34,6 +35,7 @@ import ( "github.com/cs3org/reva/pkg/user" "github.com/pkg/errors" "github.com/pkg/xattr" + "github.com/rs/zerolog" ) // Recycle items are stored inside the node folder and start with the uuid of the deleted node. @@ -45,12 +47,12 @@ import ( // contain a directory with symlinks to trash files for every userid/"root" // ListRecycle returns the list of available recycle items -func (fs *Decomposedfs) ListRecycle(ctx context.Context) (items []*provider.RecycleItem, err error) { +func (fs *Decomposedfs) ListRecycle(ctx context.Context, ref *provider.Reference) ([]*provider.RecycleItem, error) { log := appctx.GetLogger(ctx) trashRoot := fs.getRecycleRoot(ctx) - items = make([]*provider.RecycleItem, 0) + items := make([]*provider.RecycleItem, 0) // TODO how do we check if the storage allows listing the recycle for the current user? check owner of the root of the storage? // use permissions ReadUserPermissions? @@ -66,6 +68,109 @@ func (fs *Decomposedfs) ListRecycle(ctx context.Context) (items []*provider.Recy } } + if ref.Path == "/" { + return fs.listTrashRoot(ctx, trashRoot, log) + } + + f, err := os.Open(filepath.Join(trashRoot, ref.Path)) + if err != nil { + if os.IsNotExist(err) { + return items, nil + } + return nil, errors.Wrap(err, "tree: error listing "+trashRoot) + } + defer f.Close() + + // TODO single file listing + md, err := f.Stat() + if err != nil { + return nil, err + } + + root, tail := shiftPath(ref.Path) + + parentNode, err := os.Readlink(filepath.Join(trashRoot, root)) + if err != nil { + log.Error().Err(err).Str("trashRoot", trashRoot).Msg("error reading trash link, skipping") + return nil, err + } + parentPath := fs.lu.InternalPath(filepath.Base(parentNode)) + + if !md.IsDir() { + return items, nil + } + + names, err := f.Readdirnames(0) + if err != nil { + return nil, err + } + for i := range names { + trashnode, err := os.Readlink(filepath.Join(trashRoot, ref.Path, names[i])) + if err != nil { + log.Error().Err(err).Str("trashRoot", trashRoot).Str("name", names[i]).Msg("error reading trash link, skipping") + continue + } + parts := strings.SplitN(filepath.Base(parentNode), ".T.", 2) + if len(parts) != 2 { + log.Error().Err(err).Str("trashRoot", trashRoot).Str("name", names[i]).Str("trashnode", trashnode).Interface("parts", parts).Msg("malformed trash link, skipping") + continue + } + + nodePath := fs.lu.InternalPath(filepath.Base(trashnode)) + md, err := os.Stat(nodePath) + if err != nil { + log.Error().Err(err).Str("trashRoot", trashRoot).Str("name", names[i]).Str("trashnode", trashnode). /*.Interface("parts", parts)*/ Msg("could not stat trash item, skipping") + continue + } + + item := &provider.RecycleItem{ + Type: getResourceType(md.IsDir()), + Size: uint64(md.Size()), + Key: path.Join(parts[0], tail, names[i]), + } + if deletionTime, err := time.Parse(time.RFC3339Nano, parts[1]); err == nil { + item.DeletionTime = &types.Timestamp{ + Seconds: uint64(deletionTime.Unix()), + // TODO nanos + } + } else { + log.Error().Err(err).Str("trashRoot", trashRoot).Str("name", names[i]).Str("link", trashnode).Interface("parts", parts).Msg("could parse time format, ignoring") + } + + // lookup origin path in extended attributes + var attrBytes []byte + if attrBytes, err = xattr.Get(parentPath, xattrs.TrashOriginAttr); err == nil { + item.Ref = &provider.Reference{Path: filepath.Join(string(attrBytes), tail, names[i])} + } else { + log.Error().Err(err).Str("trashRoot", trashRoot).Str("name", names[i]).Str("link", trashnode).Msg("could not read origin path, skipping") + continue + } + // TODO filter results by permission ... on the original parent? or the trashed node? + // if it were on the original parent it would be possible to see files that were trashed before the current user got access + // so -> check the trash node itself + // hmm listing trash currently lists the current users trash or the 'root' trash. from ocs only the home storage is queried for trash items. + // for now we can only really check if the current user is the owner + if attrBytes, err = xattr.Get(nodePath, xattrs.OwnerIDAttr); err == nil { + if fs.o.EnableHome { + u := user.ContextMustGetUser(ctx) + if u.Id.OpaqueId != string(attrBytes) { + log.Warn().Str("trashRoot", trashRoot).Str("name", names[i]).Str("link", trashnode).Msg("trash item not owned by current user, skipping") + continue + } + } + } else { + log.Error().Err(err).Str("trashRoot", trashRoot).Str("name", names[i]).Str("link", trashnode).Msg("could not read owner, skipping") + continue + } + + items = append(items, item) + } + return items, nil +} + +func (fs *Decomposedfs) listTrashRoot(ctx context.Context, trashRoot string, log *zerolog.Logger) ([]*provider.RecycleItem, error) { + items := make([]*provider.RecycleItem, 0) + f, err := os.Open(trashRoot) if err != nil { if os.IsNotExist(err) { @@ -80,11 +185,9 @@ func (fs *Decomposedfs) ListRecycle(ctx context.Context) (items []*provider.Recy return nil, err } for i := range names { - var trashnode string - trashnode, err = os.Readlink(filepath.Join(trashRoot, names[i])) + trashnode, err := os.Readlink(filepath.Join(trashRoot, names[i])) if err != nil { log.Error().Err(err).Str("trashRoot", trashRoot).Str("name", names[i]).Msg("error reading trash link, skipping") - err = nil continue } parts := strings.SplitN(filepath.Base(trashnode), ".T.", 2) @@ -96,7 +199,7 @@ func (fs *Decomposedfs) ListRecycle(ctx context.Context) (items []*provider.Recy nodePath := fs.lu.InternalPath(filepath.Base(trashnode)) md, err := os.Stat(nodePath) if err != nil { - log.Error().Err(err).Str("trashRoot", trashRoot).Str("name", names[i]).Str("trashnode", trashnode).Interface("parts", parts).Msg("could not stat trash item, skipping") + log.Error().Err(err).Str("trashRoot", trashRoot).Str("name", names[i]).Str("trashnode", trashnode). /*.Interface("parts", parts)*/ Msg("could not stat trash item, skipping") continue } @@ -142,7 +245,7 @@ func (fs *Decomposedfs) ListRecycle(ctx context.Context) (items []*provider.Recy items = append(items, item) } - return + return items, nil } // RestoreRecycleItem restores the specified item @@ -220,3 +323,20 @@ func (fs *Decomposedfs) getRecycleRoot(ctx context.Context) string { } return filepath.Join(fs.o.Root, "trash", "root") } + +// shiftPath splits off the first component of p, which will be cleaned of +// relative components before processing. head will never contain a slash and +// tail will always be a rooted path without trailing slash. +// see https://blog.merovius.de/2017/06/18/how-not-to-use-an-http-router.html +// and https://gist.github.com/weatherglass/62bd8a704d4dfdc608fe5c5cb5a6980c#gistcomment-2161690 for the zero alloc code below +func shiftPath(p string) (head, tail string) { + if p == "" { + return "", "/" + } + p = strings.TrimPrefix(path.Clean(p), "/") + i := strings.Index(p, "/") + if i < 0 { + return p, "/" + } + return p[:i], p[i:] +} diff --git a/pkg/storage/utils/eosfs/eosfs.go b/pkg/storage/utils/eosfs/eosfs.go index f5888a48f65..abac5bce973 100644 --- a/pkg/storage/utils/eosfs/eosfs.go +++ b/pkg/storage/utils/eosfs/eosfs.go @@ -1380,7 +1380,7 @@ func (fs *eosfs) EmptyRecycle(ctx context.Context) error { return fs.c.PurgeDeletedEntries(ctx, uid, gid) } -func (fs *eosfs) ListRecycle(ctx context.Context) ([]*provider.RecycleItem, error) { +func (fs *eosfs) ListRecycle(ctx context.Context, ref *provider.Reference) ([]*provider.RecycleItem, error) { u, err := getUser(ctx) if err != nil { return nil, errors.Wrap(err, "eosfs: no user in ctx") diff --git a/pkg/storage/utils/localfs/localfs.go b/pkg/storage/utils/localfs/localfs.go index d9c7e138703..6d27e6557ff 100644 --- a/pkg/storage/utils/localfs/localfs.go +++ b/pkg/storage/utils/localfs/localfs.go @@ -1181,7 +1181,7 @@ func (fs *localfs) convertToRecycleItem(ctx context.Context, rp string, md os.Fi } } -func (fs *localfs) ListRecycle(ctx context.Context) ([]*provider.RecycleItem, error) { +func (fs *localfs) ListRecycle(ctx context.Context, ref *provider.Reference) ([]*provider.RecycleItem, error) { rp := fs.wrapRecycleBin(ctx, "/")