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

     1  // Copyright 2018-2023 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  	"os"
    25  	"path/filepath"
    26  	"strconv"
    27  	"time"
    28  
    29  	"github.com/google/renameio/v2"
    30  	tusd "github.com/tus/tusd/v2/pkg/handler"
    31  
    32  	userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
    33  	provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
    34  	typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
    35  	"github.com/cs3org/reva/v2/pkg/appctx"
    36  	ctxpkg "github.com/cs3org/reva/v2/pkg/ctx"
    37  	"github.com/cs3org/reva/v2/pkg/storage/utils/decomposedfs/node"
    38  	"github.com/cs3org/reva/v2/pkg/utils"
    39  )
    40  
    41  // OcisSession extends tus upload lifecycle with postprocessing steps.
    42  type OcisSession struct {
    43  	store OcisStore
    44  	// for now, we keep the json files in the uploads folder
    45  	info tusd.FileInfo
    46  }
    47  
    48  // Context returns a context with the user, logger and lockid used when initiating the upload session
    49  func (s *OcisSession) Context(ctx context.Context) context.Context { // restore logger from file info
    50  	sub := s.store.log.With().Int("pid", os.Getpid()).Logger()
    51  	ctx = appctx.WithLogger(ctx, &sub)
    52  	ctx = ctxpkg.ContextSetLockID(ctx, s.lockID())
    53  	ctx = ctxpkg.ContextSetUser(ctx, s.executantUser())
    54  	return ctxpkg.ContextSetInitiator(ctx, s.InitiatorID())
    55  }
    56  
    57  func (s *OcisSession) lockID() string {
    58  	return s.info.MetaData["lockid"]
    59  }
    60  func (s *OcisSession) executantUser() *userpb.User {
    61  	var o *typespb.Opaque
    62  	_ = json.Unmarshal([]byte(s.info.Storage["UserOpaque"]), &o)
    63  	return &userpb.User{
    64  		Id: &userpb.UserId{
    65  			Type:     userpb.UserType(userpb.UserType_value[s.info.Storage["UserType"]]),
    66  			Idp:      s.info.Storage["Idp"],
    67  			OpaqueId: s.info.Storage["UserId"],
    68  		},
    69  		Username:    s.info.Storage["UserName"],
    70  		DisplayName: s.info.Storage["UserDisplayName"],
    71  		Opaque:      o,
    72  	}
    73  }
    74  
    75  // Purge deletes the upload session metadata and written binary data
    76  func (s *OcisSession) Purge(ctx context.Context) error {
    77  	_, span := tracer.Start(ctx, "Purge")
    78  	defer span.End()
    79  	sessionPath := sessionPath(s.store.root, s.info.ID)
    80  	if err := os.Remove(sessionPath); err != nil {
    81  		return err
    82  	}
    83  	if err := os.Remove(s.binPath()); err != nil {
    84  		return err
    85  	}
    86  	return nil
    87  }
    88  
    89  // TouchBin creates a file to contain the binary data. It's size will be used to keep track of the tus upload offset.
    90  func (s *OcisSession) TouchBin() error {
    91  	file, err := os.OpenFile(s.binPath(), os.O_CREATE|os.O_WRONLY, defaultFilePerm)
    92  	if err != nil {
    93  		return err
    94  	}
    95  	return file.Close()
    96  }
    97  
    98  // Persist writes the upload session metadata to disk
    99  // events can update the scan outcome and the finished event might read an empty file because of race conditions
   100  // so we need to lock the file while writing and use atomic writes
   101  func (s *OcisSession) Persist(ctx context.Context) error {
   102  	_, span := tracer.Start(ctx, "Persist")
   103  	defer span.End()
   104  	sessionPath := sessionPath(s.store.root, s.info.ID)
   105  	// create folder structure (if needed)
   106  	if err := os.MkdirAll(filepath.Dir(sessionPath), 0700); err != nil {
   107  		return err
   108  	}
   109  
   110  	var d []byte
   111  	d, err := json.Marshal(s.info)
   112  	if err != nil {
   113  		return err
   114  	}
   115  	return renameio.WriteFile(sessionPath, d, 0600)
   116  }
   117  
   118  // ToFileInfo returns tus compatible FileInfo so the tus handler can access the upload offset
   119  func (s *OcisSession) ToFileInfo() tusd.FileInfo {
   120  	return s.info
   121  }
   122  
   123  // ProviderID returns the provider id
   124  func (s *OcisSession) ProviderID() string {
   125  	return s.info.MetaData["providerID"]
   126  }
   127  
   128  // SpaceID returns the space id
   129  func (s *OcisSession) SpaceID() string {
   130  	return s.info.Storage["SpaceRoot"]
   131  }
   132  
   133  // NodeID returns the node id
   134  func (s *OcisSession) NodeID() string {
   135  	return s.info.Storage["NodeId"]
   136  }
   137  
   138  // NodeParentID returns the nodes parent id
   139  func (s *OcisSession) NodeParentID() string {
   140  	return s.info.Storage["NodeParentId"]
   141  }
   142  
   143  // NodeExists returns wether or not the node existed during InitiateUpload.
   144  // FIXME If two requests try to write the same file they both will store a new
   145  // random node id in the session and try to initialize a new node when
   146  // finishing the upload. The second request will fail with an already exists
   147  // error when trying to create the symlink for the node in the parent directory.
   148  // A node should be created as part of InitiateUpload. When listing a directory
   149  // we can decide if we want to skip the entry, or expose uploed progress
   150  // information. But that is a bigger change and might involve client work.
   151  func (s *OcisSession) NodeExists() bool {
   152  	return s.info.Storage["NodeExists"] == "true"
   153  }
   154  
   155  // HeaderIfMatch returns the if-match header for the upload session
   156  func (s *OcisSession) HeaderIfMatch() string {
   157  	return s.info.MetaData["if-match"]
   158  }
   159  
   160  // HeaderIfNoneMatch returns the if-none-match header for the upload session
   161  func (s *OcisSession) HeaderIfNoneMatch() string {
   162  	return s.info.MetaData["if-none-match"]
   163  }
   164  
   165  // HeaderIfUnmodifiedSince returns the if-unmodified-since header for the upload session
   166  func (s *OcisSession) HeaderIfUnmodifiedSince() string {
   167  	return s.info.MetaData["if-unmodified-since"]
   168  }
   169  
   170  // Node returns the node for the session
   171  func (s *OcisSession) Node(ctx context.Context) (*node.Node, error) {
   172  	return node.ReadNode(ctx, s.store.lu, s.SpaceID(), s.info.Storage["NodeId"], false, nil, true)
   173  }
   174  
   175  // ID returns the upload session id
   176  func (s *OcisSession) ID() string {
   177  	return s.info.ID
   178  }
   179  
   180  // Filename returns the name of the node which is not the same as the name af the file being uploaded for legacy chunked uploads
   181  func (s *OcisSession) Filename() string {
   182  	return s.info.Storage["NodeName"]
   183  }
   184  
   185  // Chunk returns the chunk name when a legacy chunked upload was started
   186  func (s *OcisSession) Chunk() string {
   187  	return s.info.Storage["Chunk"]
   188  }
   189  
   190  // SetMetadata is used to fill the upload metadata that will be exposed to the end user
   191  func (s *OcisSession) SetMetadata(key, value string) {
   192  	s.info.MetaData[key] = value
   193  }
   194  
   195  // SetStorageValue is used to set metadata only relevant for the upload session implementation
   196  func (s *OcisSession) SetStorageValue(key, value string) {
   197  	s.info.Storage[key] = value
   198  }
   199  
   200  // SetSize will set the upload size of the underlying tus info.
   201  func (s *OcisSession) SetSize(size int64) {
   202  	s.info.Size = size
   203  }
   204  
   205  // SetSizeIsDeferred is uset to change the SizeIsDeferred property of the underlying tus info.
   206  func (s *OcisSession) SetSizeIsDeferred(value bool) {
   207  	s.info.SizeIsDeferred = value
   208  }
   209  
   210  // Dir returns the directory to which the upload is made
   211  // TODO get rid of Dir(), whoever consumes the reference should be able to deal
   212  // with a relative reference.
   213  // Dir is only used to:
   214  //   - fill the Path property when emitting the UploadReady event after
   215  //     postprocessing finished. I wonder why the UploadReady contains a finished
   216  //     flag ... maybe multiple distinct events would make more sense.
   217  //   - build the reference that is passed to the FileUploaded event in the
   218  //     UploadFinishedFunc callback passed to the Upload call used for simple
   219  //     datatx put requests
   220  //
   221  // AFAICT only search and audit services consume the path.
   222  //   - search needs to index from the root anyway. And it only needs the most
   223  //     recent path to put it in the index. So it should already be able to deal
   224  //     with an id based reference.
   225  //   - audit on the other hand needs to log events with the path at the state of
   226  //     the event ... so it does need the full path.
   227  //
   228  // I think we can safely determine the path later, right before emitting the
   229  // event. And maybe make it configurable, because only audit needs it, anyway.
   230  func (s *OcisSession) Dir() string {
   231  	return s.info.Storage["Dir"]
   232  }
   233  
   234  // Size returns the upload size
   235  func (s *OcisSession) Size() int64 {
   236  	return s.info.Size
   237  }
   238  
   239  // SizeDiff returns the size diff that was calculated after postprocessing
   240  func (s *OcisSession) SizeDiff() int64 {
   241  	sizeDiff, _ := strconv.ParseInt(s.info.MetaData["sizeDiff"], 10, 64)
   242  	return sizeDiff
   243  }
   244  
   245  // Reference returns a reference that can be used to access the uploaded resource
   246  func (s *OcisSession) Reference() provider.Reference {
   247  	return provider.Reference{
   248  		ResourceId: &provider.ResourceId{
   249  			StorageId: s.info.MetaData["providerID"],
   250  			SpaceId:   s.info.Storage["SpaceRoot"],
   251  			OpaqueId:  s.info.Storage["NodeId"],
   252  		},
   253  		// Path is not used
   254  	}
   255  }
   256  
   257  // Executant returns the id of the user that initiated the upload session
   258  func (s *OcisSession) Executant() userpb.UserId {
   259  	return userpb.UserId{
   260  		Type:     userpb.UserType(userpb.UserType_value[s.info.Storage["UserType"]]),
   261  		Idp:      s.info.Storage["Idp"],
   262  		OpaqueId: s.info.Storage["UserId"],
   263  	}
   264  }
   265  
   266  // SetExecutant is used to remember the user that initiated the upload session
   267  func (s *OcisSession) SetExecutant(u *userpb.User) {
   268  	s.info.Storage["Idp"] = u.GetId().GetIdp()
   269  	s.info.Storage["UserId"] = u.GetId().GetOpaqueId()
   270  	s.info.Storage["UserType"] = utils.UserTypeToString(u.GetId().Type)
   271  	s.info.Storage["UserName"] = u.GetUsername()
   272  	s.info.Storage["UserDisplayName"] = u.GetDisplayName()
   273  
   274  	b, _ := json.Marshal(u.GetOpaque())
   275  	s.info.Storage["UserOpaque"] = string(b)
   276  }
   277  
   278  // Offset returns the current upload offset
   279  func (s *OcisSession) Offset() int64 {
   280  	return s.info.Offset
   281  }
   282  
   283  // SpaceOwner returns the id of the space owner
   284  func (s *OcisSession) SpaceOwner() *userpb.UserId {
   285  	return &userpb.UserId{
   286  		// idp and type do not seem to be consumed and the node currently only stores the user id anyway
   287  		OpaqueId: s.info.Storage["SpaceOwnerOrManager"],
   288  	}
   289  }
   290  
   291  // Expires returns the time the upload session expires
   292  func (s *OcisSession) Expires() time.Time {
   293  	var t time.Time
   294  	if value, ok := s.info.MetaData["expires"]; ok {
   295  		t, _ = utils.MTimeToTime(value)
   296  	}
   297  	return t
   298  }
   299  
   300  // MTime returns the mtime to use for the uploaded file
   301  func (s *OcisSession) MTime() time.Time {
   302  	var t time.Time
   303  	if value, ok := s.info.MetaData["mtime"]; ok {
   304  		t, _ = utils.MTimeToTime(value)
   305  	}
   306  	return t
   307  }
   308  
   309  // IsProcessing returns true if all bytes have been received. The session then has entered postprocessing state.
   310  func (s *OcisSession) IsProcessing() bool {
   311  	// We might need a more sophisticated way to determine processing status soon
   312  	return s.info.Size == s.info.Offset && s.info.MetaData["scanResult"] == ""
   313  }
   314  
   315  // binPath returns the path to the file storing the binary data.
   316  func (s *OcisSession) binPath() string {
   317  	return filepath.Join(s.store.root, "uploads", s.info.ID)
   318  }
   319  
   320  // InitiatorID returns the id of the initiating client
   321  func (s *OcisSession) InitiatorID() string {
   322  	return s.info.MetaData["initiatorid"]
   323  }
   324  
   325  // SetScanData sets virus scan data to the upload session
   326  func (s *OcisSession) SetScanData(result string, date time.Time) {
   327  	s.info.MetaData["scanResult"] = result
   328  	s.info.MetaData["scanDate"] = date.Format(time.RFC3339)
   329  }
   330  
   331  // ScanData returns the virus scan data
   332  func (s *OcisSession) ScanData() (string, time.Time) {
   333  	date := s.info.MetaData["scanDate"]
   334  	if date == "" {
   335  		return "", time.Time{}
   336  	}
   337  	d, _ := time.Parse(time.RFC3339, date)
   338  	return s.info.MetaData["scanResult"], d
   339  }
   340  
   341  // sessionPath returns the path to the .info file storing the file's info.
   342  func sessionPath(root, id string) string {
   343  	return filepath.Join(root, "uploads", id+".info")
   344  }