github.com/10XDev/rclone@v1.52.3-0.20200626220027-16af9ab76b2a/backend/sharefile/upload.go (about) 1 // Upload large files for sharefile 2 // 3 // Docs - https://api.sharefile.com/rest/docs/resource.aspx?name=Items#Upload_File 4 5 package sharefile 6 7 import ( 8 "bytes" 9 "context" 10 "crypto/md5" 11 "encoding/hex" 12 "encoding/json" 13 "fmt" 14 "io" 15 "strings" 16 "sync" 17 18 "github.com/pkg/errors" 19 "github.com/rclone/rclone/backend/sharefile/api" 20 "github.com/rclone/rclone/fs" 21 "github.com/rclone/rclone/fs/accounting" 22 "github.com/rclone/rclone/lib/readers" 23 "github.com/rclone/rclone/lib/rest" 24 ) 25 26 // largeUpload is used to control the upload of large files which need chunking 27 type largeUpload struct { 28 ctx context.Context 29 f *Fs // parent Fs 30 o *Object // object being uploaded 31 in io.Reader // read the data from here 32 wrap accounting.WrapFn // account parts being transferred 33 size int64 // total size 34 parts int64 // calculated number of parts, if known 35 info *api.UploadSpecification // where to post chunks etc 36 threads int // number of threads to use in upload 37 streamed bool // set if using streamed upload 38 } 39 40 // newLargeUpload starts an upload of object o from in with metadata in src 41 func (f *Fs) newLargeUpload(ctx context.Context, o *Object, in io.Reader, src fs.ObjectInfo, info *api.UploadSpecification) (up *largeUpload, err error) { 42 size := src.Size() 43 parts := int64(-1) 44 if size >= 0 { 45 parts = size / int64(o.fs.opt.ChunkSize) 46 if size%int64(o.fs.opt.ChunkSize) != 0 { 47 parts++ 48 } 49 } 50 51 var streamed bool 52 switch strings.ToLower(info.Method) { 53 case "streamed": 54 streamed = true 55 case "threaded": 56 streamed = false 57 default: 58 return nil, errors.Errorf("can't use method %q with newLargeUpload", info.Method) 59 } 60 61 threads := fs.Config.Transfers 62 if threads > info.MaxNumberOfThreads { 63 threads = info.MaxNumberOfThreads 64 } 65 66 // unwrap the accounting from the input, we use wrap to put it 67 // back on after the buffering 68 in, wrap := accounting.UnWrap(in) 69 up = &largeUpload{ 70 ctx: ctx, 71 f: f, 72 o: o, 73 in: in, 74 wrap: wrap, 75 size: size, 76 threads: threads, 77 info: info, 78 parts: parts, 79 streamed: streamed, 80 } 81 return up, nil 82 } 83 84 // parse the api.UploadFinishResponse in respBody 85 func (up *largeUpload) parseUploadFinishResponse(respBody []byte) (err error) { 86 var finish api.UploadFinishResponse 87 err = json.Unmarshal(respBody, &finish) 88 if err != nil { 89 // Sometimes the unmarshal fails in which case return the body 90 return errors.Errorf("upload: bad response: %q", bytes.TrimSpace(respBody)) 91 } 92 return up.o.checkUploadResponse(up.ctx, &finish) 93 } 94 95 // Transfer a chunk 96 func (up *largeUpload) transferChunk(ctx context.Context, part int64, offset int64, body []byte, fileHash string) error { 97 md5sumRaw := md5.Sum(body) 98 md5sum := hex.EncodeToString(md5sumRaw[:]) 99 size := int64(len(body)) 100 101 // Add some more parameters to the ChunkURI 102 u := up.info.ChunkURI 103 u += fmt.Sprintf("&index=%d&byteOffset=%d&hash=%s&fmt=json", 104 part, offset, md5sum, 105 ) 106 if fileHash != "" { 107 u += fmt.Sprintf("&finish=true&fileSize=%d&fileHash=%s", 108 offset+int64(len(body)), 109 fileHash, 110 ) 111 } 112 opts := rest.Opts{ 113 Method: "POST", 114 RootURL: u, 115 ContentLength: &size, 116 } 117 var respBody []byte 118 err := up.f.pacer.Call(func() (bool, error) { 119 fs.Debugf(up.o, "Sending chunk %d length %d", part, len(body)) 120 opts.Body = up.wrap(bytes.NewReader(body)) 121 resp, err := up.f.srv.Call(ctx, &opts) 122 if err != nil { 123 fs.Debugf(up.o, "Error sending chunk %d: %v", part, err) 124 } else { 125 respBody, err = rest.ReadBody(resp) 126 } 127 // retry all errors now that the multipart upload has started 128 return err != nil, err 129 }) 130 if err != nil { 131 fs.Debugf(up.o, "Error sending chunk %d: %v", part, err) 132 return err 133 } 134 // If last chunk and using "streamed" transfer, get the response back now 135 if up.streamed && fileHash != "" { 136 return up.parseUploadFinishResponse(respBody) 137 } 138 fs.Debugf(up.o, "Done sending chunk %d", part) 139 return nil 140 } 141 142 // finish closes off the large upload and reads the metadata 143 func (up *largeUpload) finish(ctx context.Context) error { 144 fs.Debugf(up.o, "Finishing large file upload") 145 // For a streamed transfer we will already have read the info 146 if up.streamed { 147 return nil 148 } 149 150 opts := rest.Opts{ 151 Method: "POST", 152 RootURL: up.info.FinishURI, 153 } 154 var respBody []byte 155 err := up.f.pacer.Call(func() (bool, error) { 156 resp, err := up.f.srv.Call(ctx, &opts) 157 if err != nil { 158 return shouldRetry(resp, err) 159 } 160 respBody, err = rest.ReadBody(resp) 161 // retry all errors now that the multipart upload has started 162 return err != nil, err 163 }) 164 if err != nil { 165 return err 166 } 167 return up.parseUploadFinishResponse(respBody) 168 } 169 170 // Upload uploads the chunks from the input 171 func (up *largeUpload) Upload(ctx context.Context) error { 172 if up.parts >= 0 { 173 fs.Debugf(up.o, "Starting upload of large file in %d chunks", up.parts) 174 } else { 175 fs.Debugf(up.o, "Starting streaming upload of large file") 176 } 177 var ( 178 offset int64 179 errs = make(chan error, 1) 180 wg sync.WaitGroup 181 err error 182 wholeFileHash = md5.New() 183 eof = false 184 ) 185 outer: 186 for part := int64(0); !eof; part++ { 187 // Check any errors 188 select { 189 case err = <-errs: 190 break outer 191 default: 192 } 193 194 // Get a block of memory 195 buf := up.f.getUploadBlock() 196 197 // Read the chunk 198 var n int 199 n, err = readers.ReadFill(up.in, buf) 200 if err == io.EOF { 201 eof = true 202 buf = buf[:n] 203 err = nil 204 } else if err != nil { 205 up.f.putUploadBlock(buf) 206 break outer 207 } 208 209 // Hash it 210 _, _ = io.Copy(wholeFileHash, bytes.NewBuffer(buf)) 211 212 // Get file hash if was last chunk 213 fileHash := "" 214 if eof { 215 fileHash = hex.EncodeToString(wholeFileHash.Sum(nil)) 216 } 217 218 // Transfer the chunk 219 wg.Add(1) 220 transferChunk := func(part, offset int64, buf []byte, fileHash string) { 221 defer wg.Done() 222 defer up.f.putUploadBlock(buf) 223 err := up.transferChunk(ctx, part, offset, buf, fileHash) 224 if err != nil { 225 select { 226 case errs <- err: 227 default: 228 } 229 } 230 } 231 if up.streamed { 232 transferChunk(part, offset, buf, fileHash) // streamed 233 } else { 234 go transferChunk(part, offset, buf, fileHash) // multithreaded 235 } 236 237 offset += int64(n) 238 } 239 wg.Wait() 240 241 // check size read is correct 242 if eof && err == nil && up.size >= 0 && up.size != offset { 243 err = errors.Errorf("upload: short read: read %d bytes expected %d", up.size, offset) 244 } 245 246 // read any errors 247 if err == nil { 248 select { 249 case err = <-errs: 250 default: 251 } 252 } 253 254 // finish regardless of errors 255 finishErr := up.finish(ctx) 256 if err == nil { 257 err = finishErr 258 } 259 260 return err 261 }