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