github.com/rclone/rclone@v1.66.1-0.20240517100346-7b89735ae726/backend/box/upload.go (about) 1 // multipart upload for box 2 3 package box 4 5 import ( 6 "bytes" 7 "context" 8 "crypto/sha1" 9 "encoding/base64" 10 "encoding/json" 11 "errors" 12 "fmt" 13 "io" 14 "net/http" 15 "strconv" 16 "sync" 17 "time" 18 19 "github.com/rclone/rclone/backend/box/api" 20 "github.com/rclone/rclone/fs" 21 "github.com/rclone/rclone/fs/accounting" 22 "github.com/rclone/rclone/lib/atexit" 23 "github.com/rclone/rclone/lib/rest" 24 ) 25 26 // createUploadSession creates an upload session for the object 27 func (o *Object) createUploadSession(ctx context.Context, leaf, directoryID string, size int64) (response *api.UploadSessionResponse, err error) { 28 opts := rest.Opts{ 29 Method: "POST", 30 Path: "/files/upload_sessions", 31 RootURL: uploadURL, 32 } 33 request := api.UploadSessionRequest{ 34 FileSize: size, 35 } 36 // If object has an ID then it is existing so create a new version 37 if o.id != "" { 38 opts.Path = "/files/" + o.id + "/upload_sessions" 39 } else { 40 opts.Path = "/files/upload_sessions" 41 request.FolderID = directoryID 42 request.FileName = o.fs.opt.Enc.FromStandardName(leaf) 43 } 44 var resp *http.Response 45 err = o.fs.pacer.Call(func() (bool, error) { 46 resp, err = o.fs.srv.CallJSON(ctx, &opts, &request, &response) 47 return shouldRetry(ctx, resp, err) 48 }) 49 return 50 } 51 52 // sha1Digest produces a digest using sha1 as per RFC3230 53 func sha1Digest(digest []byte) string { 54 return "sha=" + base64.StdEncoding.EncodeToString(digest) 55 } 56 57 // uploadPart uploads a part in an upload session 58 func (o *Object) uploadPart(ctx context.Context, SessionID string, offset, totalSize int64, chunk []byte, wrap accounting.WrapFn, options ...fs.OpenOption) (response *api.UploadPartResponse, err error) { 59 chunkSize := int64(len(chunk)) 60 sha1sum := sha1.Sum(chunk) 61 opts := rest.Opts{ 62 Method: "PUT", 63 Path: "/files/upload_sessions/" + SessionID, 64 RootURL: uploadURL, 65 ContentType: "application/octet-stream", 66 ContentLength: &chunkSize, 67 ContentRange: fmt.Sprintf("bytes %d-%d/%d", offset, offset+chunkSize-1, totalSize), 68 Options: options, 69 ExtraHeaders: map[string]string{ 70 "Digest": sha1Digest(sha1sum[:]), 71 }, 72 } 73 var resp *http.Response 74 err = o.fs.pacer.Call(func() (bool, error) { 75 opts.Body = wrap(bytes.NewReader(chunk)) 76 resp, err = o.fs.srv.CallJSON(ctx, &opts, nil, &response) 77 return shouldRetry(ctx, resp, err) 78 }) 79 if err != nil { 80 return nil, err 81 } 82 return response, nil 83 } 84 85 // commitUpload finishes an upload session 86 func (o *Object) commitUpload(ctx context.Context, SessionID string, parts []api.Part, modTime time.Time, sha1sum []byte) (result *api.FolderItems, err error) { 87 opts := rest.Opts{ 88 Method: "POST", 89 Path: "/files/upload_sessions/" + SessionID + "/commit", 90 RootURL: uploadURL, 91 ExtraHeaders: map[string]string{ 92 "Digest": sha1Digest(sha1sum), 93 }, 94 } 95 request := api.CommitUpload{ 96 Parts: parts, 97 } 98 request.Attributes.ContentModifiedAt = api.Time(modTime) 99 request.Attributes.ContentCreatedAt = api.Time(modTime) 100 var body []byte 101 var resp *http.Response 102 // For discussion of this value see: 103 // https://github.com/rclone/rclone/issues/2054 104 maxTries := o.fs.opt.CommitRetries 105 const defaultDelay = 10 106 var tries int 107 outer: 108 for tries = 0; tries < maxTries; tries++ { 109 err = o.fs.pacer.Call(func() (bool, error) { 110 resp, err = o.fs.srv.CallJSON(ctx, &opts, &request, nil) 111 if err != nil { 112 return shouldRetry(ctx, resp, err) 113 } 114 body, err = rest.ReadBody(resp) 115 return shouldRetry(ctx, resp, err) 116 }) 117 delay := defaultDelay 118 var why string 119 if err != nil { 120 // Sometimes we get 400 Error with 121 // parts_mismatch immediately after uploading 122 // the last part. Ignore this error and wait. 123 if boxErr, ok := err.(*api.Error); ok && boxErr.Code == "parts_mismatch" { 124 why = err.Error() 125 } else { 126 return nil, err 127 } 128 } else { 129 switch resp.StatusCode { 130 case http.StatusOK, http.StatusCreated: 131 break outer 132 case http.StatusAccepted: 133 why = "not ready yet" 134 delayString := resp.Header.Get("Retry-After") 135 if delayString != "" { 136 delay, err = strconv.Atoi(delayString) 137 if err != nil { 138 fs.Debugf(o, "Couldn't decode Retry-After header %q: %v", delayString, err) 139 delay = defaultDelay 140 } 141 } 142 default: 143 return nil, fmt.Errorf("unknown HTTP status return %q (%d)", resp.Status, resp.StatusCode) 144 } 145 } 146 fs.Debugf(o, "commit multipart upload failed %d/%d - trying again in %d seconds (%s)", tries+1, maxTries, delay, why) 147 time.Sleep(time.Duration(delay) * time.Second) 148 } 149 if tries >= maxTries { 150 return nil, errors.New("too many tries to commit multipart upload - increase --low-level-retries") 151 } 152 err = json.Unmarshal(body, &result) 153 if err != nil { 154 return nil, fmt.Errorf("couldn't decode commit response: %q: %w", body, err) 155 } 156 return result, nil 157 } 158 159 // abortUpload cancels an upload session 160 func (o *Object) abortUpload(ctx context.Context, SessionID string) (err error) { 161 opts := rest.Opts{ 162 Method: "DELETE", 163 Path: "/files/upload_sessions/" + SessionID, 164 RootURL: uploadURL, 165 NoResponse: true, 166 } 167 var resp *http.Response 168 err = o.fs.pacer.Call(func() (bool, error) { 169 resp, err = o.fs.srv.Call(ctx, &opts) 170 return shouldRetry(ctx, resp, err) 171 }) 172 return err 173 } 174 175 // uploadMultipart uploads a file using multipart upload 176 func (o *Object) uploadMultipart(ctx context.Context, in io.Reader, leaf, directoryID string, size int64, modTime time.Time, options ...fs.OpenOption) (err error) { 177 // Create upload session 178 session, err := o.createUploadSession(ctx, leaf, directoryID, size) 179 if err != nil { 180 return fmt.Errorf("multipart upload create session failed: %w", err) 181 } 182 chunkSize := session.PartSize 183 fs.Debugf(o, "Multipart upload session started for %d parts of size %v", session.TotalParts, fs.SizeSuffix(chunkSize)) 184 185 // Cancel the session if something went wrong 186 defer atexit.OnError(&err, func() { 187 fs.Debugf(o, "Cancelling multipart upload: %v", err) 188 cancelErr := o.abortUpload(ctx, session.ID) 189 if cancelErr != nil { 190 fs.Logf(o, "Failed to cancel multipart upload: %v", cancelErr) 191 } 192 })() 193 194 // unwrap the accounting from the input, we use wrap to put it 195 // back on after the buffering 196 in, wrap := accounting.UnWrap(in) 197 198 // Upload the chunks 199 remaining := size 200 position := int64(0) 201 parts := make([]api.Part, session.TotalParts) 202 hash := sha1.New() 203 errs := make(chan error, 1) 204 var wg sync.WaitGroup 205 outer: 206 for part := 0; part < session.TotalParts; part++ { 207 // Check any errors 208 select { 209 case err = <-errs: 210 break outer 211 default: 212 } 213 214 reqSize := remaining 215 if reqSize >= chunkSize { 216 reqSize = chunkSize 217 } 218 219 // Make a block of memory 220 buf := make([]byte, reqSize) 221 222 // Read the chunk 223 _, err = io.ReadFull(in, buf) 224 if err != nil { 225 err = fmt.Errorf("multipart upload failed to read source: %w", err) 226 break outer 227 } 228 229 // Make the global hash (must be done sequentially) 230 _, _ = hash.Write(buf) 231 232 // Transfer the chunk 233 wg.Add(1) 234 o.fs.uploadToken.Get() 235 go func(part int, position int64) { 236 defer wg.Done() 237 defer o.fs.uploadToken.Put() 238 fs.Debugf(o, "Uploading part %d/%d offset %v/%v part size %v", part+1, session.TotalParts, fs.SizeSuffix(position), fs.SizeSuffix(size), fs.SizeSuffix(chunkSize)) 239 partResponse, err := o.uploadPart(ctx, session.ID, position, size, buf, wrap, options...) 240 if err != nil { 241 err = fmt.Errorf("multipart upload failed to upload part: %w", err) 242 select { 243 case errs <- err: 244 default: 245 } 246 return 247 } 248 parts[part] = partResponse.Part 249 }(part, position) 250 251 // ready for next block 252 remaining -= chunkSize 253 position += chunkSize 254 } 255 wg.Wait() 256 if err == nil { 257 select { 258 case err = <-errs: 259 default: 260 } 261 } 262 if err != nil { 263 return err 264 } 265 266 // Finalise the upload session 267 result, err := o.commitUpload(ctx, session.ID, parts, modTime, hash.Sum(nil)) 268 if err != nil { 269 return fmt.Errorf("multipart upload failed to finalize: %w", err) 270 } 271 272 if result.TotalCount != 1 || len(result.Entries) != 1 { 273 return fmt.Errorf("multipart upload failed %v - not sure why", o) 274 } 275 return o.setMetaData(&result.Entries[0]) 276 }