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  }