github.com/Files-com/files-sdk-go/v2@v2.1.2/file/uploadio.go (about)

     1  package file
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"errors"
     7  	"io"
     8  	"net/http"
     9  	"net/url"
    10  	"os"
    11  	"strconv"
    12  	"strings"
    13  	"sync/atomic"
    14  	"time"
    15  
    16  	files_sdk "github.com/Files-com/files-sdk-go/v2"
    17  	"github.com/Files-com/files-sdk-go/v2/file/manager"
    18  	"github.com/Files-com/files-sdk-go/v2/file/status"
    19  	"github.com/Files-com/files-sdk-go/v2/lib"
    20  )
    21  
    22  type Progress func(int64)
    23  
    24  type Len interface {
    25  	Len() int
    26  }
    27  
    28  type UploadResumable struct {
    29  	files_sdk.FileUploadPart
    30  	Parts
    31  	files_sdk.File
    32  }
    33  
    34  type uploadIO struct {
    35  	ByteOffset
    36  	Path     string
    37  	reader   io.Reader
    38  	readerAt io.ReaderAt
    39  	Size     *int64
    40  	Progress
    41  	Manager       lib.ConcurrencyManagerWithSubWorker
    42  	ProvidedMtime time.Time
    43  	Parts
    44  	files_sdk.FileUploadPart
    45  	MkdirParents    *bool
    46  	passedInContext context.Context
    47  
    48  	onComplete    chan *Part
    49  	partsFinished chan struct{}
    50  	bytesWritten  int64
    51  	*Client
    52  	etags                      []files_sdk.EtagsParam
    53  	file                       files_sdk.File
    54  	RewindAllProgressOnFailure bool
    55  	disableParallelParts       bool
    56  	notResumable               *atomic.Bool
    57  }
    58  
    59  func (u *uploadIO) Run(ctx context.Context) (UploadResumable, error) {
    60  	u.notResumable = &atomic.Bool{}
    61  	u.notResumable.Store(false)
    62  	if u.Path == "" {
    63  		return u.UploadResumable(), errors.New("UploadWithDestinationPath is required")
    64  	}
    65  
    66  	if u.reader == nil && u.readerAt == nil {
    67  		return u.UploadResumable(), errors.New("UploadWithReader or UploadWithReaderAt required")
    68  	}
    69  
    70  	u.onComplete = make(chan *Part)
    71  	u.partsFinished = make(chan struct{})
    72  	u.etags = make([]files_sdk.EtagsParam, 0)
    73  	if u.Manager == nil {
    74  		u.Manager = manager.Sync().FilePartsManager
    75  	}
    76  	if u.Progress == nil {
    77  		u.Progress = func(i int64) {}
    78  	}
    79  
    80  	var defaultSize int64
    81  	if u.Size != nil {
    82  		defaultSize = *u.Size
    83  	}
    84  
    85  	var expires time.Time
    86  	var err error
    87  	if u.FileUploadPart.Expires != "" {
    88  		expires, _ = time.Parse(time.RFC3339, u.FileUploadPart.Expires)
    89  	}
    90  	if !time.Now().Before(expires) || !lib.UnWrapBool(u.FileUploadPart.ParallelParts) {
    91  		u.Parts = Parts{} // parts are invalidated
    92  	}
    93  
    94  	if u.MkdirParents == nil {
    95  		u.MkdirParents = lib.Bool(true)
    96  	}
    97  
    98  	if expires.IsZero() || !time.Now().Before(expires) {
    99  		if u.Manager.WaitWithContext(ctx) {
   100  			u.FileUploadPart, err = u.startUpload(
   101  				ctx,
   102  				files_sdk.FileBeginUploadParams{
   103  					Size:         defaultSize,
   104  					Path:         u.Path,
   105  					MkdirParents: u.MkdirParents,
   106  				},
   107  			)
   108  			u.Manager.Done()
   109  		} else {
   110  			return u.UploadResumable(), ctx.Err()
   111  		}
   112  		if err != nil {
   113  			return u.UploadResumable(), err
   114  		}
   115  	}
   116  
   117  	partCtx, cancel := context.WithCancelCause(ctx)
   118  
   119  	u.FileUploadPart.Path = u.Path
   120  	go u.uploadParts(partCtx)
   121  	return u.waitOnParts(ctx, cancel)
   122  }
   123  
   124  func (u *uploadIO) UploadResumable() UploadResumable {
   125  	if u.notResumable.Load() {
   126  		return UploadResumable{File: u.file}
   127  	}
   128  	return UploadResumable{Parts: u.Parts, FileUploadPart: u.FileUploadPart, File: u.file}
   129  }
   130  
   131  func (u *uploadIO) rewindSuccessfulParts() {
   132  	if !u.RewindAllProgressOnFailure {
   133  		return
   134  	}
   135  	for _, part := range u.Parts {
   136  		if part.Successful() {
   137  			u.Progress(-part.bytes)
   138  		}
   139  	}
   140  }
   141  
   142  func (u *uploadIO) waitOnParts(ctx context.Context, cancelParts context.CancelCauseFunc) (UploadResumable, error) {
   143  	var allErrors error
   144  	for {
   145  		select {
   146  		case <-u.partsFinished:
   147  			close(u.onComplete)
   148  			if allErrors != nil {
   149  				u.rewindSuccessfulParts()
   150  				return u.UploadResumable(), allErrors
   151  			}
   152  			// Rate limit all outgoing connections
   153  			if u.Manager.WaitWithContext(ctx) {
   154  				var err error
   155  				u.file, err = u.completeUpload(ctx, &u.ProvidedMtime, u.etags, u.bytesWritten, u.Path, u.FileUploadPart.Ref)
   156  				u.Manager.Done()
   157  				if err != nil {
   158  					u.rewindSuccessfulParts()
   159  				}
   160  				return u.UploadResumable(), err
   161  			} else {
   162  				u.rewindSuccessfulParts()
   163  				return u.UploadResumable(), ctx.Err()
   164  			}
   165  		case part := <-u.onComplete:
   166  			if part.error == nil {
   167  				u.etags = append(u.etags, part.EtagsParam)
   168  				u.bytesWritten += part.bytes
   169  			} else {
   170  				u.Progress(-part.bytes)
   171  				allErrors = errors.Join(allErrors, part.error)
   172  
   173  				if strings.Contains(part.Error(), "File Upload Not Found") {
   174  					cancelParts(part.error)
   175  
   176  					u.notResumable.Store(true)
   177  				}
   178  			}
   179  		}
   180  	}
   181  }
   182  
   183  func (u *uploadIO) Reader() (io.Reader, bool) {
   184  	return u.reader, u.reader != nil
   185  }
   186  
   187  func (u *uploadIO) ReaderAt() (io.ReaderAt, bool) {
   188  	if u.readerAt != nil {
   189  		return u.readerAt, true
   190  	}
   191  	readerAt, ok := u.reader.(io.ReaderAt)
   192  	return readerAt, ok
   193  }
   194  
   195  func (u *uploadIO) partBuilder(offset OffSet, final bool, number int) *Part {
   196  	part := &Part{OffSet: offset, number: number, final: final, ProxyReader: u.buildReader(offset)}
   197  	// Parts are stored so a retry can pick up failed parts. Since io.Reader is a stream is better to just retry the whole file
   198  	if _, readerAtOk := u.ReaderAt(); readerAtOk && u.Size != nil {
   199  		u.Parts = append(u.Parts, part)
   200  	}
   201  
   202  	return part
   203  }
   204  
   205  func (u *uploadIO) buildReader(offset OffSet) ProxyReader {
   206  	var readerAt io.ReaderAt
   207  	var readerAtOk bool
   208  
   209  	if readerAt, readerAtOk = u.ReaderAt(); !readerAtOk {
   210  		u.disableParallelParts = true
   211  	}
   212  
   213  	if u.Size == nil {
   214  		if readerAtOk {
   215  			sectionReader := io.NewSectionReader(readerAt, offset.off, offset.len)
   216  			buf := new(bytes.Buffer)
   217  
   218  			// Use io.CopyN to copy up to fiveMB into buf
   219  			n, err := io.CopyN(buf, sectionReader, offset.len)
   220  			if err != nil && err != io.EOF {
   221  				// handle error
   222  			}
   223  
   224  			return &ProxyRead{
   225  				Reader: buf,
   226  				len:    n,
   227  				onRead: u.Progress,
   228  			}
   229  		} else {
   230  			buf := new(bytes.Buffer)
   231  
   232  			// Use io.CopyN to copy up to fiveMB into buf
   233  			reader, _ := u.Reader()
   234  			n, err := io.CopyN(buf, reader, offset.len)
   235  			if err != nil && err != io.EOF {
   236  				// handle error
   237  			}
   238  
   239  			return &ProxyRead{
   240  				Reader: buf,
   241  				len:    n,
   242  				onRead: u.Progress,
   243  			}
   244  		}
   245  	}
   246  
   247  	if readerAtOk {
   248  		return &ProxyReaderAt{
   249  			ReaderAt: readerAt,
   250  			off:      offset.off,
   251  			len:      offset.len,
   252  			onRead:   u.Progress,
   253  		}
   254  	} else {
   255  		reader, _ := u.Reader()
   256  		return &ProxyRead{
   257  			Reader: reader,
   258  			len:    offset.len,
   259  			onRead: u.Progress,
   260  		}
   261  	}
   262  }
   263  
   264  type PartRunnerReturn int
   265  
   266  func (u *uploadIO) manageUpdatePart(ctx context.Context, part *Part, wait lib.ConcurrencyManager) bool {
   267  	if wait.WaitWithContext(ctx) {
   268  		if *u.FileUploadPart.ParallelParts && !u.disableParallelParts && u.Size != nil {
   269  			go func() {
   270  				u.runUploadPart(ctx, part)
   271  				wait.Done()
   272  			}()
   273  		} else {
   274  			u.runUploadPart(ctx, part)
   275  			wait.Done()
   276  		}
   277  		if part.ProxyReader.Len() != int(part.len) {
   278  			return true
   279  		}
   280  	} else {
   281  		return true
   282  	}
   283  
   284  	return false
   285  }
   286  
   287  func (u *uploadIO) runUploadPart(ctx context.Context, part *Part) {
   288  	fileUploadPart := u.FileUploadPart
   289  	fileUploadPart.PartNumber = int64(part.number)
   290  	maxRetries := 2
   291  	retryCount := 0
   292  
   293  	for retryCount <= maxRetries {
   294  		part.EtagsParam, part.error = u.createPart(ctx, part.ProxyReader, int64(part.ProxyReader.Len()), fileUploadPart, part.final)
   295  
   296  		if part.error == nil {
   297  			break
   298  		}
   299  
   300  		var pathErr *os.PathError
   301  		if errors.As(part.error, &pathErr) {
   302  			part.error = pathErr
   303  		}
   304  
   305  		var s3Err lib.S3Error
   306  		expires, _ := time.Parse(time.RFC3339, u.FileUploadPart.Expires)
   307  		if errors.As(part.error, &s3Err) && part.ProxyReader.Rewind() && time.Now().Before(expires) {
   308  			if s3Err.Code == "RequestTimeout" {
   309  				retryCount++
   310  				if retryCount > maxRetries {
   311  					break
   312  				}
   313  				u.LogPath(u.Path, map[string]interface{}{"error": s3Err.Error(), "message": "retrying"})
   314  				continue
   315  			}
   316  		}
   317  		break
   318  	}
   319  
   320  	part.bytes = int64(part.ProxyReader.BytesRead())
   321  	part.Touch()
   322  	u.onComplete <- part
   323  }
   324  
   325  func (u *uploadIO) uploadParts(ctx context.Context) {
   326  	wait := u.Manager.NewSubWorker()
   327  	defer func() {
   328  		wait.WaitAllDone()
   329  		u.partsFinished <- struct{}{}
   330  	}()
   331  
   332  	for _, part := range u.Parts {
   333  		if part.Successful() {
   334  			u.Progress(part.bytes)
   335  			u.onComplete <- part
   336  		} else {
   337  			part.Clear()
   338  			part.ProxyReader = u.buildReader(part.OffSet)
   339  
   340  			if u.manageUpdatePart(ctx, part, wait) {
   341  				break
   342  			}
   343  		}
   344  	}
   345  	var (
   346  		offset   OffSet
   347  		iterator Iterator
   348  		index    int
   349  	)
   350  	if len(u.Parts) > 0 {
   351  		lastPart := u.Parts[len(u.Parts)-1]
   352  		iterator = u.ByteOffset.Resume(u.Size, lastPart.off, lastPart.number-1)
   353  		offset, iterator, index = iterator()
   354  	} else {
   355  		iterator = u.ByteOffset.BySize(u.Size)
   356  	}
   357  
   358  	for {
   359  		if iterator == nil {
   360  			break
   361  		}
   362  
   363  		offset, iterator, index = iterator()
   364  
   365  		if u.manageUpdatePart(
   366  			ctx,
   367  			u.partBuilder(
   368  				offset,
   369  				iterator == nil && u.Size != nil,
   370  				index+1,
   371  			),
   372  			wait,
   373  		) {
   374  			break
   375  		}
   376  	}
   377  }
   378  
   379  func (u *uploadIO) startUpload(ctx context.Context, beginUpload files_sdk.FileBeginUploadParams) (files_sdk.FileUploadPart, error) {
   380  	uploads, err := u.BeginUpload(beginUpload, files_sdk.WithContext(ctx))
   381  	if err != nil {
   382  		return files_sdk.FileUploadPart{}, err
   383  	}
   384  	return uploads[0], err
   385  }
   386  
   387  func (u *uploadIO) completeUpload(ctx context.Context, providedMtime *time.Time, etags []files_sdk.EtagsParam, bytesWritten int64, path string, ref string) (files_sdk.File, error) {
   388  	if providedMtime.IsZero() {
   389  		providedMtime = nil
   390  	}
   391  
   392  	return u.Create(files_sdk.FileCreateParams{
   393  		ProvidedMtime: providedMtime,
   394  		EtagsParam:    etags,
   395  		Action:        "end",
   396  		Path:          path,
   397  		Ref:           ref,
   398  		Size:          bytesWritten,
   399  		MkdirParents:  lib.Bool(true),
   400  	}, files_sdk.WithContext(ctx))
   401  }
   402  
   403  func (u *uploadIO) createPart(ctx context.Context, reader io.ReadCloser, len int64, fileUploadPart files_sdk.FileUploadPart, lastPart bool) (files_sdk.EtagsParam, error) {
   404  	partNumber := fileUploadPart.PartNumber
   405  	var err error
   406  	if partNumber != 1 && *fileUploadPart.ParallelParts { // Remote Mounts use the same url
   407  		fileUploadPart, err = u.startUpload(
   408  			ctx, files_sdk.FileBeginUploadParams{Path: fileUploadPart.Path, Ref: fileUploadPart.Ref, Part: fileUploadPart.PartNumber, MkdirParents: lib.Bool(true)},
   409  		)
   410  		fileUploadPart.PartNumber = partNumber
   411  		if err != nil {
   412  			return files_sdk.EtagsParam{}, err
   413  		}
   414  	}
   415  	uri, err := url.Parse(fileUploadPart.UploadUri)
   416  	if err == nil {
   417  		q := uri.Query()
   418  		if q.Get("partNumber") == "" {
   419  			q.Add("part_number", strconv.FormatInt(partNumber, 10))
   420  			uri.RawQuery = q.Encode()
   421  			fileUploadPart.UploadUri = uri.String()
   422  		}
   423  	}
   424  
   425  	headers := http.Header{}
   426  	headers.Add("Content-Length", strconv.FormatInt(len, 10))
   427  	res, err := files_sdk.CallRaw(
   428  		&files_sdk.CallParams{
   429  			Method:   fileUploadPart.HttpMethod,
   430  			Config:   u.Config,
   431  			Uri:      fileUploadPart.UploadUri,
   432  			BodyIo:   reader,
   433  			Headers:  &headers,
   434  			Context:  ctx,
   435  			StayOpen: !*fileUploadPart.ParallelParts && !lastPart, // Since Remote Mounts use the same url only close the connection on the last part.
   436  		},
   437  	)
   438  	if err != nil {
   439  		return files_sdk.EtagsParam{}, err
   440  	}
   441  	if err := lib.ResponseErrors(res, files_sdk.APIError(), lib.S3XMLError, lib.NonOkError); err != nil {
   442  		return files_sdk.EtagsParam{}, err
   443  	}
   444  	etag := strings.Trim(res.Header.Get("Etag"), "\"")
   445  	if etag == "" {
   446  		// With remote mounts this has no value, but the code strip the value causing a validation error.
   447  		etag = "null"
   448  	}
   449  	if res != nil {
   450  		err = res.Body.Close()
   451  		if err != nil {
   452  			return files_sdk.EtagsParam{}, err
   453  		}
   454  	}
   455  	return files_sdk.EtagsParam{
   456  		Etag: etag,
   457  		Part: strconv.FormatInt(fileUploadPart.PartNumber, 10),
   458  	}, nil
   459  }
   460  
   461  func uploadProgress(uploadStatus *UploadStatus) func(bytesCount int64) {
   462  	return func(bytesCount int64) {
   463  		uploadStatus.Job().UpdateStatusWithBytes(status.Uploading, uploadStatus, bytesCount)
   464  	}
   465  }