github.com/rclone/rclone@v1.66.1-0.20240517100346-7b89735ae726/backend/box/upload.go (about)

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