Skip to content

Commit

Permalink
implement depth handling for propfinds on the trash-bin
Browse files Browse the repository at this point in the history
  • Loading branch information
David Christofas committed Jun 25, 2021
1 parent df52c78 commit 9e40f2e
Show file tree
Hide file tree
Showing 10 changed files with 208 additions and 21 deletions.
9 changes: 8 additions & 1 deletion internal/grpc/services/gateway/storageprovider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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,
})
Expand Down
12 changes: 10 additions & 2 deletions internal/grpc/services/storageprovider/storageprovider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Expand Down
62 changes: 57 additions & 5 deletions internal/http/services/owncloud/ocdav/trashbin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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" {
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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")
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion pkg/storage/fs/owncloud/owncloud.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion pkg/storage/fs/owncloudsql/owncloudsql.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion pkg/storage/fs/s3/s3.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}

Expand Down
2 changes: 1 addition & 1 deletion pkg/storage/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
134 changes: 127 additions & 7 deletions pkg/storage/utils/decomposedfs/recycle.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ package decomposedfs
import (
"context"
"os"
"path"
"path/filepath"
"strings"
"time"
Expand All @@ -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.
Expand All @@ -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?
Expand All @@ -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) {
Expand All @@ -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)
Expand All @@ -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
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:]
}
2 changes: 1 addition & 1 deletion pkg/storage/utils/eosfs/eosfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
2 changes: 1 addition & 1 deletion pkg/storage/utils/localfs/localfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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, "/")

Expand Down

0 comments on commit 9e40f2e

Please sign in to comment.