gitlab.com/SkynetLabs/skyd@v1.6.9/skymodules/renter/tus.go (about)

     1  package renter
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"fmt"
     8  	"io"
     9  	"net/http"
    10  	"os"
    11  	"sync"
    12  	"time"
    13  
    14  	"github.com/tus/tusd/pkg/handler"
    15  	"gitlab.com/NebulousLabs/errors"
    16  	"gitlab.com/NebulousLabs/fastrand"
    17  	"gitlab.com/SkynetLabs/skyd/build"
    18  	"gitlab.com/SkynetLabs/skyd/skymodules"
    19  	"gitlab.com/SkynetLabs/skyd/skymodules/renter/filesystem"
    20  	"go.sia.tech/siad/crypto"
    21  	"go.sia.tech/siad/modules"
    22  	"go.sia.tech/siad/persist"
    23  )
    24  
    25  var (
    26  	// PruneTUSUploadTimeout is the time of inactivity after which a
    27  	// skynetTUSUpload is pruned from skynetTUSUploader. Inactivity refers to
    28  	// the time passed since WriteChunk was called.
    29  	PruneTUSUploadTimeout = build.Select(build.Var{
    30  		Dev:      5 * time.Minute,
    31  		Standard: 24 * time.Hour, // 24 hours to give plenty of time to finish an upload
    32  		Testing:  10 * time.Second,
    33  	}).(time.Duration)
    34  
    35  	// PruneTUSUploadInterval is the time that passes between pruning attempts.
    36  	// The smaller the interval, the smaller the batches of uploads we prune at
    37  	// a time.
    38  	PruneTUSUploadInterval = build.Select(build.Var{
    39  		Dev:      time.Minute,
    40  		Standard: time.Hour,
    41  		Testing:  time.Second,
    42  	}).(time.Duration)
    43  )
    44  
    45  // ErrTUSUploadInterrupted is returned if the upload seemingly succeeded but
    46  // didn't actually upload a full chunk.
    47  var ErrTUSUploadInterrupted = errors.New("tus upload was interrupted - please retry")
    48  
    49  type (
    50  	// skynetTUSUploader implements multiple TUS interfaces for skynet uploads
    51  	// allowing for resumable uploads.
    52  	skynetTUSUploader struct {
    53  		staticUploadStore skymodules.SkynetTUSUploadStore
    54  
    55  		ongoingUploads map[string]*ongoingTUSUpload
    56  		staticRenter   *Renter
    57  		mu             sync.Mutex
    58  	}
    59  
    60  	// ongoingTUSUpload implements multiple TUS interfaces for uploads.
    61  	ongoingTUSUpload struct {
    62  		staticUpload   skymodules.SkynetTUSUpload
    63  		staticUploader *skynetTUSUploader
    64  
    65  		fileNode *filesystem.FileNode
    66  		closed   bool
    67  		mu       sync.Mutex
    68  	}
    69  )
    70  
    71  // TusSkyfileMetadata is a helper which constructs metadata for tus uploads give
    72  // some inputs.
    73  func TusSkyfileMetadata(fileName string, fileType string, size uint64, mode os.FileMode) skymodules.SkyfileMetadata {
    74  	return skymodules.SkyfileMetadata{
    75  		Filename: fileName,
    76  		Length:   size,
    77  		Mode:     mode,
    78  		Subfiles: skymodules.SkyfileSubfiles{
    79  			fileName: skymodules.SkyfileSubfileMetadata{
    80  				Filename:    fileName,
    81  				ContentType: fileType,
    82  				Len:         size,
    83  				Offset:      0,
    84  			},
    85  		},
    86  	}
    87  }
    88  
    89  // newSkynetTUSUploader creates a new uploader.
    90  func newSkynetTUSUploader(renter *Renter, tus skymodules.SkynetTUSUploadStore) *skynetTUSUploader {
    91  	return &skynetTUSUploader{
    92  		staticUploadStore: tus,
    93  		staticRenter:      renter,
    94  		ongoingUploads:    make(map[string]*ongoingTUSUpload),
    95  	}
    96  }
    97  
    98  // SkynetTUSUploader returns the renter's uploader for registering in the API.
    99  func (r *Renter) SkynetTUSUploader() skymodules.SkynetTUSDataStore {
   100  	return r.staticSkynetTUSUploader
   101  }
   102  
   103  // Close closes the underlying upload store.
   104  func (stu *skynetTUSUploader) Close() error {
   105  	return stu.staticUploadStore.Close()
   106  }
   107  
   108  // AsConcatableUpload implements the ConcaterDataStore interface.
   109  func (stu *skynetTUSUploader) AsConcatableUpload(upload handler.Upload) handler.ConcatableUpload {
   110  	return upload.(*ongoingTUSUpload)
   111  }
   112  
   113  // NewLock implements the handler.Locker interface by passing on the call to the
   114  // upload storage backend.
   115  func (stu *skynetTUSUploader) NewLock(id string) (handler.Lock, error) {
   116  	return stu.staticUploadStore.NewLock(id)
   117  }
   118  
   119  // NewUpload creates a new upload from fileinfo.
   120  func (stu *skynetTUSUploader) NewUpload(ctx context.Context, info handler.FileInfo) (handler.Upload, error) {
   121  	// Create the upload object.
   122  	info.ID = persist.UID()
   123  
   124  	// Get a siapath.
   125  	sp := skymodules.RandomSkynetFilePath()
   126  
   127  	// Get the filename from either the metadata or path.
   128  	fileName := sp.Name()
   129  	fileNameMD, fileNameFound := info.MetaData["filename"]
   130  	if fileNameFound {
   131  		fileName = fileNameMD
   132  	}
   133  	fileType := info.MetaData["filetype"]
   134  
   135  	// Create the skyfile upload params.
   136  	sup := skymodules.SkyfileUploadParameters{
   137  		SiaPath:             sp,
   138  		Filename:            fileName,
   139  		BaseChunkRedundancy: SkyfileDefaultBaseChunkRedundancy,
   140  	}
   141  
   142  	// Create metadata.
   143  	var sm skymodules.SkyfileMetadata
   144  	var smBytes []byte
   145  	var err error
   146  	smStr, customSM := info.MetaData["skyfilemetadata"]
   147  	if customSM {
   148  		// Check if the client provided custom metadata. If they do, this will
   149  		// completely overwrite any other filename or filetype they might have
   150  		// provided and use that instead0.
   151  		smBytes = []byte(smStr)
   152  		err = json.Unmarshal(smBytes, &sm)
   153  		if err != nil {
   154  			return nil, errors.AddContext(err, "failed to unmarshal custom metadata")
   155  		}
   156  	} else {
   157  		// Use regular metadata.
   158  		sm = TusSkyfileMetadata(sup.Filename, fileType, uint64(info.Size), sup.Mode)
   159  		smBytes, err = json.Marshal(sm)
   160  		if err != nil {
   161  			return nil, errors.AddContext(err, "failed to marhsal metadata")
   162  		}
   163  	}
   164  
   165  	// Check whether it's valid.
   166  	err = skymodules.ValidateSkyfileMetadata(sm)
   167  	if err != nil {
   168  		return nil, errors.AddContext(err, "invalid metadata")
   169  	}
   170  
   171  	// Set the upload params to 'force' to allow overwriting the fileNode.
   172  	sup.Force = true
   173  
   174  	// Create the upload.
   175  	upload, err := stu.managedCreateUpload(info, sp, fileName, SkyfileDefaultBaseChunkRedundancy, skymodules.RenterDefaultDataPieces, skymodules.RenterDefaultParityPieces, smBytes, crypto.TypePlain)
   176  	if err != nil {
   177  		return nil, errors.AddContext(err, "failed to save new upload")
   178  	}
   179  	return upload, nil
   180  }
   181  
   182  // managedCreateUpload creates a new ongoing upload.
   183  func (stu *skynetTUSUploader) managedCreateUpload(fi handler.FileInfo, sp skymodules.SiaPath, fileName string, baseChunkRedundancy uint8, fanoutDataPieces, fanoutParityPieces int, smBytes []byte, ct crypto.CipherType) (*ongoingTUSUpload, error) {
   184  	ctx := stu.staticRenter.tg.StopCtx()
   185  	upload, err := stu.staticUploadStore.CreateUpload(ctx, fi, sp, fileName, baseChunkRedundancy, fanoutDataPieces, fanoutParityPieces, smBytes, ct)
   186  	if err != nil {
   187  		return nil, errors.AddContext(err, "upload store failed to create new upload")
   188  	}
   189  	stu.mu.Lock()
   190  	defer stu.mu.Unlock()
   191  	ou := &ongoingTUSUpload{
   192  		staticUpload:   upload,
   193  		staticUploader: stu,
   194  	}
   195  	stu.ongoingUploads[fi.ID] = ou
   196  	return ou, nil
   197  }
   198  
   199  // GetUpload returns an existing upload.
   200  func (stu *skynetTUSUploader) GetUpload(ctx context.Context, id string) (handler.Upload, error) {
   201  	// Get the upload from the db first.
   202  	upload, err := stu.staticUploadStore.GetUpload(ctx, id)
   203  	if err != nil {
   204  		return nil, err
   205  	}
   206  	// Search for ongoing upload.
   207  	stu.mu.Lock()
   208  	defer stu.mu.Unlock()
   209  	ou, exists := stu.ongoingUploads[id]
   210  	if exists {
   211  		// Only swap out the upload we just fetched.
   212  		ou.staticUpload = upload
   213  		return ou, nil
   214  	}
   215  
   216  	// If it doesn't exist create one.
   217  	ou = &ongoingTUSUpload{
   218  		staticUpload:   upload,
   219  		staticUploader: stu,
   220  	}
   221  	stu.ongoingUploads[id] = ou
   222  	return ou, nil
   223  }
   224  
   225  // Skylink returns the skylink for the upload with the given ID.
   226  func (stu *skynetTUSUploader) Skylink(id string) (skymodules.Skylink, bool) {
   227  	u, err := stu.staticUploadStore.GetUpload(stu.staticRenter.tg.StopCtx(), id)
   228  	if err != nil {
   229  		return skymodules.Skylink{}, false
   230  	}
   231  	return u.GetSkylink()
   232  }
   233  
   234  // Close closes the upload and underlying filenode.
   235  func (u *ongoingTUSUpload) Close() error {
   236  	return u.managedClose()
   237  }
   238  
   239  // tryUploadSmallFile checks if the file to upload and its metadata fit within a
   240  // single sector. It returns true or false depending on whether the file is
   241  // small and any buffered data.
   242  func (u *ongoingTUSUpload) tryUploadSmallFile(ctx context.Context, reader io.Reader) ([]byte, bool, error) {
   243  	// Get skyfile metadata.
   244  	fi, err := u.staticUpload.GetInfo(ctx)
   245  	if err != nil {
   246  		return nil, false, err
   247  	}
   248  	smBytes, err := u.staticUpload.SkyfileMetadata(ctx)
   249  	if err != nil {
   250  		return nil, false, err
   251  	}
   252  
   253  	// Check if the file is indeed a small file.
   254  	headerSize := uint64(skymodules.SkyfileLayoutSize + len(smBytes))
   255  	if uint64(fi.Size)+headerSize > modules.SectorSize {
   256  		return nil, false, nil
   257  	}
   258  
   259  	// Download the data and save it.
   260  	buf := make([]byte, fi.Size)
   261  	_, err = io.ReadFull(reader, buf)
   262  	return buf, true, err
   263  }
   264  
   265  // WriteChunk writes the chunk to the provided offset.
   266  func (u *ongoingTUSUpload) WriteChunk(ctx context.Context, offset int64, src io.Reader) (_ int64, err error) {
   267  	u.mu.Lock()
   268  	defer u.mu.Unlock()
   269  	uploader := u.staticUploader
   270  
   271  	// Get fileinfo from upload.
   272  	fi, err := u.staticUpload.GetInfo(ctx)
   273  	if err != nil {
   274  		return 0, errors.AddContext(err, "failed to get fileinfo")
   275  	}
   276  
   277  	// Fetch the upload params.
   278  	sup, up, err := u.staticUpload.UploadParams(ctx)
   279  	if err != nil {
   280  		return 0, errors.AddContext(err, "failed to fetch upload params")
   281  	}
   282  
   283  	// Make sure to queue a bubble for the new chunk at the end.
   284  	dir, err := up.SiaPath.Dir()
   285  	if err != nil {
   286  		return 0, errors.AddContext(err, "failed to get parent folder's siapath")
   287  	}
   288  	u.staticUploader.staticRenter.staticDirUpdateBatcher.callQueueDirUpdate(dir)
   289  
   290  	// If the offset is 0, we try to determine if the upload is large or small.
   291  	var isSmall bool
   292  	if offset == 0 {
   293  		var smallFileData []byte
   294  		smallFileData, isSmall, err = u.tryUploadSmallFile(ctx, src)
   295  		if err != nil {
   296  			return 0, err
   297  		}
   298  		// If it is a small file we are done. Unless it's a partial
   299  		// upload. In that case we still upload it the same way as a
   300  		// large upload to receive a fanout.
   301  		if isSmall && !fi.IsPartial {
   302  			// Finish the small upload.
   303  			smBytes, err := u.staticUpload.SkyfileMetadata(ctx)
   304  			if err != nil {
   305  				return 0, errors.AddContext(err, "failed to fetch smBytes")
   306  			}
   307  			skylink, err := u.finishUploadSmall(ctx, sup, smBytes, smallFileData)
   308  			if err != nil {
   309  				return 0, err
   310  			}
   311  			err = u.staticUpload.CommitFinishUpload(ctx, skylink)
   312  			if err != nil {
   313  				return 0, errors.AddContext(err, "failed to finish small upload")
   314  			}
   315  			return int64(len(smallFileData)), nil
   316  		}
   317  		// Otherwise we add the data to a reader to be uploaded as a
   318  		// large file.
   319  		src = io.MultiReader(bytes.NewReader(smallFileData), src)
   320  	}
   321  
   322  	// If we get to this point with a small file, something is wrong.
   323  	// Theoretically this is not possible but return an error for extra safety.
   324  	if isSmall && !fi.IsPartial {
   325  		return 0, errors.New("can't upload another chunk to a small file upload")
   326  	}
   327  
   328  	// Upload is a large upload - we need a filenode for the fanout. If it
   329  	// doesn't exist yet, create it.
   330  	if u.fileNode == nil {
   331  		u.fileNode, err = uploader.staticRenter.managedInitFileNode(up, 0)
   332  		if err != nil {
   333  			return 0, err
   334  		}
   335  	}
   336  	fileNode := u.fileNode
   337  	ec := fileNode.ErasureCode()
   338  
   339  	// Sanity check offset.
   340  	// NOTE: If the offset is not chunk aligned, it means that a previous call
   341  	// to WriteChunk read an incomplete chunk from src and padded it. After
   342  	// uploading a padded chunk, we can't upload more chunks. That's why the
   343  	// client needs to make sure that the chunkSize they use is aligned with the
   344  	// chunkSize of the skyfile's fanout.
   345  	deps := u.staticUploader.staticRenter.staticDeps
   346  	if offset%int64(fileNode.ChunkSize()) != 0 {
   347  		err := fmt.Errorf("offset is not chunk aligned - make sure chunkSize is set to a multiple of %v for these upload params", fileNode.ChunkSize())
   348  		if build.Release == "testing" {
   349  			// In test builds we want to be aware of this.
   350  			build.Critical(err)
   351  		}
   352  		return 0, err
   353  	}
   354  
   355  	// Simulate unstable connection.
   356  	if deps.Disrupt("TUSUnstable") {
   357  		// 50% chance that write fails
   358  		if fastrand.Intn(2) == 0 {
   359  			return 0, errors.New("TUSUnstable")
   360  		}
   361  	}
   362  
   363  	// Upload.
   364  	cr := NewFanoutChunkReader(src, ec, fileNode.MasterKey())
   365  	var chunks []*unfinishedUploadChunk
   366  	chunks, n, err := uploader.staticRenter.callUploadStreamFromReaderWithFileNodeNoBlock(ctx, fileNode, cr, offset)
   367  
   368  	// Simulate loss of connection one byte early.
   369  	if deps.Disrupt("TUSConnectionDropped") {
   370  		n--
   371  		err = nil
   372  	}
   373  
   374  	// If less than a full chunk was uploaded, we expect the file to be done. If
   375  	// that's not the case, the connection was closed early. That means the chunk
   376  	// was incorrectly padded and needs to be removed again by shrinking the siafile
   377  	// by one chunk before the user can retry the upload.
   378  	if n%int64(fileNode.ChunkSize()) != 0 && fi.Offset+n != fi.Size {
   379  		shrinkErr := fileNode.Shrink(uint64(fi.Offset) / fileNode.ChunkSize())
   380  		if shrinkErr != nil {
   381  			return 0, shrinkErr
   382  		}
   383  		// Make sure that we return an error if none was returned by the
   384  		// upload. That way the client will know to retry. This usually
   385  		// happens if we reach a timeout in the reverse proxy.
   386  		err = errors.Compose(err, ErrTUSUploadInterrupted)
   387  	}
   388  	// In case of any error, return early.
   389  	if err != nil {
   390  		return 0, err
   391  	}
   392  
   393  	// Wait for chunks to become available.
   394  	for _, chunk := range chunks {
   395  		select {
   396  		case <-ctx.Done():
   397  			return 0, errors.New("reached timeout before chunks became available")
   398  		case <-chunk.staticAvailableChan:
   399  		}
   400  		if chunk.err != nil {
   401  			return 0, errors.AddContext(err, "failed to upload chunk")
   402  		}
   403  	}
   404  	return n, u.staticUpload.CommitWriteChunk(ctx, fi.Offset+n, time.Now(), isSmall, cr.Fanout())
   405  }
   406  
   407  // GetInfo returns the file info.
   408  func (u *ongoingTUSUpload) GetInfo(ctx context.Context) (handler.FileInfo, error) {
   409  	return u.staticUpload.GetInfo(ctx)
   410  }
   411  
   412  // GetReader returns a reader for the upload.
   413  // NOTE: This is part of the core upload interface but doesn't seem to be
   414  // required for uploads to work. It is not necessary for this to work on
   415  // incomplete uploads and it's recommended to implement this for completed
   416  // uploads.
   417  func (u *ongoingTUSUpload) GetReader(ctx context.Context) (io.Reader, error) {
   418  	return bytes.NewReader([]byte{}), handler.ErrNotImplemented
   419  }
   420  
   421  // finishUploadLarge handles finishing up a large upload.
   422  func (u *ongoingTUSUpload) finishUploadLarge(ctx context.Context, fanout []byte, sup skymodules.SkyfileUploadParameters, fi handler.FileInfo, masterKey crypto.CipherKey, ec skymodules.ErasureCoder, smBytes []byte, concat bool) (skymodules.Skylink, error) {
   423  	r := u.staticUploader.staticRenter
   424  	// Sanity check fanout length
   425  	expectedLength := skymodules.ExpectedFanoutBytesLen(uint64(fi.Size), ec.MinPieces(), ec.NumPieces()-ec.MinPieces(), masterKey.Type())
   426  	if len(fanout) != int(expectedLength) {
   427  		return skymodules.Skylink{}, fmt.Errorf("can't finish large upload - invalid fanout length: got %v expected %v", len(fanout), expectedLength)
   428  	}
   429  	skylink, err := r.managedCreateSkylinkRawMD(ctx, sup, smBytes, fanout, uint64(fi.Size), masterKey, ec)
   430  	if err != nil {
   431  		return skymodules.Skylink{}, errors.AddContext(err, "failed to create skylink for large upload")
   432  	}
   433  	if concat {
   434  		// Pin the large file if this is finishing up a concatenated
   435  		// upload. We don't need to do this for small uploads because
   436  		// they are always uploaded in one go. Large uploads can be
   437  		// spread across multiple servers.
   438  		// NOTE: We do this lazily since we know all the data has already been
   439  		// uploaded recently. We also skip the base sector upload because that
   440  		// just happened in managedCreateSkylinkRawMD.
   441  		err = u.staticUploader.staticRenter.managedPinSkylink(ctx, skylink, skymodules.SkyfileUploadParameters{
   442  			BaseChunkRedundancy: SkyfileDefaultBaseChunkRedundancy,
   443  			SiaPath:             skymodules.RandomSkynetFilePath(),
   444  		}, defaultNewStreamTimeout, skymodules.DefaultSkynetPricePerMS, true, true)
   445  		if err != nil {
   446  			return skymodules.Skylink{}, errors.AddContext(err, "failed to pin large upload")
   447  		}
   448  	}
   449  	return skylink, nil
   450  }
   451  
   452  // finishUploadSmall handles finishing up a small upload.
   453  func (u *ongoingTUSUpload) finishUploadSmall(ctx context.Context, sup skymodules.SkyfileUploadParameters, smBytes, smallUploadData []byte) (skylink skymodules.Skylink, err error) {
   454  	r := u.staticUploader.staticRenter
   455  	return r.managedUploadSkyfileSmallFile(ctx, sup, smBytes, smallUploadData)
   456  }
   457  
   458  // FinishUpload is called when the upload is done.
   459  func (u *ongoingTUSUpload) FinishUpload(ctx context.Context) (err error) {
   460  	// Close upload when done.
   461  	defer func() {
   462  		err = errors.Compose(err, u.Close())
   463  	}()
   464  
   465  	u.mu.Lock()
   466  	defer u.mu.Unlock()
   467  
   468  	// If the upload is a partial upload we are done. We don't need to
   469  	// upload the metadata or create a skylink.
   470  	fi, err := u.staticUpload.GetInfo(ctx)
   471  	if err != nil {
   472  		return errors.AddContext(err, "failed to fetch fileinfo")
   473  	}
   474  	if fi.IsPartial {
   475  		return nil
   476  	}
   477  	// If the upload is a small file upload with >0 size we are done because
   478  	// it was already finalised in WriteChunk.
   479  	if u.fileNode == nil && fi.Size > 0 {
   480  		return nil
   481  	}
   482  
   483  	// Finish the large or 0-byte upload.
   484  	sup, _, err := u.staticUpload.UploadParams(ctx)
   485  	if err != nil {
   486  		return errors.AddContext(err, "failed to fetch upload params")
   487  	}
   488  	smBytes, err := u.staticUpload.SkyfileMetadata(ctx)
   489  	if err != nil {
   490  		return errors.AddContext(err, "failed to fetch smBytes")
   491  	}
   492  	fanout, err := u.staticUpload.Fanout(ctx)
   493  	if err != nil {
   494  		return errors.AddContext(err, "failed to fetch fanout")
   495  	}
   496  
   497  	// If the upload is 0-byte, WriteChunk is skipped. So we need to finish
   498  	// the upload here.
   499  	var skylink skymodules.Skylink
   500  	if fi.Size == 0 {
   501  		skylink, err = u.finishUploadSmall(ctx, sup, smBytes, []byte{})
   502  	} else {
   503  		skylink, err = u.finishUploadLarge(ctx, fanout, sup, fi, u.fileNode.MasterKey(), u.fileNode.ErasureCode(), smBytes, false)
   504  	}
   505  	if err != nil {
   506  		return errors.AddContext(err, "failed to finish upload")
   507  	}
   508  	return u.staticUpload.CommitFinishUpload(ctx, skylink)
   509  }
   510  
   511  // managedClose closes the upload and underlying filenode.
   512  func (u *ongoingTUSUpload) managedClose() error {
   513  	u.mu.Lock()
   514  	defer u.mu.Unlock()
   515  	if u.closed {
   516  		return nil
   517  	}
   518  	u.closed = true
   519  	// For large files we need to close the additional fileNode.
   520  	if u.fileNode != nil {
   521  		return u.fileNode.Close()
   522  	}
   523  	return nil
   524  }
   525  
   526  // threadedPruneTUSUploads periodically cleans up the uploads launched by the
   527  // TUS endpoints.
   528  func (r *Renter) threadedPruneTUSUploads() {
   529  	if r.staticDeps.Disrupt("TUSNoPrune") {
   530  		return // disable pruning
   531  	}
   532  	ticker := time.NewTicker(PruneTUSUploadInterval)
   533  	for {
   534  		select {
   535  		case <-r.tg.StopChan():
   536  			return // shutdown
   537  		case <-ticker.C:
   538  		}
   539  		toDelete, err := r.staticSkynetTUSUploader.staticUploadStore.ToPrune(r.tg.StopCtx())
   540  		if err != nil {
   541  			r.staticLog.Print("Failed to get TUS uploads for pruning", err)
   542  		}
   543  
   544  		// Delete files.
   545  		var prunedIDs []string
   546  		for _, upload := range toDelete {
   547  			uploadID, sp, err := upload.PruneInfo(r.tg.StopCtx())
   548  			if err != nil {
   549  				r.staticLog.Print("WARN: failed to fetch prune info from upload", err)
   550  				continue
   551  			}
   552  
   553  			// Delete on disk. We don't care if the extended siapath
   554  			// didn't exist but we print some loging if the regular
   555  			// deletion failed.
   556  			spFanout, err := sp.AddSuffixStr(skymodules.ExtendedSuffix)
   557  			if err != nil {
   558  				r.staticLog.Critical("Failed to append ExtededSuffix to SiaPath", err)
   559  			}
   560  			if err := r.DeleteFile(sp); err != nil {
   561  				r.staticLog.Printf("WARN: failed to delete SiaPath %v: %v", sp.String(), err)
   562  			}
   563  			if err := r.DeleteFile(spFanout); err != nil && !errors.Contains(err, filesystem.ErrNotExist) {
   564  				r.staticLog.Printf("WARN: failed to delete extended SiaPath %v: %v", sp.String(), err)
   565  			}
   566  
   567  			// Remember the ID to later prune it from the store.
   568  			prunedIDs = append(prunedIDs, uploadID)
   569  
   570  			// Delete from ongoing uploads if it exists.
   571  			r.staticSkynetTUSUploader.mu.Lock()
   572  			ongoingUpload, exists := r.staticSkynetTUSUploader.ongoingUploads[uploadID]
   573  			if exists {
   574  				delete(r.staticSkynetTUSUploader.ongoingUploads, uploadID)
   575  				err = ongoingUpload.Close()
   576  				if err != nil {
   577  					r.staticLog.Critical("failed to close ongoing upload", err)
   578  				}
   579  			}
   580  			r.staticSkynetTUSUploader.mu.Unlock()
   581  		}
   582  
   583  		// Prune from the store.
   584  		if len(prunedIDs) > 0 {
   585  			err = r.staticSkynetTUSUploader.staticUploadStore.Prune(r.tg.StopCtx(), prunedIDs)
   586  			if err != nil {
   587  				r.staticLog.Printf("WARN: failed to prune %v uploads from store: %v", len(prunedIDs), err)
   588  			}
   589  		}
   590  	}
   591  }
   592  
   593  // TUSPreUploadCreateCallback is called before creating an upload. It is used to
   594  // dynamically check the maximum size of the user's upload according to a set
   595  // header field.
   596  func TUSPreUploadCreateCallback(hook handler.HookEvent) error {
   597  	// Sanity check that the size is not deferred.
   598  	if hook.Upload.SizeIsDeferred {
   599  		err := errors.New("uploads with deferred size are not supported")
   600  		return handler.NewHTTPError(err, http.StatusBadRequest)
   601  	}
   602  	// Get user's max upload size from request.
   603  	maxSizeStr := hook.HTTPRequest.Header.Get("SkynetMaxUploadSize")
   604  	if maxSizeStr == "" {
   605  		err := errors.New("SkynetMaxUploadSize header is missing")
   606  		return handler.NewHTTPError(err, http.StatusBadRequest)
   607  	}
   608  	var maxSize int64
   609  	_, err := fmt.Sscan(maxSizeStr, &maxSize)
   610  	if err != nil {
   611  		err = errors.AddContext(err, "failed to parse SkynetMaxUploadSize")
   612  		return handler.NewHTTPError(err, http.StatusBadRequest)
   613  	}
   614  	// Check upload size against max size.
   615  	if hook.Upload.Size > maxSize {
   616  		err = fmt.Errorf("upload exceeds maximum size: %v > %v", hook.Upload.Size, maxSize)
   617  		return handler.NewHTTPError(err, http.StatusRequestEntityTooLarge)
   618  	}
   619  	return nil
   620  }
   621  
   622  // ConcatUploads implements the handler.ConcatableUpload interface. It combines
   623  // the provided partial uploads into a single one.
   624  func (u *ongoingTUSUpload) ConcatUploads(ctx context.Context, partialUploads []handler.Upload) error {
   625  	// Get fileinfo.
   626  	fi, err := u.GetInfo(ctx)
   627  	if err != nil {
   628  		return errors.AddContext(err, "failed to fetch fileinfo")
   629  	}
   630  
   631  	// All partial uploads should be marked as such.
   632  	for _, pu := range partialUploads {
   633  		fi, err := pu.GetInfo(ctx)
   634  		if err != nil {
   635  			return errors.AddContext(err, "failed to fetch fileinfo")
   636  		}
   637  		if !fi.IsPartial {
   638  			return errors.New("can't concat non-partial uploads")
   639  		}
   640  	}
   641  
   642  	// Concatenate the uploads by combining their fanouts. Concatenated
   643  	// uploads may never consist of small uploads except for the last
   644  	// upload.
   645  	pu := partialUploads[0].(*ongoingTUSUpload)
   646  	sup, fup, err := u.staticUpload.UploadParams(ctx)
   647  	if err != nil {
   648  		return err
   649  	}
   650  	chunkSize := skymodules.ChunkSize(fup.CipherType, uint64(fup.ErasureCode.MinPieces()))
   651  	ec := fup.ErasureCode
   652  	masterKey := fup.CipherKey
   653  	var fanout []byte
   654  	for i := range partialUploads {
   655  		fi, err := pu.GetInfo(ctx)
   656  		if err != nil {
   657  			return errors.AddContext(err, "failed to get partial upload's fileinfo")
   658  		}
   659  		isSmall := fi.Size%int64(chunkSize) != 0
   660  		if i < len(partialUploads)-1 && isSmall {
   661  			return errors.New("only last upload is allowed to be small")
   662  		}
   663  		pu := partialUploads[i].(*ongoingTUSUpload)
   664  		_, fup, err := pu.staticUpload.UploadParams(ctx)
   665  		if err != nil {
   666  			return errors.AddContext(err, "failed to get partial upload's upload params")
   667  		}
   668  		if fup.ErasureCode.Identifier() != ec.Identifier() {
   669  			return errors.New("all partial uploads need to use the same erasure coding")
   670  		}
   671  		if fup.CipherType != masterKey.Type() {
   672  			return errors.New("all masterkeys need to have the same type")
   673  		}
   674  		if !bytes.Equal(fup.CipherKey.Key(), masterKey.Key()) {
   675  			return errors.New("all masterkeys need to be the same")
   676  		}
   677  		partialFanout, err := pu.staticUpload.Fanout(ctx)
   678  		if err != nil {
   679  			return errors.AddContext(err, "failed to fetch fanout of partial upload")
   680  		}
   681  		fanout = append(fanout, partialFanout...)
   682  	}
   683  	smBytes, err := u.staticUpload.SkyfileMetadata(ctx)
   684  	if err != nil {
   685  		return err
   686  	}
   687  	sup.SiaPath = skymodules.RandomSkynetFilePath()
   688  	skylink, err := u.finishUploadLarge(ctx, fanout, sup, fi, masterKey, ec, smBytes, true)
   689  	if err != nil {
   690  		return err
   691  	}
   692  
   693  	// Make sure the updates in mongo are atomic.
   694  	return u.staticUploader.staticUploadStore.WithTransaction(ctx, func(sctx context.Context) error {
   695  		// Upon success, we mark the partial uploads as complete to prevent them
   696  		// from being pruned.
   697  		for i := range partialUploads {
   698  			// Commit the partial upload as complete as well.
   699  			pu = partialUploads[i].(*ongoingTUSUpload)
   700  			err := pu.staticUpload.CommitFinishUpload(sctx, skylink)
   701  			if err != nil {
   702  				return errors.AddContext(err, "failed to commit partial upload")
   703  			}
   704  		}
   705  		return errors.AddContext(u.staticUpload.CommitFinishUpload(sctx, skylink), "failed to commit concatenated upload")
   706  	})
   707  }