github.com/rclone/rclone@v1.66.1-0.20240517100346-7b89735ae726/lib/multipart/multipart.go (about)

     1  // Package multipart implements generic multipart uploading.
     2  package multipart
     3  
     4  import (
     5  	"context"
     6  	"fmt"
     7  	"io"
     8  	"sync"
     9  	"time"
    10  
    11  	"github.com/rclone/rclone/fs"
    12  	"github.com/rclone/rclone/fs/accounting"
    13  	"github.com/rclone/rclone/lib/atexit"
    14  	"github.com/rclone/rclone/lib/pacer"
    15  	"github.com/rclone/rclone/lib/pool"
    16  	"golang.org/x/sync/errgroup"
    17  )
    18  
    19  const (
    20  	bufferSize           = 1024 * 1024     // default size of the pages used in the reader
    21  	bufferCacheSize      = 64              // max number of buffers to keep in cache
    22  	bufferCacheFlushTime = 5 * time.Second // flush the cached buffers after this long
    23  )
    24  
    25  // bufferPool is a global pool of buffers
    26  var (
    27  	bufferPool     *pool.Pool
    28  	bufferPoolOnce sync.Once
    29  )
    30  
    31  // get a buffer pool
    32  func getPool() *pool.Pool {
    33  	bufferPoolOnce.Do(func() {
    34  		ci := fs.GetConfig(context.Background())
    35  		// Initialise the buffer pool when used
    36  		bufferPool = pool.New(bufferCacheFlushTime, bufferSize, bufferCacheSize, ci.UseMmap)
    37  	})
    38  	return bufferPool
    39  }
    40  
    41  // NewRW gets a pool.RW using the multipart pool
    42  func NewRW() *pool.RW {
    43  	return pool.NewRW(getPool())
    44  }
    45  
    46  // UploadMultipartOptions options for the generic multipart upload
    47  type UploadMultipartOptions struct {
    48  	Open        fs.OpenChunkWriter // thing to call OpenChunkWriter on
    49  	OpenOptions []fs.OpenOption    // options for OpenChunkWriter
    50  }
    51  
    52  // UploadMultipart does a generic multipart upload from src using f as OpenChunkWriter.
    53  //
    54  // in is read seqentially and chunks from it are uploaded in parallel.
    55  //
    56  // It returns the chunkWriter used in case the caller needs to extract any private info from it.
    57  func UploadMultipart(ctx context.Context, src fs.ObjectInfo, in io.Reader, opt UploadMultipartOptions) (chunkWriterOut fs.ChunkWriter, err error) {
    58  	info, chunkWriter, err := opt.Open.OpenChunkWriter(ctx, src.Remote(), src, opt.OpenOptions...)
    59  	if err != nil {
    60  		return nil, fmt.Errorf("multipart upload failed to initialise: %w", err)
    61  	}
    62  
    63  	// make concurrency machinery
    64  	concurrency := info.Concurrency
    65  	if concurrency < 1 {
    66  		concurrency = 1
    67  	}
    68  	tokens := pacer.NewTokenDispenser(concurrency)
    69  
    70  	uploadCtx, cancel := context.WithCancel(ctx)
    71  	defer cancel()
    72  	defer atexit.OnError(&err, func() {
    73  		cancel()
    74  		if info.LeavePartsOnError {
    75  			return
    76  		}
    77  		fs.Debugf(src, "Cancelling multipart upload")
    78  		errCancel := chunkWriter.Abort(ctx)
    79  		if errCancel != nil {
    80  			fs.Debugf(src, "Failed to cancel multipart upload: %v", errCancel)
    81  		}
    82  	})()
    83  
    84  	var (
    85  		g, gCtx   = errgroup.WithContext(uploadCtx)
    86  		finished  = false
    87  		off       int64
    88  		size      = src.Size()
    89  		chunkSize = info.ChunkSize
    90  	)
    91  
    92  	// Do the accounting manually
    93  	in, acc := accounting.UnWrapAccounting(in)
    94  
    95  	for partNum := int64(0); !finished; partNum++ {
    96  		// Get a block of memory from the pool and token which limits concurrency.
    97  		tokens.Get()
    98  		rw := NewRW()
    99  		if acc != nil {
   100  			rw.SetAccounting(acc.AccountRead)
   101  		}
   102  
   103  		free := func() {
   104  			// return the memory and token
   105  			_ = rw.Close() // Can't return an error
   106  			tokens.Put()
   107  		}
   108  
   109  		// Fail fast, in case an errgroup managed function returns an error
   110  		// gCtx is cancelled. There is no point in uploading all the other parts.
   111  		if gCtx.Err() != nil {
   112  			free()
   113  			break
   114  		}
   115  
   116  		// Read the chunk
   117  		var n int64
   118  		n, err = io.CopyN(rw, in, chunkSize)
   119  		if err == io.EOF {
   120  			if n == 0 && partNum != 0 { // end if no data and if not first chunk
   121  				free()
   122  				break
   123  			}
   124  			finished = true
   125  		} else if err != nil {
   126  			free()
   127  			return nil, fmt.Errorf("multipart upload: failed to read source: %w", err)
   128  		}
   129  
   130  		partNum := partNum
   131  		partOff := off
   132  		off += n
   133  		g.Go(func() (err error) {
   134  			defer free()
   135  			fs.Debugf(src, "multipart upload: starting chunk %d size %v offset %v/%v", partNum, fs.SizeSuffix(n), fs.SizeSuffix(partOff), fs.SizeSuffix(size))
   136  			_, err = chunkWriter.WriteChunk(gCtx, int(partNum), rw)
   137  			return err
   138  		})
   139  	}
   140  
   141  	err = g.Wait()
   142  	if err != nil {
   143  		return nil, err
   144  	}
   145  
   146  	err = chunkWriter.Close(ctx)
   147  	if err != nil {
   148  		return nil, fmt.Errorf("multipart upload: failed to finalise: %w", err)
   149  	}
   150  
   151  	return chunkWriter, nil
   152  }