github.com/cs3org/reva/v2@v2.27.7/pkg/storage/fs/posix/trashbin/trashbin.go (about)

     1  // Copyright 2018-2024 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 trashbin
    20  
    21  import (
    22  	"context"
    23  	"fmt"
    24  	"os"
    25  	"path/filepath"
    26  	"strings"
    27  	"time"
    28  
    29  	"github.com/google/uuid"
    30  	"github.com/rs/zerolog"
    31  
    32  	provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
    33  	typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
    34  	"github.com/cs3org/reva/v2/pkg/storage"
    35  	"github.com/cs3org/reva/v2/pkg/storage/fs/posix/lookup"
    36  	"github.com/cs3org/reva/v2/pkg/storage/fs/posix/options"
    37  	"github.com/cs3org/reva/v2/pkg/storage/utils/decomposedfs/metadata/prefixes"
    38  	"github.com/cs3org/reva/v2/pkg/storage/utils/decomposedfs/node"
    39  	"github.com/cs3org/reva/v2/pkg/utils"
    40  )
    41  
    42  type Trashbin struct {
    43  	fs  storage.FS
    44  	o   *options.Options
    45  	lu  *lookup.Lookup
    46  	log *zerolog.Logger
    47  }
    48  
    49  const (
    50  	trashHeader = `[Trash Info]`
    51  	timeFormat  = "2006-01-02T15:04:05"
    52  )
    53  
    54  // New returns a new Trashbin
    55  func New(o *options.Options, lu *lookup.Lookup, log *zerolog.Logger) (*Trashbin, error) {
    56  	return &Trashbin{
    57  		o:   o,
    58  		lu:  lu,
    59  		log: log,
    60  	}, nil
    61  }
    62  
    63  func (tb *Trashbin) writeInfoFile(trashPath, id, path string) error {
    64  	c := trashHeader
    65  	c += "\nPath=" + path
    66  	c += "\nDeletionDate=" + time.Now().Format(timeFormat)
    67  
    68  	return os.WriteFile(filepath.Join(trashPath, "info", id+".trashinfo"), []byte(c), 0644)
    69  }
    70  
    71  func (tb *Trashbin) readInfoFile(trashPath, id string) (string, *typesv1beta1.Timestamp, error) {
    72  	c, err := os.ReadFile(filepath.Join(trashPath, "info", id+".trashinfo"))
    73  	if err != nil {
    74  		return "", nil, err
    75  	}
    76  
    77  	var (
    78  		path string
    79  		ts   *typesv1beta1.Timestamp
    80  	)
    81  
    82  	for _, line := range strings.Split(string(c), "\n") {
    83  		if strings.HasPrefix(line, "DeletionDate=") {
    84  			t, err := time.ParseInLocation(timeFormat, strings.TrimSpace(strings.TrimPrefix(line, "DeletionDate=")), time.Local)
    85  			if err != nil {
    86  				return "", nil, err
    87  			}
    88  			ts = utils.TimeToTS(t)
    89  		}
    90  		if strings.HasPrefix(line, "Path=") {
    91  			path = strings.TrimPrefix(line, "Path=")
    92  		}
    93  	}
    94  
    95  	return path, ts, nil
    96  }
    97  
    98  // Setup the trashbin
    99  func (tb *Trashbin) Setup(fs storage.FS) error {
   100  	if tb.fs != nil {
   101  		return nil
   102  	}
   103  
   104  	tb.fs = fs
   105  	return nil
   106  }
   107  
   108  func trashRootForNode(n *node.Node) string {
   109  	return filepath.Join(n.SpaceRoot.InternalPath(), ".Trash")
   110  }
   111  
   112  func (tb *Trashbin) MoveToTrash(ctx context.Context, n *node.Node, path string) error {
   113  	key := uuid.New().String()
   114  	trashPath := trashRootForNode(n)
   115  
   116  	err := os.MkdirAll(filepath.Join(trashPath, "info"), 0755)
   117  	if err != nil {
   118  		return err
   119  	}
   120  	err = os.MkdirAll(filepath.Join(trashPath, "files"), 0755)
   121  	if err != nil {
   122  		return err
   123  	}
   124  
   125  	relPath := strings.TrimPrefix(path, n.SpaceRoot.InternalPath())
   126  	relPath = strings.TrimPrefix(relPath, "/")
   127  	err = tb.writeInfoFile(trashPath, key, relPath)
   128  	if err != nil {
   129  		return err
   130  	}
   131  
   132  	// purge metadata
   133  	if err = tb.lu.IDCache.DeleteByPath(ctx, path); err != nil {
   134  		return err
   135  	}
   136  
   137  	itemTrashPath := filepath.Join(trashPath, "files", key+".trashitem")
   138  	err = tb.lu.MetadataBackend().Rename(path, itemTrashPath)
   139  	if err != nil {
   140  		return err
   141  	}
   142  
   143  	return os.Rename(path, itemTrashPath)
   144  }
   145  
   146  // ListRecycle returns the list of available recycle items
   147  // ref -> the space (= resourceid), key -> deleted node id, relativePath = relative to key
   148  func (tb *Trashbin) ListRecycle(ctx context.Context, ref *provider.Reference, key, relativePath string) ([]*provider.RecycleItem, error) {
   149  	n, err := tb.lu.NodeFromResource(ctx, ref)
   150  	if err != nil {
   151  		return nil, err
   152  	}
   153  
   154  	trashRoot := trashRootForNode(n)
   155  	base := filepath.Join(trashRoot, "files")
   156  
   157  	var originalPath string
   158  	var ts *typesv1beta1.Timestamp
   159  	if key != "" {
   160  		// this is listing a specific item/folder
   161  		base = filepath.Join(base, key+".trashitem", relativePath)
   162  		originalPath, ts, err = tb.readInfoFile(trashRoot, key)
   163  		originalPath = filepath.Join(originalPath, relativePath)
   164  		if err != nil {
   165  			return nil, err
   166  		}
   167  	}
   168  
   169  	items := []*provider.RecycleItem{}
   170  	entries, err := os.ReadDir(filepath.Clean(base))
   171  	if err != nil {
   172  		switch err.(type) {
   173  		case *os.PathError:
   174  			return items, nil
   175  		default:
   176  			return nil, err
   177  		}
   178  	}
   179  
   180  	for _, entry := range entries {
   181  		var fi os.FileInfo
   182  		var entryOriginalPath string
   183  		var entryKey string
   184  		if strings.HasSuffix(entry.Name(), ".trashitem") {
   185  			entryKey = strings.TrimSuffix(entry.Name(), ".trashitem")
   186  			entryOriginalPath, ts, err = tb.readInfoFile(trashRoot, entryKey)
   187  			if err != nil {
   188  				continue
   189  			}
   190  
   191  			fi, err = entry.Info()
   192  			if err != nil {
   193  				continue
   194  			}
   195  		} else {
   196  			fi, err = os.Stat(filepath.Join(base, entry.Name()))
   197  			entryKey = entry.Name()
   198  			entryOriginalPath = filepath.Join(originalPath, entry.Name())
   199  			if err != nil {
   200  				continue
   201  			}
   202  		}
   203  
   204  		item := &provider.RecycleItem{
   205  			Key:  filepath.Join(key, relativePath, entryKey),
   206  			Size: uint64(fi.Size()),
   207  			Ref: &provider.Reference{
   208  				ResourceId: &provider.ResourceId{
   209  					SpaceId:  ref.GetResourceId().GetSpaceId(),
   210  					OpaqueId: ref.GetResourceId().GetSpaceId(),
   211  				},
   212  				Path: entryOriginalPath,
   213  			},
   214  			DeletionTime: ts,
   215  		}
   216  		if entry.IsDir() {
   217  			item.Type = provider.ResourceType_RESOURCE_TYPE_CONTAINER
   218  		} else {
   219  			item.Type = provider.ResourceType_RESOURCE_TYPE_FILE
   220  		}
   221  
   222  		items = append(items, item)
   223  	}
   224  
   225  	return items, nil
   226  }
   227  
   228  // RestoreRecycleItem restores the specified item
   229  func (tb *Trashbin) RestoreRecycleItem(ctx context.Context, ref *provider.Reference, key, relativePath string, restoreRef *provider.Reference) error {
   230  	n, err := tb.lu.NodeFromResource(ctx, ref)
   231  	if err != nil {
   232  		return err
   233  	}
   234  
   235  	trashRoot := trashRootForNode(n)
   236  	trashPath := filepath.Clean(filepath.Join(trashRoot, "files", key+".trashitem", relativePath))
   237  
   238  	restoreBaseNode, err := tb.lu.NodeFromID(ctx, restoreRef.GetResourceId())
   239  	if err != nil {
   240  		return err
   241  	}
   242  	restorePath := filepath.Join(restoreBaseNode.InternalPath(), restoreRef.GetPath())
   243  
   244  	id, err := tb.lu.MetadataBackend().Get(ctx, trashPath, prefixes.IDAttr)
   245  	if err != nil {
   246  		return err
   247  	}
   248  
   249  	// update parent id in case it was restored to a different location
   250  	parentID, err := tb.lu.MetadataBackend().Get(ctx, filepath.Dir(restorePath), prefixes.IDAttr)
   251  	if err != nil {
   252  		return err
   253  	}
   254  	if len(parentID) == 0 {
   255  		return fmt.Errorf("trashbin: parent id not found for %s", restorePath)
   256  	}
   257  
   258  	err = tb.lu.MetadataBackend().Set(ctx, trashPath, prefixes.ParentidAttr, parentID)
   259  	if err != nil {
   260  		return err
   261  	}
   262  
   263  	// restore the item
   264  	err = os.Rename(trashPath, restorePath)
   265  	if err != nil {
   266  		return err
   267  	}
   268  	if err := tb.lu.CacheID(ctx, n.SpaceID, string(id), restorePath); err != nil {
   269  		tb.log.Error().Err(err).Str("spaceID", n.SpaceID).Str("id", string(id)).Str("path", restorePath).Msg("trashbin: error caching id")
   270  	}
   271  
   272  	// cleanup trash info
   273  	if relativePath == "." || relativePath == "/" {
   274  		return os.Remove(filepath.Join(trashRoot, "info", key+".trashinfo"))
   275  	} else {
   276  		return nil
   277  	}
   278  }
   279  
   280  // PurgeRecycleItem purges the specified item, all its children and all their revisions
   281  func (tb *Trashbin) PurgeRecycleItem(ctx context.Context, ref *provider.Reference, key, relativePath string) error {
   282  	n, err := tb.lu.NodeFromResource(ctx, ref)
   283  	if err != nil {
   284  		return err
   285  	}
   286  
   287  	trashRoot := trashRootForNode(n)
   288  	err = os.RemoveAll(filepath.Clean(filepath.Join(trashRoot, "files", key+".trashitem", relativePath)))
   289  	if err != nil {
   290  		return err
   291  	}
   292  
   293  	cleanPath := filepath.Clean(relativePath)
   294  	if cleanPath == "." || cleanPath == "/" {
   295  		return os.Remove(filepath.Join(trashRoot, "info", key+".trashinfo"))
   296  	}
   297  	return nil
   298  }
   299  
   300  // EmptyRecycle empties the trash
   301  func (tb *Trashbin) EmptyRecycle(ctx context.Context, ref *provider.Reference) error {
   302  	n, err := tb.lu.NodeFromResource(ctx, ref)
   303  	if err != nil {
   304  		return err
   305  	}
   306  
   307  	trashRoot := trashRootForNode(n)
   308  	err = os.RemoveAll(filepath.Clean(filepath.Join(trashRoot, "files")))
   309  	if err != nil {
   310  		return err
   311  	}
   312  	return os.RemoveAll(filepath.Clean(filepath.Join(trashRoot, "info")))
   313  }