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  }