github.com/cs3org/reva/v2@v2.27.7/pkg/storage/utils/decomposedfs/upload.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  	"fmt"
    24  	"os"
    25  	"path/filepath"
    26  	"strings"
    27  	"time"
    28  
    29  	provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
    30  	"github.com/google/uuid"
    31  	"github.com/pkg/errors"
    32  	tusd "github.com/tus/tusd/v2/pkg/handler"
    33  
    34  	"github.com/cs3org/reva/v2/pkg/appctx"
    35  	ctxpkg "github.com/cs3org/reva/v2/pkg/ctx"
    36  	"github.com/cs3org/reva/v2/pkg/errtypes"
    37  	"github.com/cs3org/reva/v2/pkg/rhttp/datatx/metrics"
    38  	"github.com/cs3org/reva/v2/pkg/storage"
    39  	"github.com/cs3org/reva/v2/pkg/storage/utils/chunking"
    40  	"github.com/cs3org/reva/v2/pkg/storage/utils/decomposedfs/node"
    41  	"github.com/cs3org/reva/v2/pkg/storage/utils/decomposedfs/upload"
    42  	"github.com/cs3org/reva/v2/pkg/storagespace"
    43  	"github.com/cs3org/reva/v2/pkg/utils"
    44  )
    45  
    46  // Upload uploads data to the given resource
    47  // TODO Upload (and InitiateUpload) needs a way to receive the expected checksum.
    48  // Maybe in metadata as 'checksum' => 'sha1 aeosvp45w5xaeoe' = lowercase, space separated?
    49  func (fs *Decomposedfs) Upload(ctx context.Context, req storage.UploadRequest, uff storage.UploadFinishedFunc) (*provider.ResourceInfo, error) {
    50  	_, span := tracer.Start(ctx, "Upload")
    51  	defer span.End()
    52  	up, err := fs.GetUpload(ctx, req.Ref.GetPath())
    53  	if err != nil {
    54  		return &provider.ResourceInfo{}, errors.Wrap(err, "Decomposedfs: error retrieving upload")
    55  	}
    56  
    57  	session := up.(*upload.OcisSession)
    58  
    59  	ctx = session.Context(ctx)
    60  
    61  	if session.Chunk() != "" { // check chunking v1
    62  		p, assembledFile, err := fs.chunkHandler.WriteChunk(session.Chunk(), req.Body)
    63  		if err != nil {
    64  			return &provider.ResourceInfo{}, err
    65  		}
    66  		if p == "" {
    67  			if err = session.Terminate(ctx); err != nil {
    68  				return &provider.ResourceInfo{}, errors.Wrap(err, "Decomposedfs: error removing auxiliary files")
    69  			}
    70  			return &provider.ResourceInfo{}, errtypes.PartialContent(req.Ref.String())
    71  		}
    72  		fd, err := os.Open(assembledFile)
    73  		if err != nil {
    74  			return &provider.ResourceInfo{}, errors.Wrap(err, "Decomposedfs: error opening assembled file")
    75  		}
    76  		defer fd.Close()
    77  		defer os.RemoveAll(assembledFile)
    78  		req.Body = fd
    79  
    80  		size, err := session.WriteChunk(ctx, 0, req.Body)
    81  		if err != nil {
    82  			return &provider.ResourceInfo{}, errors.Wrap(err, "Decomposedfs: error writing to binary file")
    83  		}
    84  		session.SetSize(size)
    85  	} else {
    86  		size, err := session.WriteChunk(ctx, 0, req.Body)
    87  		if err != nil {
    88  			return &provider.ResourceInfo{}, errors.Wrap(err, "Decomposedfs: error writing to binary file")
    89  		}
    90  		if size != req.Length {
    91  			return &provider.ResourceInfo{}, errtypes.PartialContent("Decomposedfs: unexpected end of stream")
    92  		}
    93  	}
    94  
    95  	if err := session.FinishUploadDecomposed(ctx); err != nil {
    96  		return &provider.ResourceInfo{}, err
    97  	}
    98  
    99  	if uff != nil {
   100  		uploadRef := &provider.Reference{
   101  			ResourceId: &provider.ResourceId{
   102  				StorageId: session.ProviderID(),
   103  				SpaceId:   session.SpaceID(),
   104  				OpaqueId:  session.SpaceID(),
   105  			},
   106  			Path: utils.MakeRelativePath(filepath.Join(session.Dir(), session.Filename())),
   107  		}
   108  		executant := session.Executant()
   109  		uff(session.SpaceOwner(), &executant, uploadRef)
   110  	}
   111  
   112  	ri := &provider.ResourceInfo{
   113  		// fill with at least fileid, mtime and etag
   114  		Id: &provider.ResourceId{
   115  			StorageId: session.ProviderID(),
   116  			SpaceId:   session.SpaceID(),
   117  			OpaqueId:  session.NodeID(),
   118  		},
   119  	}
   120  
   121  	// add etag to metadata
   122  	ri.Etag, _ = node.CalculateEtag(session.NodeID(), session.MTime())
   123  
   124  	if !session.MTime().IsZero() {
   125  		ri.Mtime = utils.TimeToTS(session.MTime())
   126  	}
   127  
   128  	return ri, nil
   129  }
   130  
   131  // InitiateUpload returns upload ids corresponding to different protocols it supports
   132  // TODO read optional content for small files in this request
   133  // TODO InitiateUpload (and Upload) needs a way to receive the expected checksum. Maybe in metadata as 'checksum' => 'sha1 aeosvp45w5xaeoe' = lowercase, space separated?
   134  func (fs *Decomposedfs) InitiateUpload(ctx context.Context, ref *provider.Reference, uploadLength int64, metadata map[string]string) (map[string]string, error) {
   135  	_, span := tracer.Start(ctx, "InitiateUpload")
   136  	defer span.End()
   137  	log := appctx.GetLogger(ctx)
   138  
   139  	// remember the path from the reference
   140  	refpath := ref.GetPath()
   141  	var chunk *chunking.ChunkBLOBInfo
   142  	var err error
   143  	if chunking.IsChunked(refpath) { // check chunking v1
   144  		chunk, err = chunking.GetChunkBLOBInfo(refpath)
   145  		if err != nil {
   146  			return nil, errtypes.BadRequest(err.Error())
   147  		}
   148  		ref.Path = chunk.Path
   149  	}
   150  	n, err := fs.lu.NodeFromResource(ctx, ref)
   151  	switch err.(type) {
   152  	case nil:
   153  		// ok
   154  	case errtypes.IsNotFound:
   155  		return nil, errtypes.PreconditionFailed(err.Error())
   156  	default:
   157  		return nil, err
   158  	}
   159  
   160  	// permissions are checked in NewUpload below
   161  
   162  	relative, err := fs.lu.Path(ctx, n, node.NoCheck)
   163  	// TODO why do we need the path here?
   164  	// jfd: it is used later when emitting the UploadReady event ...
   165  	// AAAND refPath might be . when accessing with an id / relative reference ... which causes NodeName to become . But then dir will also always be .
   166  	// That is why we still have to read the path here: so that the event we emit contains a relative reference with a path relative to the space root. WTF
   167  	if err != nil {
   168  		return nil, err
   169  	}
   170  
   171  	lockID, _ := ctxpkg.ContextGetLockID(ctx)
   172  
   173  	session := fs.sessionStore.New(ctx)
   174  	session.SetMetadata("filename", n.Name)
   175  	session.SetStorageValue("NodeName", n.Name)
   176  	if chunk != nil {
   177  		session.SetStorageValue("Chunk", filepath.Base(refpath))
   178  	}
   179  	session.SetMetadata("dir", filepath.Dir(relative))
   180  	session.SetStorageValue("Dir", filepath.Dir(relative))
   181  	session.SetMetadata("lockid", lockID)
   182  
   183  	session.SetSize(uploadLength)
   184  	session.SetStorageValue("SpaceRoot", n.SpaceRoot.ID)                                     // TODO SpaceRoot -> SpaceID
   185  	session.SetStorageValue("SpaceOwnerOrManager", n.SpaceOwnerOrManager(ctx).GetOpaqueId()) // TODO needed for what?
   186  
   187  	spaceGID, ok := ctx.Value(CtxKeySpaceGID).(uint32)
   188  	if ok {
   189  		session.SetStorageValue("SpaceGid", fmt.Sprintf("%d", spaceGID))
   190  	}
   191  
   192  	iid, _ := ctxpkg.ContextGetInitiator(ctx)
   193  	session.SetMetadata("initiatorid", iid)
   194  
   195  	if metadata != nil {
   196  		session.SetMetadata("providerID", metadata["providerID"])
   197  		if mtime, ok := metadata["mtime"]; ok {
   198  			if mtime != "null" {
   199  				session.SetMetadata("mtime", metadata["mtime"])
   200  			}
   201  		}
   202  		if expiration, ok := metadata["expires"]; ok {
   203  			if expiration != "null" {
   204  				session.SetMetadata("expires", metadata["expires"])
   205  			}
   206  		}
   207  		if _, ok := metadata["sizedeferred"]; ok {
   208  			session.SetSizeIsDeferred(true)
   209  		}
   210  		if checksum, ok := metadata["checksum"]; ok {
   211  			parts := strings.SplitN(checksum, " ", 2)
   212  			if len(parts) != 2 {
   213  				return nil, errtypes.BadRequest("invalid checksum format. must be '[algorithm] [checksum]'")
   214  			}
   215  			switch parts[0] {
   216  			case "sha1", "md5", "adler32":
   217  				session.SetMetadata("checksum", checksum)
   218  			default:
   219  				return nil, errtypes.BadRequest("unsupported checksum algorithm: " + parts[0])
   220  			}
   221  		}
   222  
   223  		// only check preconditions if they are not empty // TODO or is this a bad request?
   224  		if metadata["if-match"] != "" {
   225  			session.SetMetadata("if-match", metadata["if-match"])
   226  		}
   227  		if metadata["if-none-match"] != "" {
   228  			session.SetMetadata("if-none-match", metadata["if-none-match"])
   229  		}
   230  		if metadata["if-unmodified-since"] != "" {
   231  			session.SetMetadata("if-unmodified-since", metadata["if-unmodified-since"])
   232  		}
   233  	}
   234  
   235  	if session.MTime().IsZero() {
   236  		session.SetMetadata("mtime", utils.TimeToOCMtime(time.Now()))
   237  	}
   238  
   239  	log.Debug().Str("uploadid", session.ID()).Str("spaceid", n.SpaceID).Str("nodeid", n.ID).Interface("metadata", metadata).Msg("Decomposedfs: resolved filename")
   240  
   241  	_, err = node.CheckQuota(ctx, n.SpaceRoot, n.Exists, uint64(n.Blobsize), uint64(session.Size()))
   242  	if err != nil {
   243  		return nil, err
   244  	}
   245  
   246  	if session.Filename() == "" {
   247  		return nil, errors.New("Decomposedfs: missing filename in metadata")
   248  	}
   249  	if session.Dir() == "" {
   250  		return nil, errors.New("Decomposedfs: missing dir in metadata")
   251  	}
   252  
   253  	// the parent owner will become the new owner
   254  	parent, perr := n.Parent(ctx)
   255  	if perr != nil {
   256  		return nil, errors.Wrap(perr, "Decomposedfs: error getting parent "+n.ParentID)
   257  	}
   258  
   259  	// check permissions
   260  	var (
   261  		checkNode *node.Node
   262  		path      string
   263  	)
   264  	if n.Exists {
   265  		// check permissions of file to be overwritten
   266  		checkNode = n
   267  		path, _ = storagespace.FormatReference(&provider.Reference{ResourceId: &provider.ResourceId{
   268  			SpaceId:  checkNode.SpaceID,
   269  			OpaqueId: checkNode.ID,
   270  		}})
   271  	} else {
   272  		// check permissions of parent
   273  		checkNode = parent
   274  		path, _ = storagespace.FormatReference(&provider.Reference{ResourceId: &provider.ResourceId{
   275  			SpaceId:  checkNode.SpaceID,
   276  			OpaqueId: checkNode.ID,
   277  		}, Path: n.Name})
   278  	}
   279  	rp, err := fs.p.AssemblePermissions(ctx, checkNode)
   280  	switch {
   281  	case err != nil:
   282  		return nil, err
   283  	case !rp.InitiateFileUpload:
   284  		return nil, errtypes.PermissionDenied(path)
   285  	}
   286  
   287  	// are we trying to overwriting a folder with a file?
   288  	if n.Exists && n.IsDir(ctx) {
   289  		return nil, errtypes.PreconditionFailed("resource is not a file")
   290  	}
   291  
   292  	// check lock
   293  	if err := n.CheckLock(ctx); err != nil {
   294  		return nil, err
   295  	}
   296  
   297  	usr := ctxpkg.ContextMustGetUser(ctx)
   298  
   299  	// fill future node info
   300  	if n.Exists {
   301  		if session.HeaderIfNoneMatch() == "*" {
   302  			return nil, errtypes.Aborted(fmt.Sprintf("parent %s already has a child %s, id %s", n.ParentID, n.Name, n.ID))
   303  		}
   304  		session.SetStorageValue("NodeId", n.ID)
   305  		session.SetStorageValue("NodeExists", "true")
   306  	} else {
   307  		session.SetStorageValue("NodeId", uuid.New().String())
   308  	}
   309  	session.SetStorageValue("NodeParentId", n.ParentID)
   310  	session.SetExecutant(usr)
   311  	session.SetStorageValue("LogLevel", log.GetLevel().String())
   312  
   313  	log.Debug().Interface("session", session).Msg("Decomposedfs: built session info")
   314  
   315  	err = fs.um.RunInBaseScope(func() error {
   316  		// Create binary file in the upload folder with no content
   317  		// It will be used when determining the current offset of an upload
   318  		err := session.TouchBin()
   319  		if err != nil {
   320  			return err
   321  		}
   322  
   323  		return session.Persist(ctx)
   324  	})
   325  	if err != nil {
   326  		return nil, err
   327  	}
   328  	metrics.UploadSessionsInitiated.Inc()
   329  
   330  	if uploadLength == 0 {
   331  		// Directly finish this upload
   332  		err = session.FinishUploadDecomposed(ctx)
   333  		if err != nil {
   334  			return nil, err
   335  		}
   336  	}
   337  
   338  	return map[string]string{
   339  		"simple": session.ID(),
   340  		"tus":    session.ID(),
   341  	}, nil
   342  }
   343  
   344  // UseIn tells the tus upload middleware which extensions it supports.
   345  func (fs *Decomposedfs) UseIn(composer *tusd.StoreComposer) {
   346  	composer.UseCore(fs)
   347  	composer.UseTerminater(fs)
   348  	composer.UseConcater(fs)
   349  	composer.UseLengthDeferrer(fs)
   350  }
   351  
   352  // To implement the core tus.io protocol as specified in https://tus.io/protocols/resumable-upload.html#core-protocol
   353  // - the storage needs to implement NewUpload and GetUpload
   354  // - the upload needs to implement the tusd.Upload interface: WriteChunk, GetInfo, GetReader and FinishUpload
   355  
   356  // NewUpload returns a new tus Upload instance
   357  func (fs *Decomposedfs) NewUpload(ctx context.Context, info tusd.FileInfo) (tusd.Upload, error) {
   358  	return nil, fmt.Errorf("not implemented, use InitiateUpload on the CS3 API to start a new upload")
   359  }
   360  
   361  // GetUpload returns the Upload for the given upload id
   362  func (fs *Decomposedfs) GetUpload(ctx context.Context, id string) (tusd.Upload, error) {
   363  	var ul tusd.Upload
   364  	var err error
   365  	_ = fs.um.RunInBaseScope(func() error {
   366  		ul, err = fs.sessionStore.Get(ctx, id)
   367  		return nil
   368  	})
   369  	return ul, err
   370  }
   371  
   372  // ListUploadSessions returns the upload sessions for the given filter
   373  func (fs *Decomposedfs) ListUploadSessions(ctx context.Context, filter storage.UploadSessionFilter) ([]storage.UploadSession, error) {
   374  	var sessions []*upload.OcisSession
   375  	if filter.ID != nil && *filter.ID != "" {
   376  		session, err := fs.sessionStore.Get(ctx, *filter.ID)
   377  		if err != nil {
   378  			return nil, err
   379  		}
   380  		sessions = []*upload.OcisSession{session}
   381  	} else {
   382  		var err error
   383  		sessions, err = fs.sessionStore.List(ctx)
   384  		if err != nil {
   385  			return nil, err
   386  		}
   387  	}
   388  	filteredSessions := []storage.UploadSession{}
   389  	now := time.Now()
   390  	for _, session := range sessions {
   391  		if filter.Processing != nil && *filter.Processing != session.IsProcessing() {
   392  			continue
   393  		}
   394  		if filter.Expired != nil {
   395  			if *filter.Expired {
   396  				if now.Before(session.Expires()) {
   397  					continue
   398  				}
   399  			} else {
   400  				if now.After(session.Expires()) {
   401  					continue
   402  				}
   403  			}
   404  		}
   405  		if filter.HasVirus != nil {
   406  			sr, _ := session.ScanData()
   407  			infected := sr != ""
   408  			if *filter.HasVirus != infected {
   409  				continue
   410  			}
   411  		}
   412  		filteredSessions = append(filteredSessions, session)
   413  	}
   414  	return filteredSessions, nil
   415  }
   416  
   417  // AsTerminatableUpload returns a TerminatableUpload
   418  // To implement the termination extension as specified in https://tus.io/protocols/resumable-upload.html#termination
   419  // the storage needs to implement AsTerminatableUpload
   420  func (fs *Decomposedfs) AsTerminatableUpload(up tusd.Upload) tusd.TerminatableUpload {
   421  	return up.(*upload.OcisSession)
   422  }
   423  
   424  // AsLengthDeclarableUpload returns a LengthDeclarableUpload
   425  // To implement the creation-defer-length extension as specified in https://tus.io/protocols/resumable-upload.html#creation
   426  // the storage needs to implement AsLengthDeclarableUpload
   427  func (fs *Decomposedfs) AsLengthDeclarableUpload(up tusd.Upload) tusd.LengthDeclarableUpload {
   428  	return up.(*upload.OcisSession)
   429  }
   430  
   431  // AsConcatableUpload returns a ConcatableUpload
   432  // To implement the concatenation extension as specified in https://tus.io/protocols/resumable-upload.html#concatenation
   433  // the storage needs to implement AsConcatableUpload
   434  func (fs *Decomposedfs) AsConcatableUpload(up tusd.Upload) tusd.ConcatableUpload {
   435  	return up.(*upload.OcisSession)
   436  }