github.com/cs3org/reva/v2@v2.27.7/pkg/storage/utils/decomposedfs/recycle.go (about)

     1  // Copyright 2018-2021 CERN
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  //
    15  // In applying this license, CERN does not waive the privileges and immunities
    16  // granted to it by virtue of its status as an Intergovernmental Organization
    17  // or submit itself to any jurisdiction.
    18  
    19  package decomposedfs
    20  
    21  import (
    22  	"context"
    23  	iofs "io/fs"
    24  	"os"
    25  	"path/filepath"
    26  	"strings"
    27  	"time"
    28  
    29  	"github.com/pkg/errors"
    30  	"golang.org/x/sync/errgroup"
    31  
    32  	provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
    33  	types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
    34  	"github.com/cs3org/reva/v2/pkg/appctx"
    35  	"github.com/cs3org/reva/v2/pkg/errtypes"
    36  	"github.com/cs3org/reva/v2/pkg/storage"
    37  	"github.com/cs3org/reva/v2/pkg/storage/utils/decomposedfs/lookup"
    38  	"github.com/cs3org/reva/v2/pkg/storage/utils/decomposedfs/metadata/prefixes"
    39  	"github.com/cs3org/reva/v2/pkg/storage/utils/decomposedfs/node"
    40  	"github.com/cs3org/reva/v2/pkg/storagespace"
    41  )
    42  
    43  type DecomposedfsTrashbin struct {
    44  	fs *Decomposedfs
    45  }
    46  
    47  // Setup the trashbin
    48  func (tb *DecomposedfsTrashbin) Setup(fs storage.FS) error {
    49  	if _, ok := fs.(*Decomposedfs); !ok {
    50  		return errors.New("invalid filesystem")
    51  	}
    52  	tb.fs = fs.(*Decomposedfs)
    53  	return nil
    54  }
    55  
    56  // Recycle items are stored inside the node folder and start with the uuid of the deleted node.
    57  // The `.T.` indicates it is a trash item and what follows is the timestamp of the deletion.
    58  // The deleted file is kept in the same location/dir as the original node. This prevents deletes
    59  // from triggering cross storage moves when the trash is accidentally stored on another partition,
    60  // because the admin mounted a different partition there.
    61  // For an efficient listing of deleted nodes the ocis storage driver maintains a 'trash' folder
    62  // with symlinks to trash files for every storagespace.
    63  
    64  // ListRecycle returns the list of available recycle items
    65  // ref -> the space (= resourceid), key -> deleted node id, relativePath = relative to key
    66  func (tb *DecomposedfsTrashbin) ListRecycle(ctx context.Context, ref *provider.Reference, key, relativePath string) ([]*provider.RecycleItem, error) {
    67  	_, span := tracer.Start(ctx, "ListRecycle")
    68  	defer span.End()
    69  	if ref == nil || ref.ResourceId == nil || ref.ResourceId.OpaqueId == "" {
    70  		return nil, errtypes.BadRequest("spaceid required")
    71  	}
    72  	if key == "" && relativePath != "" {
    73  		return nil, errtypes.BadRequest("key is required when navigating with a path")
    74  	}
    75  	spaceID := ref.ResourceId.OpaqueId
    76  
    77  	sublog := appctx.GetLogger(ctx).With().Str("spaceid", spaceID).Str("key", key).Str("relative_path", relativePath).Logger()
    78  
    79  	// check permissions
    80  	trashnode, err := tb.fs.lu.NodeFromSpaceID(ctx, spaceID)
    81  	if err != nil {
    82  		return nil, err
    83  	}
    84  	rp, err := tb.fs.p.AssembleTrashPermissions(ctx, trashnode)
    85  	switch {
    86  	case err != nil:
    87  		return nil, err
    88  	case !rp.ListRecycle:
    89  		if rp.Stat {
    90  			return nil, errtypes.PermissionDenied(key)
    91  		}
    92  		return nil, errtypes.NotFound(key)
    93  	}
    94  
    95  	if key == "" && relativePath == "" {
    96  		return tb.listTrashRoot(ctx, spaceID)
    97  	}
    98  
    99  	// build a list of trash items relative to the given trash root and path
   100  	items := make([]*provider.RecycleItem, 0)
   101  
   102  	trashRootPath := filepath.Join(tb.getRecycleRoot(spaceID), lookup.Pathify(key, 4, 2))
   103  	originalPath, _, timeSuffix, err := readTrashLink(trashRootPath)
   104  	if err != nil {
   105  		sublog.Error().Err(err).Str("trashRoot", trashRootPath).Msg("error reading trash link")
   106  		return nil, err
   107  	}
   108  
   109  	origin := ""
   110  	attrs, err := tb.fs.lu.MetadataBackend().All(ctx, originalPath)
   111  	if err != nil {
   112  		return items, err
   113  	}
   114  	// lookup origin path in extended attributes
   115  	if attrBytes, ok := attrs[prefixes.TrashOriginAttr]; ok {
   116  		origin = string(attrBytes)
   117  	} else {
   118  		sublog.Error().Err(err).Str("spaceid", spaceID).Msg("could not read origin path, skipping")
   119  		return nil, err
   120  	}
   121  
   122  	// all deleted items have the same deletion time
   123  	var deletionTime *types.Timestamp
   124  	if parsed, err := time.Parse(time.RFC3339Nano, timeSuffix); err == nil {
   125  		deletionTime = &types.Timestamp{
   126  			Seconds: uint64(parsed.Unix()),
   127  			// TODO nanos
   128  		}
   129  	} else {
   130  		sublog.Error().Err(err).Msg("could not parse time format, ignoring")
   131  	}
   132  
   133  	var size int64
   134  	if relativePath == "" {
   135  		// this is the case when we want to directly list a file in the trashbin
   136  		nodeType := tb.fs.lu.TypeFromPath(ctx, originalPath)
   137  		switch nodeType {
   138  		case provider.ResourceType_RESOURCE_TYPE_FILE:
   139  			_, size, err = tb.fs.lu.ReadBlobIDAndSizeAttr(ctx, originalPath, nil)
   140  			if err != nil {
   141  				return items, err
   142  			}
   143  		case provider.ResourceType_RESOURCE_TYPE_CONTAINER:
   144  			size, err = tb.fs.lu.MetadataBackend().GetInt64(ctx, originalPath, prefixes.TreesizeAttr)
   145  			if err != nil {
   146  				return items, err
   147  			}
   148  		}
   149  		item := &provider.RecycleItem{
   150  			Type:         tb.fs.lu.TypeFromPath(ctx, originalPath),
   151  			Size:         uint64(size),
   152  			Key:          filepath.Join(key, relativePath),
   153  			DeletionTime: deletionTime,
   154  			Ref: &provider.Reference{
   155  				Path: filepath.Join(origin, relativePath),
   156  			},
   157  		}
   158  		items = append(items, item)
   159  		return items, err
   160  	}
   161  
   162  	// we have to read the names and stat the path to follow the symlinks
   163  	childrenPath := filepath.Join(originalPath, relativePath)
   164  	childrenDir, err := os.Open(childrenPath)
   165  	if err != nil {
   166  		return nil, err
   167  	}
   168  
   169  	names, err := childrenDir.Readdirnames(0)
   170  	if err != nil {
   171  		return nil, err
   172  	}
   173  	for _, name := range names {
   174  		resolvedChildPath, err := filepath.EvalSymlinks(filepath.Join(childrenPath, name))
   175  		if err != nil {
   176  			sublog.Error().Err(err).Str("name", name).Msg("could not resolve symlink, skipping")
   177  			continue
   178  		}
   179  
   180  		// reset size
   181  		size = 0
   182  
   183  		nodeType := tb.fs.lu.TypeFromPath(ctx, resolvedChildPath)
   184  		switch nodeType {
   185  		case provider.ResourceType_RESOURCE_TYPE_FILE:
   186  			_, size, err = tb.fs.lu.ReadBlobIDAndSizeAttr(ctx, resolvedChildPath, nil)
   187  			if err != nil {
   188  				sublog.Error().Err(err).Str("name", name).Msg("invalid blob size, skipping")
   189  				continue
   190  			}
   191  		case provider.ResourceType_RESOURCE_TYPE_CONTAINER:
   192  			size, err = tb.fs.lu.MetadataBackend().GetInt64(ctx, resolvedChildPath, prefixes.TreesizeAttr)
   193  			if err != nil {
   194  				sublog.Error().Err(err).Str("name", name).Msg("invalid tree size, skipping")
   195  				continue
   196  			}
   197  		case provider.ResourceType_RESOURCE_TYPE_INVALID:
   198  			sublog.Error().Err(err).Str("name", name).Str("resolvedChildPath", resolvedChildPath).Msg("invalid node type, skipping")
   199  			continue
   200  		}
   201  
   202  		item := &provider.RecycleItem{
   203  			Type:         nodeType,
   204  			Size:         uint64(size),
   205  			Key:          filepath.Join(key, relativePath, name),
   206  			DeletionTime: deletionTime,
   207  			Ref: &provider.Reference{
   208  				Path: filepath.Join(origin, relativePath, name),
   209  			},
   210  		}
   211  		items = append(items, item)
   212  	}
   213  	return items, nil
   214  }
   215  
   216  // readTrashLink returns path, nodeID and timestamp
   217  func readTrashLink(path string) (string, string, string, error) {
   218  	link, err := os.Readlink(path)
   219  	if err != nil {
   220  		return "", "", "", err
   221  	}
   222  	resolved, err := filepath.EvalSymlinks(path)
   223  	if err != nil {
   224  		return "", "", "", err
   225  	}
   226  	// ../../../../../nodes/e5/6c/75/a8/-d235-4cbb-8b4e-48b6fd0f2094.T.2022-02-16T14:38:11.769917408Z
   227  	// TODO use filepath.Separator to support windows
   228  	link = strings.ReplaceAll(link, "/", "")
   229  	// ..........nodese56c75a8-d235-4cbb-8b4e-48b6fd0f2094.T.2022-02-16T14:38:11.769917408Z
   230  	if link[0:15] != "..........nodes" || link[51:54] != node.TrashIDDelimiter {
   231  		return "", "", "", errtypes.InternalError("malformed trash link")
   232  	}
   233  	return resolved, link[15:51], link[54:], nil
   234  }
   235  
   236  func (tb *DecomposedfsTrashbin) listTrashRoot(ctx context.Context, spaceID string) ([]*provider.RecycleItem, error) {
   237  	log := appctx.GetLogger(ctx)
   238  	trashRoot := tb.getRecycleRoot(spaceID)
   239  	items := []*provider.RecycleItem{}
   240  	subTrees, err := filepath.Glob(trashRoot + "/*")
   241  	if err != nil {
   242  		return nil, err
   243  	}
   244  
   245  	numWorkers := tb.fs.o.MaxConcurrency
   246  	if len(subTrees) < numWorkers {
   247  		numWorkers = len(subTrees)
   248  	}
   249  
   250  	work := make(chan string, len(subTrees))
   251  	results := make(chan *provider.RecycleItem, len(subTrees))
   252  
   253  	g, ctx := errgroup.WithContext(ctx)
   254  
   255  	// Distribute work
   256  	g.Go(func() error {
   257  		defer close(work)
   258  		for _, itemPath := range subTrees {
   259  			select {
   260  			case work <- itemPath:
   261  			case <-ctx.Done():
   262  				return ctx.Err()
   263  			}
   264  		}
   265  		return nil
   266  	})
   267  
   268  	// Spawn workers that'll concurrently work the queue
   269  	for i := 0; i < numWorkers; i++ {
   270  		g.Go(func() error {
   271  			for subTree := range work {
   272  				matches, err := filepath.Glob(subTree + "/*/*/*/*")
   273  				if err != nil {
   274  					return err
   275  				}
   276  
   277  				for _, itemPath := range matches {
   278  					// TODO can we encode this in the path instead of reading the link?
   279  					nodePath, nodeID, timeSuffix, err := readTrashLink(itemPath)
   280  					if err != nil {
   281  						log.Error().Err(err).Str("trashRoot", trashRoot).Str("item", itemPath).Msg("error reading trash link, skipping")
   282  						continue
   283  					}
   284  
   285  					md, err := os.Stat(nodePath)
   286  					if err != nil {
   287  						log.Error().Err(err).Str("trashRoot", trashRoot).Str("item", itemPath).Str("node_path", nodePath).Msg("could not stat trash item, skipping")
   288  						continue
   289  					}
   290  
   291  					attrs, err := tb.fs.lu.MetadataBackend().All(ctx, nodePath)
   292  					if err != nil {
   293  						log.Error().Err(err).Str("trashRoot", trashRoot).Str("item", itemPath).Str("node_path", nodePath).Msg("could not get extended attributes, skipping")
   294  						continue
   295  					}
   296  
   297  					nodeType := tb.fs.lu.TypeFromPath(ctx, nodePath)
   298  					if nodeType == provider.ResourceType_RESOURCE_TYPE_INVALID {
   299  						log.Error().Err(err).Str("trashRoot", trashRoot).Str("item", itemPath).Str("node_path", nodePath).Msg("invalid node type, skipping")
   300  						continue
   301  					}
   302  
   303  					item := &provider.RecycleItem{
   304  						Type: nodeType,
   305  						Size: uint64(md.Size()),
   306  						Key:  nodeID,
   307  					}
   308  					if deletionTime, err := time.Parse(time.RFC3339Nano, timeSuffix); err == nil {
   309  						item.DeletionTime = &types.Timestamp{
   310  							Seconds: uint64(deletionTime.Unix()),
   311  							// TODO nanos
   312  						}
   313  					} else {
   314  						log.Error().Err(err).Str("trashRoot", trashRoot).Str("item", itemPath).Str("spaceid", spaceID).Str("nodeid", nodeID).Str("dtime", timeSuffix).Msg("could not parse time format, ignoring")
   315  					}
   316  
   317  					// lookup origin path in extended attributes
   318  					if attr, ok := attrs[prefixes.TrashOriginAttr]; ok {
   319  						item.Ref = &provider.Reference{Path: string(attr)}
   320  					} else {
   321  						log.Error().Str("trashRoot", trashRoot).Str("item", itemPath).Str("spaceid", spaceID).Str("nodeid", nodeID).Str("dtime", timeSuffix).Msg("could not read origin path")
   322  					}
   323  
   324  					select {
   325  					case results <- item:
   326  					case <-ctx.Done():
   327  						return ctx.Err()
   328  					}
   329  				}
   330  			}
   331  			return nil
   332  		})
   333  	}
   334  
   335  	// Wait for things to settle down, then close results chan
   336  	go func() {
   337  		_ = g.Wait() // error is checked later
   338  		close(results)
   339  	}()
   340  
   341  	// Collect results
   342  	for ri := range results {
   343  		items = append(items, ri)
   344  	}
   345  	return items, nil
   346  }
   347  
   348  // RestoreRecycleItem restores the specified item
   349  func (tb *DecomposedfsTrashbin) RestoreRecycleItem(ctx context.Context, ref *provider.Reference, key, relativePath string, restoreRef *provider.Reference) error {
   350  	_, span := tracer.Start(ctx, "RestoreRecycleItem")
   351  	defer span.End()
   352  	if ref == nil {
   353  		return errtypes.BadRequest("missing reference, needs a space id")
   354  	}
   355  
   356  	var targetNode *node.Node
   357  	if restoreRef != nil {
   358  		tn, err := tb.fs.lu.NodeFromResource(ctx, restoreRef)
   359  		if err != nil {
   360  			return err
   361  		}
   362  
   363  		targetNode = tn
   364  	}
   365  
   366  	rn, parent, restoreFunc, err := tb.fs.tp.RestoreRecycleItemFunc(ctx, ref.ResourceId.SpaceId, key, relativePath, targetNode)
   367  	if err != nil {
   368  		return err
   369  	}
   370  
   371  	// check permissions of deleted node
   372  	rp, err := tb.fs.p.AssembleTrashPermissions(ctx, rn)
   373  	switch {
   374  	case err != nil:
   375  		return err
   376  	case !rp.RestoreRecycleItem:
   377  		if rp.Stat {
   378  			return errtypes.PermissionDenied(key)
   379  		}
   380  		return errtypes.NotFound(key)
   381  	}
   382  
   383  	// Set space owner in context
   384  	storagespace.ContextSendSpaceOwnerID(ctx, rn.SpaceOwnerOrManager(ctx))
   385  
   386  	// check we can write to the parent of the restore reference
   387  	pp, err := tb.fs.p.AssemblePermissions(ctx, parent)
   388  	switch {
   389  	case err != nil:
   390  		return err
   391  	case !pp.InitiateFileUpload:
   392  		// share receiver cannot restore to a shared resource to which she does not have write permissions.
   393  		if rp.Stat {
   394  			return errtypes.PermissionDenied(key)
   395  		}
   396  		return errtypes.NotFound(key)
   397  	}
   398  
   399  	// Run the restore func
   400  	return restoreFunc()
   401  }
   402  
   403  // PurgeRecycleItem purges the specified item, all its children and all their revisions
   404  func (tb *DecomposedfsTrashbin) PurgeRecycleItem(ctx context.Context, ref *provider.Reference, key, relativePath string) error {
   405  	_, span := tracer.Start(ctx, "PurgeRecycleItem")
   406  	defer span.End()
   407  	if ref == nil {
   408  		return errtypes.BadRequest("missing reference, needs a space id")
   409  	}
   410  
   411  	rn, purgeFunc, err := tb.fs.tp.PurgeRecycleItemFunc(ctx, ref.ResourceId.OpaqueId, key, relativePath)
   412  	if err != nil {
   413  		if errors.Is(err, iofs.ErrNotExist) {
   414  			return errtypes.NotFound(key)
   415  		}
   416  		return err
   417  	}
   418  
   419  	// check permissions of deleted node
   420  	rp, err := tb.fs.p.AssembleTrashPermissions(ctx, rn)
   421  	switch {
   422  	case err != nil:
   423  		return err
   424  	case !rp.PurgeRecycle:
   425  		if rp.Stat {
   426  			return errtypes.PermissionDenied(key)
   427  		}
   428  		return errtypes.NotFound(key)
   429  	}
   430  
   431  	// Run the purge func
   432  	return purgeFunc()
   433  }
   434  
   435  // EmptyRecycle empties the trash
   436  func (tb *DecomposedfsTrashbin) EmptyRecycle(ctx context.Context, ref *provider.Reference) error {
   437  	_, span := tracer.Start(ctx, "EmptyRecycle")
   438  	defer span.End()
   439  	if ref == nil || ref.ResourceId == nil || ref.ResourceId.OpaqueId == "" {
   440  		return errtypes.BadRequest("spaceid must be set")
   441  	}
   442  
   443  	items, err := tb.ListRecycle(ctx, ref, "", "")
   444  	if err != nil {
   445  		return err
   446  	}
   447  
   448  	for _, i := range items {
   449  		if err := tb.PurgeRecycleItem(ctx, ref, i.Key, ""); err != nil {
   450  			return err
   451  		}
   452  	}
   453  	// TODO what permission should we check? we could check the root node of the user? or the owner permissions on his home root node?
   454  	// The current impl will wipe your own trash. or when no user provided the trash of 'root'
   455  	return os.RemoveAll(tb.getRecycleRoot(ref.ResourceId.SpaceId))
   456  }
   457  
   458  func (tb *DecomposedfsTrashbin) getRecycleRoot(spaceID string) string {
   459  	return filepath.Join(tb.fs.getSpaceRoot(spaceID), "trash")
   460  }