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