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

     1  // Copyright 2018-2022 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 upload
    20  
    21  import (
    22  	"context"
    23  	"encoding/json"
    24  	"fmt"
    25  	iofs "io/fs"
    26  	"os"
    27  	"path/filepath"
    28  	"regexp"
    29  	"strconv"
    30  	"strings"
    31  	"syscall"
    32  	"time"
    33  
    34  	provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
    35  	"github.com/cs3org/reva/v2/pkg/appctx"
    36  	"github.com/cs3org/reva/v2/pkg/errtypes"
    37  	"github.com/cs3org/reva/v2/pkg/events"
    38  	"github.com/cs3org/reva/v2/pkg/storage"
    39  	"github.com/cs3org/reva/v2/pkg/storage/utils/decomposedfs/aspects"
    40  	"github.com/cs3org/reva/v2/pkg/storage/utils/decomposedfs/metadata"
    41  	"github.com/cs3org/reva/v2/pkg/storage/utils/decomposedfs/metadata/prefixes"
    42  	"github.com/cs3org/reva/v2/pkg/storage/utils/decomposedfs/node"
    43  	"github.com/cs3org/reva/v2/pkg/storage/utils/decomposedfs/options"
    44  	"github.com/cs3org/reva/v2/pkg/storage/utils/decomposedfs/usermapper"
    45  	"github.com/google/uuid"
    46  	"github.com/pkg/errors"
    47  	"github.com/rogpeppe/go-internal/lockedfile"
    48  	"github.com/rs/zerolog"
    49  	tusd "github.com/tus/tusd/v2/pkg/handler"
    50  )
    51  
    52  var _idRegexp = regexp.MustCompile(".*/([^/]+).info")
    53  
    54  // PermissionsChecker defines an interface for checking permissions on a Node
    55  type PermissionsChecker interface {
    56  	AssemblePermissions(ctx context.Context, n *node.Node) (ap provider.ResourcePermissions, err error)
    57  }
    58  
    59  // OcisStore manages upload sessions
    60  type OcisStore struct {
    61  	fs                storage.FS
    62  	lu                node.PathLookup
    63  	tp                node.Tree
    64  	um                usermapper.Mapper
    65  	root              string
    66  	pub               events.Publisher
    67  	async             bool
    68  	tknopts           options.TokenOptions
    69  	disableVersioning bool
    70  	log               *zerolog.Logger
    71  }
    72  
    73  // NewSessionStore returns a new OcisStore
    74  func NewSessionStore(fs storage.FS, aspects aspects.Aspects, root string, async bool, tknopts options.TokenOptions, log *zerolog.Logger) *OcisStore {
    75  	return &OcisStore{
    76  		fs:                fs,
    77  		lu:                aspects.Lookup,
    78  		tp:                aspects.Tree,
    79  		root:              root,
    80  		pub:               aspects.EventStream,
    81  		async:             async,
    82  		tknopts:           tknopts,
    83  		disableVersioning: aspects.DisableVersioning,
    84  		um:                aspects.UserMapper,
    85  		log:               log,
    86  	}
    87  }
    88  
    89  // New returns a new upload session
    90  func (store OcisStore) New(ctx context.Context) *OcisSession {
    91  	return &OcisSession{
    92  		store: store,
    93  		info: tusd.FileInfo{
    94  			ID: uuid.New().String(),
    95  			Storage: map[string]string{
    96  				"Type": "OCISStore",
    97  			},
    98  			MetaData: tusd.MetaData{},
    99  		},
   100  	}
   101  }
   102  
   103  // List lists all upload sessions
   104  func (store OcisStore) List(ctx context.Context) ([]*OcisSession, error) {
   105  	uploads := []*OcisSession{}
   106  	infoFiles, err := filepath.Glob(filepath.Join(store.root, "uploads", "*.info"))
   107  	if err != nil {
   108  		return nil, err
   109  	}
   110  
   111  	for _, info := range infoFiles {
   112  		id := strings.TrimSuffix(filepath.Base(info), filepath.Ext(info))
   113  		progress, err := store.Get(ctx, id)
   114  		if err != nil {
   115  			appctx.GetLogger(ctx).Error().Interface("path", info).Msg("Decomposedfs: could not getUploadSession")
   116  			continue
   117  		}
   118  
   119  		uploads = append(uploads, progress)
   120  	}
   121  	return uploads, nil
   122  }
   123  
   124  // Get returns the upload session for the given upload id
   125  func (store OcisStore) Get(ctx context.Context, id string) (*OcisSession, error) {
   126  	sessionPath := sessionPath(store.root, id)
   127  	match := _idRegexp.FindStringSubmatch(sessionPath)
   128  	if match == nil || len(match) < 2 {
   129  		return nil, fmt.Errorf("invalid upload path")
   130  	}
   131  
   132  	session := OcisSession{
   133  		store: store,
   134  		info:  tusd.FileInfo{},
   135  	}
   136  	data, err := os.ReadFile(sessionPath)
   137  	if err != nil {
   138  		// handle stale NFS file handles that can occur when the file is deleted betwenn the ATTR and FOPEN call of os.ReadFile
   139  		if pathErr, ok := err.(*os.PathError); ok && pathErr.Err == syscall.ESTALE {
   140  			appctx.GetLogger(ctx).Info().Str("session", id).Err(err).Msg("treating stale file handle as not found")
   141  			err = tusd.ErrNotFound
   142  		}
   143  		if errors.Is(err, iofs.ErrNotExist) {
   144  			// Interpret os.ErrNotExist as 404 Not Found
   145  			err = tusd.ErrNotFound
   146  		}
   147  		return nil, err
   148  	}
   149  
   150  	if err := json.Unmarshal(data, &session.info); err != nil {
   151  		return nil, err
   152  	}
   153  
   154  	stat, err := os.Stat(session.binPath())
   155  	if err != nil {
   156  		if os.IsNotExist(err) {
   157  			// Interpret os.ErrNotExist as 404 Not Found
   158  			err = tusd.ErrNotFound
   159  		}
   160  		return nil, err
   161  	}
   162  
   163  	session.info.Offset = stat.Size()
   164  
   165  	return &session, nil
   166  }
   167  
   168  // Session is the interface used by the Cleanup call
   169  type Session interface {
   170  	ID() string
   171  	Node(ctx context.Context) (*node.Node, error)
   172  	Context(ctx context.Context) context.Context
   173  	Cleanup(revertNodeMetadata, cleanBin, cleanInfo bool)
   174  }
   175  
   176  // Cleanup cleans upload metadata, binary data and processing status as necessary
   177  func (store OcisStore) Cleanup(ctx context.Context, session Session, revertNodeMetadata, keepUpload, unmarkPostprocessing bool) {
   178  	ctx, span := tracer.Start(session.Context(ctx), "Cleanup")
   179  	defer span.End()
   180  	session.Cleanup(revertNodeMetadata, !keepUpload, !keepUpload)
   181  
   182  	// unset processing status
   183  	if unmarkPostprocessing {
   184  		n, err := session.Node(ctx)
   185  		if err != nil {
   186  			appctx.GetLogger(ctx).Info().Str("session", session.ID()).Err(err).Msg("could not read node")
   187  			return
   188  		}
   189  		// FIXME: after cleanup the node might already be deleted ...
   190  		if n != nil { // node can be nil when there was an error before it was created (eg. checksum-mismatch)
   191  			if err := n.UnmarkProcessing(ctx, session.ID()); err != nil {
   192  				appctx.GetLogger(ctx).Info().Str("path", n.InternalPath()).Err(err).Msg("unmarking processing failed")
   193  			}
   194  		}
   195  	}
   196  }
   197  
   198  // CreateNodeForUpload will create the target node for the Upload
   199  // TODO move this to the node package as NodeFromUpload?
   200  // should we in InitiateUpload create the node first? and then the upload?
   201  func (store OcisStore) CreateNodeForUpload(ctx context.Context, session *OcisSession, initAttrs node.Attributes) (*node.Node, error) {
   202  	ctx, span := tracer.Start(session.Context(ctx), "CreateNodeForUpload")
   203  	defer span.End()
   204  	n := node.New(
   205  		session.SpaceID(),
   206  		session.NodeID(),
   207  		session.NodeParentID(),
   208  		session.Filename(),
   209  		session.Size(),
   210  		session.ID(),
   211  		provider.ResourceType_RESOURCE_TYPE_FILE,
   212  		nil,
   213  		store.lu,
   214  	)
   215  	var err error
   216  	n.SpaceRoot, err = node.ReadNode(ctx, store.lu, session.SpaceID(), session.SpaceID(), false, nil, false)
   217  	if err != nil {
   218  		return nil, err
   219  	}
   220  
   221  	// check lock
   222  	if err := n.CheckLock(ctx); err != nil {
   223  		return nil, err
   224  	}
   225  
   226  	var unlock metadata.UnlockFunc
   227  	if session.NodeExists() { // TODO this is wrong. The node should be created when the upload starts, the revisions should be created independently of the node
   228  		// we do not need to propagate a change when a node is created, only when the upload is ready.
   229  		// that still creates problems for desktop clients because if another change causes propagation it will detects an empty file
   230  		// so the first upload has to point to the first revision with the expected size. The file cannot be downloaded, but it can be overwritten (which will create a new revision and make the node reflect the latest revision)
   231  		// any finished postprocessing will not affect the node metadata.
   232  		// *thinking* but then initializing an upload will lock the file until the upload has finished. That sucks.
   233  		// so we have to check if the node has been created meanwhile (well, only in case the upload does not know the nodeid ... or the NodeExists array that is checked by session.NodeExists())
   234  		// FIXME look at the disk again to see if the file has been created in between, or just try initializing a new node and do the update existing node as a fallback. <- the latter!
   235  
   236  		unlock, err = store.updateExistingNode(ctx, session, n, session.SpaceID(), uint64(session.Size()))
   237  		if err != nil {
   238  			appctx.GetLogger(ctx).Error().Err(err).Msg("failed to update existing node")
   239  		}
   240  	} else {
   241  		if c, ok := store.lu.(node.IDCacher); ok {
   242  			err := c.CacheID(ctx, n.SpaceID, n.ID, filepath.Join(n.ParentPath(), n.Name))
   243  			if err != nil {
   244  				appctx.GetLogger(ctx).Error().Err(err).Msg("failed to cache id")
   245  			}
   246  		}
   247  
   248  		unlock, err = store.tp.InitNewNode(ctx, n, uint64(session.Size()))
   249  		if err != nil {
   250  			appctx.GetLogger(ctx).Error().Str("path", n.InternalPath()).Err(err).Msg("failed to init new node")
   251  		}
   252  		session.info.MetaData["sizeDiff"] = strconv.FormatInt(session.Size(), 10)
   253  	}
   254  	defer func() {
   255  		if unlock == nil {
   256  			appctx.GetLogger(ctx).Info().Msg("did not get a unlockfunc, not unlocking")
   257  			return
   258  		}
   259  
   260  		if err := unlock(); err != nil {
   261  			appctx.GetLogger(ctx).Error().Err(err).Str("nodeid", n.ID).Str("parentid", n.ParentID).Msg("could not close lock")
   262  		}
   263  	}()
   264  	if err != nil {
   265  		return nil, err
   266  	}
   267  
   268  	// overwrite technical information
   269  	initAttrs.SetString(prefixes.IDAttr, n.ID)
   270  	initAttrs.SetInt64(prefixes.TypeAttr, int64(provider.ResourceType_RESOURCE_TYPE_FILE))
   271  	initAttrs.SetString(prefixes.ParentidAttr, n.ParentID)
   272  	initAttrs.SetString(prefixes.NameAttr, n.Name)
   273  	initAttrs.SetString(prefixes.BlobIDAttr, n.BlobID)
   274  	initAttrs.SetInt64(prefixes.BlobsizeAttr, n.Blobsize)
   275  	initAttrs.SetString(prefixes.StatusPrefix, node.ProcessingStatus+session.ID())
   276  
   277  	// set mtime on the new node
   278  	mtime := time.Now()
   279  	if !session.MTime().IsZero() {
   280  		// overwrite mtime if requested
   281  		mtime = session.MTime()
   282  	}
   283  	err = store.lu.TimeManager().OverrideMtime(ctx, n, &initAttrs, mtime)
   284  	if err != nil {
   285  		return nil, errors.Wrap(err, "Decomposedfs: failed to set the mtime")
   286  	}
   287  
   288  	// update node metadata with new blobid etc
   289  	err = n.SetXattrsWithContext(ctx, initAttrs, false)
   290  	if err != nil {
   291  		return nil, errors.Wrap(err, "Decomposedfs: could not write metadata")
   292  	}
   293  
   294  	err = store.um.RunInBaseScope(func() error {
   295  		return session.Persist(ctx)
   296  	})
   297  	if err != nil {
   298  		return nil, err
   299  	}
   300  
   301  	return n, nil
   302  }
   303  
   304  func (store OcisStore) updateExistingNode(ctx context.Context, session *OcisSession, n *node.Node, spaceID string, fsize uint64) (metadata.UnlockFunc, error) {
   305  	_, span := tracer.Start(ctx, "updateExistingNode")
   306  	defer span.End()
   307  	targetPath := n.InternalPath()
   308  
   309  	// write lock existing node before reading any metadata
   310  	f, err := lockedfile.OpenFile(store.lu.MetadataBackend().LockfilePath(targetPath), os.O_RDWR|os.O_CREATE, 0600)
   311  	if err != nil {
   312  		return nil, err
   313  	}
   314  
   315  	unlock := func() error {
   316  		// NOTE: to prevent stale NFS file handles do not remove lock file!
   317  		return f.Close()
   318  	}
   319  
   320  	old, _ := node.ReadNode(ctx, store.lu, spaceID, n.ID, false, nil, false)
   321  	if _, err := node.CheckQuota(ctx, n.SpaceRoot, true, uint64(old.Blobsize), fsize); err != nil {
   322  		return unlock, err
   323  	}
   324  
   325  	oldNodeMtime, err := old.GetMTime(ctx)
   326  	if err != nil {
   327  		return unlock, err
   328  	}
   329  	oldNodeEtag, err := node.CalculateEtag(old.ID, oldNodeMtime)
   330  	if err != nil {
   331  		return unlock, err
   332  	}
   333  
   334  	// When the if-match header was set we need to check if the
   335  	// etag still matches before finishing the upload.
   336  	if session.HeaderIfMatch() != "" && session.HeaderIfMatch() != oldNodeEtag {
   337  		return unlock, errtypes.Aborted("etag mismatch")
   338  	}
   339  
   340  	// When the if-none-match header was set we need to check if any of the
   341  	// etags matches before finishing the upload.
   342  	if session.HeaderIfNoneMatch() != "" {
   343  		if session.HeaderIfNoneMatch() == "*" {
   344  			return unlock, errtypes.Aborted("etag mismatch, resource exists")
   345  		}
   346  		for _, ifNoneMatchTag := range strings.Split(session.HeaderIfNoneMatch(), ",") {
   347  			if ifNoneMatchTag == oldNodeEtag {
   348  				return unlock, errtypes.Aborted("etag mismatch")
   349  			}
   350  		}
   351  	}
   352  
   353  	// When the if-unmodified-since header was set we need to check if the
   354  	// etag still matches before finishing the upload.
   355  	if session.HeaderIfUnmodifiedSince() != "" {
   356  		ifUnmodifiedSince, err := time.Parse(time.RFC3339Nano, session.HeaderIfUnmodifiedSince())
   357  		if err != nil {
   358  			return unlock, errtypes.InternalError(fmt.Sprintf("failed to parse if-unmodified-since time: %s", err))
   359  		}
   360  
   361  		if oldNodeMtime.After(ifUnmodifiedSince) {
   362  			return unlock, errtypes.Aborted("if-unmodified-since mismatch")
   363  		}
   364  	}
   365  
   366  	if !store.disableVersioning {
   367  		versionPath := session.store.lu.InternalPath(spaceID, n.ID+node.RevisionIDDelimiter+oldNodeMtime.UTC().Format(time.RFC3339Nano))
   368  
   369  		// create version node
   370  		_, err := os.OpenFile(versionPath, os.O_CREATE|os.O_EXCL, 0600)
   371  		if err != nil {
   372  			if !errors.Is(err, os.ErrExist) {
   373  				return unlock, err
   374  			}
   375  
   376  			// a revision with this mtime does already exist.
   377  			// If the blobs are the same we can just delete the old one
   378  			if err := validateChecksums(ctx, old, session, versionPath); err != nil {
   379  				return unlock, err
   380  			}
   381  
   382  			// delete old blob
   383  			bID, _, err := session.store.lu.ReadBlobIDAndSizeAttr(ctx, versionPath, nil)
   384  			if err != nil {
   385  				return unlock, err
   386  			}
   387  			if err := session.store.tp.DeleteBlob(&node.Node{BlobID: bID, SpaceID: n.SpaceID}); err != nil {
   388  				return unlock, err
   389  			}
   390  
   391  			// clean revision file
   392  			span.AddEvent("os.Create")
   393  			if _, err := os.Create(versionPath); err != nil {
   394  				return unlock, err
   395  			}
   396  		}
   397  
   398  		// copy blob metadata to version node
   399  		if err := store.lu.CopyMetadataWithSourceLock(ctx, targetPath, versionPath, func(attributeName string, value []byte) (newValue []byte, copy bool) {
   400  			return value, strings.HasPrefix(attributeName, prefixes.ChecksumPrefix) ||
   401  				attributeName == prefixes.TypeAttr ||
   402  				attributeName == prefixes.BlobIDAttr ||
   403  				attributeName == prefixes.BlobsizeAttr ||
   404  				attributeName == prefixes.MTimeAttr
   405  		}, f, true); err != nil {
   406  			return unlock, err
   407  		}
   408  		session.info.MetaData["versionsPath"] = versionPath
   409  		// keep mtime from previous version
   410  		span.AddEvent("os.Chtimes")
   411  		if err := os.Chtimes(session.info.MetaData["versionsPath"], oldNodeMtime, oldNodeMtime); err != nil {
   412  			return unlock, errtypes.InternalError(fmt.Sprintf("failed to change mtime of version node: %s", err))
   413  		}
   414  	}
   415  
   416  	session.info.MetaData["sizeDiff"] = strconv.FormatInt((int64(fsize) - old.Blobsize), 10)
   417  
   418  	return unlock, nil
   419  }
   420  
   421  func validateChecksums(ctx context.Context, n *node.Node, session *OcisSession, versionPath string) error {
   422  	for _, t := range []string{"md5", "sha1", "adler32"} {
   423  		key := prefixes.ChecksumPrefix + t
   424  
   425  		checksum, err := n.Xattr(ctx, key)
   426  		if err != nil {
   427  			return err
   428  		}
   429  
   430  		revisionChecksum, err := session.store.lu.MetadataBackend().Get(ctx, versionPath, key)
   431  		if err != nil {
   432  			return err
   433  		}
   434  
   435  		if string(checksum) == "" || string(revisionChecksum) == "" {
   436  			return errors.New("checksum not found")
   437  		}
   438  
   439  		if string(checksum) != string(revisionChecksum) {
   440  			return errors.New("checksum mismatch")
   441  		}
   442  	}
   443  
   444  	return nil
   445  }