github.com/cs3org/reva/v2@v2.27.7/pkg/storage/utils/decomposedfs/revisions.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  	"io"
    24  	"os"
    25  	"path/filepath"
    26  	"strings"
    27  	"time"
    28  
    29  	provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
    30  	"github.com/pkg/errors"
    31  	"github.com/rogpeppe/go-internal/lockedfile"
    32  
    33  	"github.com/cs3org/reva/v2/pkg/appctx"
    34  	"github.com/cs3org/reva/v2/pkg/errtypes"
    35  	"github.com/cs3org/reva/v2/pkg/storage/utils/decomposedfs/metadata/prefixes"
    36  	"github.com/cs3org/reva/v2/pkg/storage/utils/decomposedfs/node"
    37  	"github.com/cs3org/reva/v2/pkg/storagespace"
    38  	"github.com/cs3org/reva/v2/pkg/utils"
    39  )
    40  
    41  // Revision entries are stored inside the node folder and start with the same uuid as the current version.
    42  // The `.REV.` indicates it is a revision and what follows is a timestamp, so multiple versions
    43  // can be kept in the same location as the current file content. This prevents new fileuploads
    44  // to trigger cross storage moves when revisions accidentally are stored on another partition,
    45  // because the admin mounted a different partition there.
    46  // We can add a background process to move old revisions to a slower storage
    47  // and replace the revision file with a symbolic link in the future, if necessary.
    48  
    49  // ListRevisions lists the revisions of the given resource
    50  func (fs *Decomposedfs) ListRevisions(ctx context.Context, ref *provider.Reference) (revisions []*provider.FileVersion, err error) {
    51  	_, span := tracer.Start(ctx, "ListRevisions")
    52  	defer span.End()
    53  	var n *node.Node
    54  	if n, err = fs.lu.NodeFromResource(ctx, ref); err != nil {
    55  		return
    56  	}
    57  	if !n.Exists {
    58  		err = errtypes.NotFound(filepath.Join(n.ParentID, n.Name))
    59  		return
    60  	}
    61  
    62  	rp, err := fs.p.AssemblePermissions(ctx, n)
    63  	switch {
    64  	case err != nil:
    65  		return nil, err
    66  	case !rp.ListFileVersions:
    67  		f, _ := storagespace.FormatReference(ref)
    68  		if rp.Stat {
    69  			return nil, errtypes.PermissionDenied(f)
    70  		}
    71  		return nil, errtypes.NotFound(f)
    72  	}
    73  
    74  	revisions = []*provider.FileVersion{}
    75  	np := n.InternalPath()
    76  	if items, err := filepath.Glob(np + node.RevisionIDDelimiter + "*"); err == nil {
    77  		for i := range items {
    78  			if fs.lu.MetadataBackend().IsMetaFile(items[i]) || strings.HasSuffix(items[i], ".mlock") {
    79  				continue
    80  			}
    81  
    82  			if fi, err := os.Stat(items[i]); err == nil {
    83  				parts := strings.SplitN(fi.Name(), node.RevisionIDDelimiter, 2)
    84  				if len(parts) != 2 {
    85  					appctx.GetLogger(ctx).Error().Err(err).Str("name", fi.Name()).Msg("invalid revision name, skipping")
    86  					continue
    87  				}
    88  				mtime := fi.ModTime()
    89  				rev := &provider.FileVersion{
    90  					Key:   n.ID + node.RevisionIDDelimiter + parts[1],
    91  					Mtime: uint64(mtime.Unix()),
    92  				}
    93  				_, blobSize, err := fs.lu.ReadBlobIDAndSizeAttr(ctx, items[i], nil)
    94  				if err != nil {
    95  					appctx.GetLogger(ctx).Error().Err(err).Str("name", fi.Name()).Msg("error reading blobsize xattr, using 0")
    96  				}
    97  				rev.Size = uint64(blobSize)
    98  				etag, err := node.CalculateEtag(n.ID, mtime)
    99  				if err != nil {
   100  					return nil, errors.Wrapf(err, "error calculating etag")
   101  				}
   102  				rev.Etag = etag
   103  				revisions = append(revisions, rev)
   104  			}
   105  		}
   106  	}
   107  	// maybe we need to sort the list by key
   108  	/*
   109  		sort.Slice(revisions, func(i, j int) bool {
   110  			return revisions[i].Key > revisions[j].Key
   111  		})
   112  	*/
   113  
   114  	return
   115  }
   116  
   117  // DownloadRevision returns a reader for the specified revision
   118  // FIXME the CS3 api should explicitly allow initiating revision and trash download, a related issue is https://github.com/cs3org/reva/issues/1813
   119  func (fs *Decomposedfs) DownloadRevision(ctx context.Context, ref *provider.Reference, revisionKey string, openReaderFunc func(md *provider.ResourceInfo) bool) (*provider.ResourceInfo, io.ReadCloser, error) {
   120  	_, span := tracer.Start(ctx, "DownloadRevision")
   121  	defer span.End()
   122  	log := appctx.GetLogger(ctx)
   123  
   124  	// verify revision key format
   125  	kp := strings.SplitN(revisionKey, node.RevisionIDDelimiter, 2)
   126  	if len(kp) != 2 {
   127  		log.Error().Str("revisionKey", revisionKey).Msg("malformed revisionKey")
   128  		return nil, nil, errtypes.NotFound(revisionKey)
   129  	}
   130  	log.Debug().Str("revisionKey", revisionKey).Msg("DownloadRevision")
   131  
   132  	spaceID := ref.ResourceId.SpaceId
   133  	// check if the node is available and has not been deleted
   134  	n, err := node.ReadNode(ctx, fs.lu, spaceID, kp[0], false, nil, false)
   135  	if err != nil {
   136  		return nil, nil, err
   137  	}
   138  	if !n.Exists {
   139  		err = errtypes.NotFound(filepath.Join(n.ParentID, n.Name))
   140  		return nil, nil, err
   141  	}
   142  
   143  	rp, err := fs.p.AssemblePermissions(ctx, n)
   144  	switch {
   145  	case err != nil:
   146  		return nil, nil, err
   147  	case !rp.ListFileVersions || !rp.InitiateFileDownload: // TODO add explicit permission in the CS3 api?
   148  		f, _ := storagespace.FormatReference(ref)
   149  		if rp.Stat {
   150  			return nil, nil, errtypes.PermissionDenied(f)
   151  		}
   152  		return nil, nil, errtypes.NotFound(f)
   153  	}
   154  
   155  	contentPath := fs.lu.InternalPath(spaceID, revisionKey)
   156  
   157  	blobid, blobsize, err := fs.lu.ReadBlobIDAndSizeAttr(ctx, contentPath, nil)
   158  	if err != nil {
   159  		return nil, nil, errors.Wrapf(err, "Decomposedfs: could not read blob id and size for revision '%s' of node '%s'", kp[1], n.ID)
   160  	}
   161  
   162  	revisionNode := node.Node{SpaceID: spaceID, BlobID: blobid, Blobsize: blobsize} // blobsize is needed for the s3ng blobstore
   163  
   164  	ri, err := n.AsResourceInfo(ctx, rp, nil, []string{"size", "mimetype", "etag"}, true)
   165  	if err != nil {
   166  		return nil, nil, err
   167  	}
   168  
   169  	// update resource info with revision data
   170  	mtime, err := time.Parse(time.RFC3339Nano, kp[1])
   171  	if err != nil {
   172  		return nil, nil, errors.Wrapf(err, "Decomposedfs: could not parse mtime for revision '%s' of node '%s'", kp[1], n.ID)
   173  	}
   174  	ri.Size = uint64(blobsize)
   175  	ri.Mtime = utils.TimeToTS(mtime)
   176  	ri.Etag, err = node.CalculateEtag(n.ID, mtime)
   177  	if err != nil {
   178  		return nil, nil, errors.Wrapf(err, "error calculating etag for revision '%s' of node '%s'", kp[1], n.ID)
   179  	}
   180  
   181  	var reader io.ReadCloser
   182  	if openReaderFunc(ri) {
   183  		reader, err = fs.tp.ReadBlob(&revisionNode)
   184  		if err != nil {
   185  			return nil, nil, errors.Wrapf(err, "Decomposedfs: could not download blob of revision '%s' for node '%s'", n.ID, revisionKey)
   186  		}
   187  	}
   188  	return ri, reader, nil
   189  }
   190  
   191  // RestoreRevision restores the specified revision of the resource
   192  func (fs *Decomposedfs) RestoreRevision(ctx context.Context, ref *provider.Reference, revisionKey string) (returnErr error) {
   193  	_, span := tracer.Start(ctx, "RestoreRevision")
   194  	defer span.End()
   195  	log := appctx.GetLogger(ctx)
   196  
   197  	// verify revision key format
   198  	kp := strings.SplitN(revisionKey, node.RevisionIDDelimiter, 2)
   199  	if len(kp) != 2 {
   200  		log.Error().Str("revisionKey", revisionKey).Msg("malformed revisionKey")
   201  		return errtypes.NotFound(revisionKey)
   202  	}
   203  
   204  	spaceID := ref.ResourceId.SpaceId
   205  	// check if the node is available and has not been deleted
   206  	n, err := node.ReadNode(ctx, fs.lu, spaceID, kp[0], false, nil, false)
   207  	if err != nil {
   208  		return err
   209  	}
   210  	if !n.Exists {
   211  		err = errtypes.NotFound(filepath.Join(n.ParentID, n.Name))
   212  		return err
   213  	}
   214  
   215  	rp, err := fs.p.AssemblePermissions(ctx, n)
   216  	switch {
   217  	case err != nil:
   218  		return err
   219  	case !rp.RestoreFileVersion:
   220  		f, _ := storagespace.FormatReference(ref)
   221  		if rp.Stat {
   222  			return errtypes.PermissionDenied(f)
   223  		}
   224  		return errtypes.NotFound(f)
   225  	}
   226  
   227  	// Set space owner in context
   228  	storagespace.ContextSendSpaceOwnerID(ctx, n.SpaceOwnerOrManager(ctx))
   229  
   230  	// check lock
   231  	if err := n.CheckLock(ctx); err != nil {
   232  		return err
   233  	}
   234  
   235  	// write lock node before copying metadata
   236  	f, err := lockedfile.OpenFile(fs.lu.MetadataBackend().LockfilePath(n.InternalPath()), os.O_RDWR|os.O_CREATE, 0600)
   237  	if err != nil {
   238  		return err
   239  	}
   240  	defer func() {
   241  		_ = f.Close()
   242  		_ = os.Remove(fs.lu.MetadataBackend().LockfilePath(n.InternalPath()))
   243  	}()
   244  
   245  	// move current version to new revision
   246  	nodePath := fs.lu.InternalPath(spaceID, kp[0])
   247  	mtime, err := n.GetMTime(ctx)
   248  	if err != nil {
   249  		log.Error().Err(err).Interface("ref", ref).Str("originalnode", kp[0]).Str("revisionKey", revisionKey).Msg("cannot read mtime")
   250  		return err
   251  	}
   252  
   253  	// revisions are stored alongside the actual file, so a rename can be efficient and does not cross storage / partition boundaries
   254  	newRevisionPath := fs.lu.InternalPath(spaceID, kp[0]+node.RevisionIDDelimiter+mtime.UTC().Format(time.RFC3339Nano))
   255  
   256  	// touch new revision
   257  	if _, err := os.Create(newRevisionPath); err != nil {
   258  		return err
   259  	}
   260  	defer func() {
   261  		if returnErr != nil {
   262  			if err := os.Remove(newRevisionPath); err != nil {
   263  				log.Error().Err(err).Str("revision", filepath.Base(newRevisionPath)).Msg("could not clean up revision node")
   264  			}
   265  			if err := fs.lu.MetadataBackend().Purge(ctx, newRevisionPath); err != nil {
   266  				log.Error().Err(err).Str("revision", filepath.Base(newRevisionPath)).Msg("could not clean up revision node")
   267  			}
   268  		}
   269  	}()
   270  
   271  	// copy blob metadata from node to new revision node
   272  	err = fs.lu.CopyMetadataWithSourceLock(ctx, nodePath, newRevisionPath, func(attributeName string, value []byte) (newValue []byte, copy bool) {
   273  		return value, strings.HasPrefix(attributeName, prefixes.ChecksumPrefix) || // for checksums
   274  			attributeName == prefixes.TypeAttr ||
   275  			attributeName == prefixes.BlobIDAttr ||
   276  			attributeName == prefixes.BlobsizeAttr ||
   277  			attributeName == prefixes.MTimeAttr // FIXME somewhere I mix up the revision time and the mtime, causing the restore to overwrite the other existing revisien
   278  	}, f, true)
   279  	if err != nil {
   280  		return errtypes.InternalError("failed to copy blob xattrs to version node: " + err.Error())
   281  	}
   282  
   283  	// remember mtime from node as new revision mtime
   284  	if err = os.Chtimes(newRevisionPath, mtime, mtime); err != nil {
   285  		return errtypes.InternalError("failed to change mtime of version node")
   286  	}
   287  
   288  	// update blob id in node
   289  
   290  	// copy blob metadata from restored revision to node
   291  	restoredRevisionPath := fs.lu.InternalPath(spaceID, revisionKey)
   292  	err = fs.lu.CopyMetadata(ctx, restoredRevisionPath, nodePath, func(attributeName string, value []byte) (newValue []byte, copy bool) {
   293  		return value, strings.HasPrefix(attributeName, prefixes.ChecksumPrefix) ||
   294  			attributeName == prefixes.TypeAttr ||
   295  			attributeName == prefixes.BlobIDAttr ||
   296  			attributeName == prefixes.BlobsizeAttr
   297  	}, false)
   298  	if err != nil {
   299  		return errtypes.InternalError("failed to copy blob xattrs to old revision to node: " + err.Error())
   300  	}
   301  	// always set the node mtime to the current time
   302  	err = fs.lu.MetadataBackend().SetMultiple(ctx, nodePath,
   303  		map[string][]byte{
   304  			prefixes.MTimeAttr: []byte(time.Now().UTC().Format(time.RFC3339Nano)),
   305  		},
   306  		false)
   307  	if err != nil {
   308  		return errtypes.InternalError("failed to set mtime attribute on node: " + err.Error())
   309  	}
   310  
   311  	revisionSize, err := fs.lu.MetadataBackend().GetInt64(ctx, restoredRevisionPath, prefixes.BlobsizeAttr)
   312  	if err != nil {
   313  		return errtypes.InternalError("failed to read blob size xattr from old revision")
   314  	}
   315  
   316  	// drop old revision
   317  	if err := os.Remove(restoredRevisionPath); err != nil {
   318  		log.Warn().Err(err).Interface("ref", ref).Str("originalnode", kp[0]).Str("revisionKey", revisionKey).Msg("could not delete old revision, continuing")
   319  	}
   320  	if err := os.Remove(fs.lu.MetadataBackend().MetadataPath(restoredRevisionPath)); err != nil {
   321  		log.Warn().Err(err).Interface("ref", ref).Str("originalnode", kp[0]).Str("revisionKey", revisionKey).Msg("could not delete old revision metadata, continuing")
   322  	}
   323  	if err := os.Remove(fs.lu.MetadataBackend().LockfilePath(restoredRevisionPath)); err != nil {
   324  		log.Warn().Err(err).Interface("ref", ref).Str("originalnode", kp[0]).Str("revisionKey", revisionKey).Msg("could not delete old revision metadata lockfile, continuing")
   325  	}
   326  	if err := fs.lu.MetadataBackend().Purge(ctx, restoredRevisionPath); err != nil {
   327  		log.Warn().Err(err).Interface("ref", ref).Str("originalnode", kp[0]).Str("revisionKey", revisionKey).Msg("could not purge old revision from cache, continuing")
   328  	}
   329  
   330  	// revision 5, current 10 (restore a smaller blob) -> 5-10 = -5
   331  	// revision 10, current 5 (restore a bigger blob) -> 10-5 = +5
   332  	sizeDiff := revisionSize - n.Blobsize
   333  
   334  	return fs.tp.Propagate(ctx, n, sizeDiff)
   335  }
   336  
   337  // DeleteRevision deletes the specified revision of the resource
   338  func (fs *Decomposedfs) DeleteRevision(ctx context.Context, ref *provider.Reference, revisionKey string) error {
   339  	_, span := tracer.Start(ctx, "DeleteRevision")
   340  	defer span.End()
   341  	n, err := fs.getRevisionNode(ctx, ref, revisionKey, func(rp *provider.ResourcePermissions) bool {
   342  		return rp.RestoreFileVersion
   343  	})
   344  	if err != nil {
   345  		return err
   346  	}
   347  
   348  	if err := os.RemoveAll(fs.lu.InternalPath(n.SpaceID, revisionKey)); err != nil {
   349  		return err
   350  	}
   351  
   352  	return fs.tp.DeleteBlob(n)
   353  }
   354  
   355  func (fs *Decomposedfs) getRevisionNode(ctx context.Context, ref *provider.Reference, revisionKey string, hasPermission func(*provider.ResourcePermissions) bool) (*node.Node, error) {
   356  	_, span := tracer.Start(ctx, "getRevisionNode")
   357  	defer span.End()
   358  	log := appctx.GetLogger(ctx)
   359  
   360  	// verify revision key format
   361  	kp := strings.SplitN(revisionKey, node.RevisionIDDelimiter, 2)
   362  	if len(kp) != 2 {
   363  		log.Error().Str("revisionKey", revisionKey).Msg("malformed revisionKey")
   364  		return nil, errtypes.NotFound(revisionKey)
   365  	}
   366  	log.Debug().Str("revisionKey", revisionKey).Msg("DownloadRevision")
   367  
   368  	spaceID := ref.ResourceId.SpaceId
   369  	// check if the node is available and has not been deleted
   370  	n, err := node.ReadNode(ctx, fs.lu, spaceID, kp[0], false, nil, false)
   371  	if err != nil {
   372  		return nil, err
   373  	}
   374  	if !n.Exists {
   375  		err = errtypes.NotFound(filepath.Join(n.ParentID, n.Name))
   376  		return nil, err
   377  	}
   378  
   379  	p, err := fs.p.AssemblePermissions(ctx, n)
   380  	switch {
   381  	case err != nil:
   382  		return nil, err
   383  	case !hasPermission(p):
   384  		return nil, errtypes.PermissionDenied(filepath.Join(n.ParentID, n.Name))
   385  	}
   386  
   387  	// Set space owner in context
   388  	storagespace.ContextSendSpaceOwnerID(ctx, n.SpaceOwnerOrManager(ctx))
   389  
   390  	return n, nil
   391  }