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  }