Skip to content

Commit 92d1c7a

Browse files
committed
cephfs: initial implementation of recycle bin functionality
1 parent 2026acd commit 92d1c7a

File tree

5 files changed

+156
-11
lines changed

5 files changed

+156
-11
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Enhancement: recycle bin functionality for cephfs
2+
3+
This implementation is modeled after the CERN-deployed WinSpaces,
4+
where a folder within each space is designated as the recycle folder
5+
and organized by dates.
6+
7+
https://github.com/cs3org/reva/pull/4713

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ require (
2626
github.com/go-playground/validator/v10 v10.19.0
2727
github.com/go-sql-driver/mysql v1.8.0
2828
github.com/gofrs/uuid v4.4.0+incompatible
29+
github.com/gogo/protobuf v1.3.2
2930
github.com/golang-jwt/jwt v3.2.2+incompatible
3031
github.com/golang/protobuf v1.5.4
3132
github.com/gomodule/redigo v1.9.2

go.sum

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1019,6 +1019,7 @@ github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFG
10191019
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
10201020
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
10211021
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
1022+
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
10221023
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
10231024
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
10241025
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=

pkg/storage/fs/cephfs/cephfs.go

Lines changed: 123 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import (
3838
goceph "github.com/ceph/go-ceph/cephfs"
3939
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
4040
typepb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
41+
typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
4142
"github.com/cs3org/reva/pkg/appctx"
4243
"github.com/cs3org/reva/pkg/errtypes"
4344
"github.com/cs3org/reva/pkg/storage"
@@ -149,6 +150,21 @@ func (fs *cephfs) CreateDir(ctx context.Context, ref *provider.Reference) error
149150
return getRevaError(err)
150151
}
151152

153+
func getRecycleTargetFromPath(path string, recyclePath string, recyclePathDepth int) (string, error) {
154+
// Tokenize the given (absolute) path
155+
components := strings.Split(filepath.Clean(string(filepath.Separator)+path), string(filepath.Separator))
156+
if recyclePathDepth > len(components)-1 {
157+
return "", errors.New("path is too short")
158+
}
159+
160+
// And construct the target by injecting the recyclePath at the required depth
161+
var target []string = []string{string(filepath.Separator)}
162+
target = append(target, components[:recyclePathDepth+1]...)
163+
target = append(target, recyclePath, time.Now().Format("2006/01/02"))
164+
target = append(target, components[recyclePathDepth+1:]...)
165+
return filepath.Join(target...), nil
166+
}
167+
152168
func (fs *cephfs) Delete(ctx context.Context, ref *provider.Reference) (err error) {
153169
var path string
154170
user := fs.makeUser(ctx)
@@ -158,8 +174,16 @@ func (fs *cephfs) Delete(ctx context.Context, ref *provider.Reference) (err erro
158174
}
159175

160176
user.op(func(cv *cacheVal) {
161-
if err = cv.mount.Unlink(path); err != nil && err.Error() == errIsADirectory {
162-
err = cv.mount.RemoveDir(path)
177+
if fs.conf.RecyclePath != "" {
178+
// Recycle bin is configured, move to recycle as opposed to unlink
179+
targetPath, err := getRecycleTargetFromPath(path, fs.conf.RecyclePath, fs.conf.RecyclePathDepth)
180+
if err == nil {
181+
err = cv.mount.Rename(path, targetPath)
182+
}
183+
} else {
184+
if err = cv.mount.Unlink(path); err != nil && err.Error() == errIsADirectory {
185+
err = cv.mount.RemoveDir(path)
186+
}
163187
}
164188
})
165189

@@ -477,24 +501,113 @@ func (fs *cephfs) TouchFile(ctx context.Context, ref *provider.Reference) error
477501
return getRevaError(err)
478502
}
479503

480-
func (fs *cephfs) EmptyRecycle(ctx context.Context) error {
481-
return errtypes.NotSupported("unimplemented")
482-
}
504+
func (fs *cephfs) listDeletedEntries(ctx context.Context, maxentries int, basePath string, from, to time.Time) (res []*provider.RecycleItem, err error) {
505+
res = []*provider.RecycleItem{}
506+
user := fs.makeUser(ctx)
507+
count := 0
508+
rootRecyclePath := filepath.Join(basePath, fs.conf.RecyclePath)
509+
for d := to; !d.Before(from); d = d.AddDate(0, 0, -1) {
483510

484-
func (fs *cephfs) CreateStorageSpace(ctx context.Context, req *provider.CreateStorageSpaceRequest) (r *provider.CreateStorageSpaceResponse, err error) {
485-
return nil, errtypes.NotSupported("unimplemented")
511+
user.op(func(cv *cacheVal) {
512+
var dir *goceph.Directory
513+
if dir, err = cv.mount.OpenDir(filepath.Join(rootRecyclePath, d.Format("2006/01/02"))); err != nil {
514+
return
515+
}
516+
defer closeDir(dir)
517+
518+
var entry *goceph.DirEntryPlus
519+
for entry, err = dir.ReadDirPlus(goceph.StatxBasicStats, 0); entry != nil && err == nil; entry, err = dir.ReadDirPlus(goceph.StatxBasicStats, 0) {
520+
//TODO(lopresti) validate content of entry.Name() here.
521+
targetPath := filepath.Join(basePath, entry.Name())
522+
stat := entry.Statx()
523+
res = append(res, &provider.RecycleItem{
524+
Ref: &provider.Reference{Path: targetPath},
525+
Key: filepath.Join(rootRecyclePath, targetPath),
526+
Size: stat.Size,
527+
DeletionTime: &typesv1beta1.Timestamp{
528+
Seconds: uint64(stat.Mtime.Sec),
529+
Nanos: uint32(stat.Mtime.Nsec),
530+
},
531+
})
532+
533+
count += 1
534+
if count > maxentries {
535+
err = errtypes.BadRequest("list too long")
536+
return
537+
}
538+
}
539+
})
540+
}
541+
return res, err
486542
}
487543

488544
func (fs *cephfs) ListRecycle(ctx context.Context, basePath, key, relativePath string, from, to *typepb.Timestamp) ([]*provider.RecycleItem, error) {
489-
return nil, errtypes.NotSupported("unimplemented")
545+
md, err := fs.GetMD(ctx, &provider.Reference{Path: basePath}, nil)
546+
if err != nil {
547+
return nil, err
548+
}
549+
if !md.PermissionSet.ListRecycle {
550+
return nil, errtypes.PermissionDenied("cephfs: user doesn't have permissions to restore recycled items")
551+
}
552+
553+
var dateFrom, dateTo time.Time
554+
if from != nil && to != nil {
555+
dateFrom = time.Unix(int64(from.Seconds), 0)
556+
dateTo = time.Unix(int64(to.Seconds), 0)
557+
if dateFrom.AddDate(0, 0, fs.conf.MaxDaysInRecycleList).Before(dateTo) {
558+
return nil, errtypes.BadRequest("cephfs: too many days requested in listing the recycle bin")
559+
}
560+
} else {
561+
// if no date range was given, list up to two days ago
562+
dateTo = time.Now()
563+
dateFrom = dateTo.AddDate(0, 0, -2)
564+
}
565+
566+
sublog := appctx.GetLogger(ctx).With().Logger()
567+
sublog.Debug().Time("from", dateFrom).Time("to", dateTo).Msg("executing ListDeletedEntries")
568+
recycleEntries, err := fs.listDeletedEntries(ctx, fs.conf.MaxRecycleEntries, basePath, dateFrom, dateTo)
569+
if err != nil {
570+
switch err.(type) {
571+
case errtypes.IsBadRequest:
572+
return nil, errtypes.BadRequest("cephfs: too many entries found in listing the recycle bin")
573+
default:
574+
return nil, errors.Wrap(err, "cephfs: error listing deleted entries")
575+
}
576+
}
577+
return recycleEntries, nil
490578
}
491579

492580
func (fs *cephfs) RestoreRecycleItem(ctx context.Context, basePath, key, relativePath string, restoreRef *provider.Reference) error {
493-
return errtypes.NotSupported("unimplemented")
581+
user := fs.makeUser(ctx)
582+
md, err := fs.GetMD(ctx, &provider.Reference{Path: basePath}, nil)
583+
if err != nil {
584+
return err
585+
}
586+
if !md.PermissionSet.RestoreRecycleItem {
587+
return errtypes.PermissionDenied("cephfs: user doesn't have permissions to restore recycled items")
588+
}
589+
590+
user.op(func(cv *cacheVal) {
591+
//TODO(lopresti) validate content of basePath and relativePath. Key is expected to contain the recycled path
592+
if err = cv.mount.Rename(key, filepath.Join(basePath, relativePath)); err != nil {
593+
return
594+
}
595+
//TODO(tmourati): Add entry id logic, handle already moved file error
596+
})
597+
598+
return getRevaError(err)
494599
}
495600

496601
func (fs *cephfs) PurgeRecycleItem(ctx context.Context, basePath, key, relativePath string) error {
497-
return errtypes.NotSupported("unimplemented")
602+
return errtypes.NotSupported("cephfs: operation not supported")
603+
}
604+
605+
func (fs *cephfs) EmptyRecycle(ctx context.Context) error {
606+
return errtypes.NotSupported("cephfs: operation not supported")
607+
}
608+
609+
func (fs *cephfs) CreateStorageSpace(ctx context.Context, req *provider.CreateStorageSpaceRequest) (r *provider.CreateStorageSpaceResponse, err error) {
610+
return nil, errtypes.NotSupported("unimplemented")
498611
}
499612

500613
func (fs *cephfs) ListStorageSpaces(ctx context.Context, filter []*provider.ListStorageSpacesRequest_Filter) ([]*provider.StorageSpace, error) {

pkg/storage/fs/cephfs/options.go

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,19 @@ type Options struct {
3838
DirPerms uint32 `mapstructure:"dir_perms"`
3939
FilePerms uint32 `mapstructure:"file_perms"`
4040
UserQuotaBytes uint64 `mapstructure:"user_quota_bytes"`
41-
HiddenDirs map[string]bool
41+
// Path of the recycle bin. If empty, recycling is disabled.
42+
RecyclePath string `mapstructure:"recycle_path"`
43+
// Depth of the Recycle bin location, that is after how many path components
44+
// the recycle path is located: this allows supporting recycles such as
45+
// /top-level/s/space/.recycle with a depth = 3. Defaults to 0.
46+
RecyclePathDepth int `mapstructure:"recycle_path_depth"`
47+
// Maximum entries count a ListRecycle call may return: if exceeded, ListRecycle
48+
// will return a BadRequest error
49+
MaxRecycleEntries int `mapstructure:"max_recycle_entries"`
50+
// Maximum time span in days a ListRecycle call may return: if exceeded, ListRecycle
51+
// will override the "to" date with "from" + this value
52+
MaxDaysInRecycleList int `mapstructure:"max_days_in_recycle_list"`
53+
HiddenDirs map[string]bool
4254
}
4355

4456
func (c *Options) ApplyDefaults() {
@@ -83,6 +95,9 @@ func (c *Options) ApplyDefaults() {
8395
"..": true,
8496
removeLeadingSlash(c.UploadFolder): true,
8597
}
98+
if c.RecyclePath != "" {
99+
c.HiddenDirs[c.RecyclePath] = true
100+
}
86101

87102
if c.DirPerms == 0 {
88103
c.DirPerms = dirPermDefault
@@ -95,4 +110,12 @@ func (c *Options) ApplyDefaults() {
95110
if c.UserQuotaBytes == 0 {
96111
c.UserQuotaBytes = 50000000000
97112
}
113+
114+
if c.MaxDaysInRecycleList == 0 {
115+
c.MaxDaysInRecycleList = 14
116+
}
117+
118+
if c.MaxRecycleEntries == 0 {
119+
c.MaxRecycleEntries = 2000
120+
}
98121
}

0 commit comments

Comments
 (0)