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 }