88 "crypto/sha256"
99 "encoding/base64"
1010 "encoding/hex"
11+ "errors"
12+ "fmt"
1113 "io"
1214 "mime"
1315 "os"
@@ -19,14 +21,154 @@ import (
1921 "github.com/rclone/go-proton-api"
2022)
2123
22- func collectUploadErrors (errChan <- chan error , count int ) error {
23- var firstErr error
24- for range count {
25- if err := <- errChan ; err != nil && firstErr == nil {
26- firstErr = err
24+ const (
25+ blockUploadMaxAttempts = 5
26+ blockUploadRetryBaseDelay = time .Second
27+ blockUploadRetryMaxDelay = 15 * time .Second
28+ )
29+
30+ type pendingUploadBlock struct {
31+ blockUploadInfo proton.BlockUploadInfo
32+ encData []byte
33+ }
34+
35+ type blockUploadResult struct {
36+ index int
37+ err error
38+ }
39+
40+ type blockUploadRetryLogger interface {
41+ Warnf (format string , v ... interface {})
42+ }
43+
44+ func retryableBlockUploadError (err error ) bool {
45+ if err == nil || errors .Is (err , context .Canceled ) || errors .Is (err , context .DeadlineExceeded ) {
46+ return false
47+ }
48+
49+ var apiErr * proton.APIError
50+ if errors .As (err , & apiErr ) {
51+ return apiErr .Status >= 500 && apiErr .Status <= 599
52+ }
53+
54+ var protonNetErr * proton.NetError
55+ return errors .As (err , & protonNetErr )
56+ }
57+
58+ func blockUploadRetryDelay (failedAttempt int ) time.Duration {
59+ delay := blockUploadRetryBaseDelay
60+ for i := 1 ; i < failedAttempt && delay < blockUploadRetryMaxDelay ; i ++ {
61+ delay *= 2
62+ }
63+ if delay > blockUploadRetryMaxDelay {
64+ return blockUploadRetryMaxDelay
65+ }
66+ return delay
67+ }
68+
69+ func waitForBlockUploadRetry (ctx context.Context , delay time.Duration ) error {
70+ timer := time .NewTimer (delay )
71+ defer timer .Stop ()
72+ select {
73+ case <- ctx .Done ():
74+ return ctx .Err ()
75+ case <- timer .C :
76+ return nil
77+ }
78+ }
79+
80+ func uploadBlockBatchWithRetry (
81+ ctx context.Context ,
82+ blocks []pendingUploadBlock ,
83+ maxAttempts int ,
84+ requestLinks func (context.Context , []proton.BlockUploadInfo ) ([]proton.BlockUploadLink , error ),
85+ uploadBlock func (context.Context , proton.BlockUploadLink , []byte ) error ,
86+ wait func (context.Context , time.Duration ) error ,
87+ logger blockUploadRetryLogger ,
88+ ) error {
89+ remaining := append ([]pendingUploadBlock (nil ), blocks ... )
90+ var lastErr error
91+
92+ for attempt := 1 ; attempt <= maxAttempts ; attempt ++ {
93+ blockList := make ([]proton.BlockUploadInfo , len (remaining ))
94+ for i := range remaining {
95+ blockList [i ] = remaining [i ].blockUploadInfo
96+ }
97+
98+ links , err := requestLinks (ctx , blockList )
99+ if err != nil {
100+ lastErr = err
101+ if ! retryableBlockUploadError (err ) || attempt == maxAttempts {
102+ return err
103+ }
104+ } else {
105+ if len (links ) != len (remaining ) {
106+ return fmt .Errorf (
107+ "requested %d Proton block upload links, received %d" ,
108+ len (remaining ),
109+ len (links ),
110+ )
111+ }
112+
113+ results := make (chan blockUploadResult , len (remaining ))
114+ for i := range remaining {
115+ go func (index int ) {
116+ results <- blockUploadResult {
117+ index : index ,
118+ err : uploadBlock (ctx , links [index ], remaining [index ].encData ),
119+ }
120+ }(i )
121+ }
122+
123+ errorsByIndex := make ([]error , len (remaining ))
124+ for range remaining {
125+ result := <- results
126+ errorsByIndex [result .index ] = result .err
127+ }
128+
129+ failed := make ([]pendingUploadBlock , 0 , len (remaining ))
130+ var terminalErr error
131+ lastErr = nil
132+ for i , uploadErr := range errorsByIndex {
133+ if uploadErr == nil {
134+ continue
135+ }
136+ if ! retryableBlockUploadError (uploadErr ) && terminalErr == nil {
137+ terminalErr = uploadErr
138+ }
139+ if lastErr == nil {
140+ lastErr = uploadErr
141+ }
142+ failed = append (failed , remaining [i ])
143+ }
144+ if terminalErr != nil {
145+ return terminalErr
146+ }
147+ if len (failed ) == 0 {
148+ return nil
149+ }
150+ if attempt == maxAttempts {
151+ return lastErr
152+ }
153+ remaining = failed
154+ }
155+
156+ delay := blockUploadRetryDelay (attempt )
157+ if logger != nil {
158+ logger .Warnf (
159+ "Retrying %d transient Proton block upload(s) after %s (attempt %d/%d)" ,
160+ len (remaining ),
161+ delay ,
162+ attempt + 1 ,
163+ maxAttempts ,
164+ )
165+ }
166+ if err := wait (ctx , delay ); err != nil {
167+ return err
27168 }
28169 }
29- return firstErr
170+
171+ return lastErr
30172}
31173
32174func (protonDrive * ProtonDrive ) handleRevisionConflict (ctx context.Context , link * proton.Link , createFileResp * proton.CreateFileRes ) (string , bool , error ) {
@@ -267,56 +409,45 @@ func (protonDrive *ProtonDrive) createFileUploadDraft(ctx context.Context, paren
267409}
268410
269411func (protonDrive * ProtonDrive ) uploadAndCollectBlockData (ctx context.Context , newSessionKey * crypto.SessionKey , newNodeKR * crypto.KeyRing , file io.Reader , linkID , revisionID string ) ([]byte , int64 , []int64 , string , error ) {
270- type PendingUploadBlocks struct {
271- blockUploadInfo proton.BlockUploadInfo
272- encData []byte
273- }
274-
275412 if newSessionKey == nil || newNodeKR == nil {
276413 return nil , 0 , nil , "" , ErrMissingInputUploadAndCollectBlockData
277414 }
278415
279416 totalFileSize := int64 (0 )
280417
281- pendingUploadBlocks := make ([]PendingUploadBlocks , 0 )
418+ pendingUploadBlocks := make ([]pendingUploadBlock , 0 )
282419 manifestSignatureData := make ([]byte , 0 )
283420 uploadPendingBlocks := func () error {
284421 if len (pendingUploadBlocks ) == 0 {
285422 return nil
286423 }
287424
288- blockList := make ([]proton.BlockUploadInfo , 0 )
289- for i := range pendingUploadBlocks {
290- blockList = append (blockList , pendingUploadBlocks [i ].blockUploadInfo )
291- }
292- blockUploadReq := proton.BlockUploadReq {
293- AddressID : protonDrive .MainShare .AddressID ,
294- ShareID : protonDrive .MainShare .ShareID ,
295- LinkID : linkID ,
296- RevisionID : revisionID ,
297-
298- BlockList : blockList ,
425+ requestLinks := func (ctx context.Context , blockList []proton.BlockUploadInfo ) ([]proton.BlockUploadLink , error ) {
426+ return protonDrive .c .RequestBlockUpload (ctx , proton.BlockUploadReq {
427+ AddressID : protonDrive .MainShare .AddressID ,
428+ ShareID : protonDrive .MainShare .ShareID ,
429+ LinkID : linkID ,
430+ RevisionID : revisionID ,
431+ BlockList : blockList ,
432+ })
299433 }
300- blockUploadResp , err := protonDrive .c .RequestBlockUpload (ctx , blockUploadReq )
301- if err != nil {
302- return err
303- }
304-
305- errChan := make (chan error , len (blockUploadResp ))
306- uploadBlockWrapper := func (ctx context.Context , errChan chan error , bareURL , token string , block io.Reader ) {
434+ uploadBlock := func (ctx context.Context , link proton.BlockUploadLink , block []byte ) error {
307435 if err := protonDrive .blockUploadSemaphore .Acquire (ctx , 1 ); err != nil {
308- errChan <- err
309- return
436+ return err
310437 }
311438 defer protonDrive .blockUploadSemaphore .Release (1 )
312439
313- errChan <- protonDrive .c .UploadBlock (ctx , bareURL , token , block )
314- }
315- for i := range blockUploadResp {
316- go uploadBlockWrapper (ctx , errChan , blockUploadResp [i ].BareURL , blockUploadResp [i ].Token , bytes .NewReader (pendingUploadBlocks [i ].encData ))
440+ return protonDrive .c .UploadBlock (ctx , link .BareURL , link .Token , bytes .NewReader (block ))
317441 }
318-
319- if err := collectUploadErrors (errChan , len (blockUploadResp )); err != nil {
442+ if err := uploadBlockBatchWithRetry (
443+ ctx ,
444+ pendingUploadBlocks ,
445+ blockUploadMaxAttempts ,
446+ requestLinks ,
447+ uploadBlock ,
448+ waitForBlockUploadRetry ,
449+ protonDrive .Config .GetLogger (),
450+ ); err != nil {
320451 return err
321452 }
322453
@@ -410,7 +541,7 @@ func (protonDrive *ProtonDrive) uploadAndCollectBlockData(ctx context.Context, n
410541 }
411542 manifestSignatureData = append (manifestSignatureData , hash ... )
412543
413- pendingUploadBlocks = append (pendingUploadBlocks , PendingUploadBlocks {
544+ pendingUploadBlocks = append (pendingUploadBlocks , pendingUploadBlock {
414545 blockUploadInfo : proton.BlockUploadInfo {
415546 Index : i , // iOS drive: BE starts with 1
416547 Size : int64 (len (encData )),
0 commit comments