-
Notifications
You must be signed in to change notification settings - Fork 15
Feat/large file upload #231
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
7da0640
Initial page iterator commit
rkodev 52bc4f9
Large file upload task
rkodev a20948a
Upload file in ranges
rkodev 67b42db
Update large file upload
rkodev 6c4f147
Tests large file upload
rkodev de20c3b
Add more tests
rkodev 5e7609f
Updates the large file session on resume
rkodev 507a7ce
group uploaded slices result
rkodev d1613bf
Refactor stream upload
rkodev 2373109
refactor log
rkodev d7da20b
Make refresh upload public
rkodev 6d0818f
Return url from location header
rkodev 56cf42e
Adds location from headers
rkodev 34056d4
Code cleanup
rkodev File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| package fileuploader | ||
|
|
||
| import ( | ||
| "strings" | ||
| "time" | ||
| ) | ||
|
|
||
| type rangePair struct { | ||
| Start int64 | ||
| End int64 | ||
| } | ||
|
|
||
| func stringIsNullOrEmpty(s string) bool { | ||
| s = strings.TrimSpace(s) | ||
| if s == "" || len(s) == 0 { | ||
| return true | ||
| } | ||
| return false | ||
| } | ||
|
|
||
| type UploadSession interface { | ||
| GetExpirationDateTime() *time.Time | ||
| SetExpirationDateTime(expirationDateTime *time.Time) | ||
| GetNextExpectedRanges() []string | ||
| SetNextExpectedRanges(nextExpectedRanges []string) | ||
| GetOdataType() *string | ||
| GetUploadUrl() *string | ||
| } | ||
|
|
||
| type ProgressCallBack func(current int64, total int64) | ||
|
|
||
| type UploadResult[T interface{}] interface { | ||
| SetItemResponse(response T) | ||
| GetItemResponse() T | ||
| SetUploadSession(uploadSession UploadSession) | ||
| GetUploadSession() UploadSession | ||
| SetURI(uri *string) | ||
| GetURI() *string | ||
| SetUploadSucceeded(isSuccessful bool) | ||
| GetUploadSucceeded() bool | ||
| SetResponseErrors(errors []error) | ||
| GetResponseErrors() []error | ||
| } | ||
|
|
||
| func NewUploadResult[T interface{}]() UploadResult[T] { | ||
| return &uploadResult[T]{} | ||
| } | ||
|
|
||
| type uploadResult[T interface{}] struct { | ||
| itemResponse T | ||
| uploadSession UploadSession | ||
| uri *string | ||
| uploadSucceeded bool | ||
| responseErrors []error | ||
| } | ||
|
|
||
| func (u *uploadResult[T]) SetItemResponse(response T) { | ||
| u.itemResponse = response | ||
| } | ||
|
|
||
| func (u *uploadResult[T]) GetItemResponse() T { | ||
| return u.itemResponse | ||
| } | ||
|
|
||
| func (u *uploadResult[T]) SetUploadSession(uploadSession UploadSession) { | ||
| u.uploadSession = uploadSession | ||
| } | ||
|
|
||
| func (u *uploadResult[T]) GetUploadSession() UploadSession { | ||
| return u.uploadSession | ||
| } | ||
|
|
||
| func (u *uploadResult[T]) SetURI(uri *string) { | ||
| u.uri = uri | ||
| } | ||
|
|
||
| func (u *uploadResult[T]) GetURI() *string { | ||
| return u.uri | ||
| } | ||
|
|
||
| func (u *uploadResult[T]) SetUploadSucceeded(isSuccessful bool) { | ||
| u.uploadSucceeded = isSuccessful | ||
| } | ||
|
|
||
| func (u *uploadResult[T]) GetUploadSucceeded() bool { | ||
| return u.uploadSucceeded | ||
| } | ||
|
|
||
| func (u *uploadResult[T]) SetResponseErrors(errors []error) { | ||
| u.responseErrors = errors | ||
| } | ||
|
|
||
| func (u *uploadResult[T]) GetResponseErrors() []error { | ||
| return u.responseErrors | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| package fileuploader | ||
|
|
||
| import ( | ||
| "github.com/microsoft/kiota-abstractions-go/serialization" | ||
| "time" | ||
| ) | ||
|
|
||
| type UploadSessionResponse interface { | ||
| serialization.Parsable | ||
| GetExpirationDateTime() *time.Time | ||
| SetExpirationDateTime(expirationDateTime *time.Time) | ||
| GetNextExpectedRanges() []string | ||
| SetNextExpectedRanges(nextExpectedRanges []string) | ||
| } | ||
|
|
||
| type largeFileUploadSession struct { | ||
| expirationDateTime *time.Time | ||
| nextExpectedRanges []string | ||
| } | ||
|
|
||
| func (l *largeFileUploadSession) Serialize(writer serialization.SerializationWriter) error { | ||
| if l.expirationDateTime != nil { | ||
| if err := writer.WriteTimeValue("expirationDateTime", l.expirationDateTime); err != nil { | ||
| return err | ||
| } | ||
| } | ||
| if l.nextExpectedRanges != nil { | ||
| if err := writer.WriteCollectionOfStringValues("nextExpectedRanges", l.nextExpectedRanges); err != nil { | ||
| return err | ||
| } | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| func (l *largeFileUploadSession) GetFieldDeserializers() map[string]func(serialization.ParseNode) error { | ||
| return map[string]func(serialization.ParseNode) error{ | ||
| "expirationDateTime": func(n serialization.ParseNode) error { | ||
| val, err := n.GetTimeValue() | ||
| if err != nil { | ||
| return err | ||
| } | ||
| if val != nil { | ||
| l.SetExpirationDateTime(val) | ||
| } | ||
| return nil | ||
| }, | ||
| "nextExpectedRanges": func(n serialization.ParseNode) error { | ||
| val, err := n.GetCollectionOfPrimitiveValues("string") | ||
| if err != nil { | ||
| return err | ||
| } | ||
| if val != nil { | ||
| res := make([]string, len(val)) | ||
| for i, v := range val { | ||
| if v != nil { | ||
| res[i] = *(v.(*string)) | ||
| } | ||
| } | ||
| l.SetNextExpectedRanges(res) | ||
| } | ||
| return nil | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| func (l *largeFileUploadSession) GetExpirationDateTime() *time.Time { | ||
| return l.expirationDateTime | ||
| } | ||
|
|
||
| func (l *largeFileUploadSession) SetExpirationDateTime(expirationDateTime *time.Time) { | ||
| l.expirationDateTime = expirationDateTime | ||
| } | ||
|
|
||
| func (l *largeFileUploadSession) GetNextExpectedRanges() []string { | ||
| return l.nextExpectedRanges | ||
| } | ||
|
|
||
| func (l *largeFileUploadSession) SetNextExpectedRanges(nextExpectedRanges []string) { | ||
| l.nextExpectedRanges = nextExpectedRanges | ||
| } | ||
|
|
||
| func newLargeFileUploadSession() UploadSessionResponse { | ||
| return &largeFileUploadSession{} | ||
| } | ||
|
|
||
| // CreateUploadSessionDiscriminator creates a new instance of the appropriate class based on discriminator value | ||
| func CreateUploadSessionDiscriminator(serialization.ParseNode) (serialization.Parsable, error) { | ||
| return newLargeFileUploadSession(), nil | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,196 @@ | ||
| package fileuploader | ||
|
|
||
| import ( | ||
| "context" | ||
| "errors" | ||
| abstractions "github.com/microsoft/kiota-abstractions-go" | ||
| "github.com/microsoft/kiota-abstractions-go/serialization" | ||
| "io" | ||
| "os" | ||
| "strconv" | ||
| "strings" | ||
| "sync" | ||
| "time" | ||
| ) | ||
|
|
||
| type LargeFileUploadTask[T serialization.Parsable] interface { | ||
| Upload(progress ProgressCallBack) UploadResult[T] | ||
| Resume(progress ProgressCallBack) (UploadResult[T], error) | ||
| RefreshUploadStatus() error | ||
| Cancel() error | ||
| } | ||
|
|
||
| // ByteStream is an interface that represents a stream of bytes | ||
| type ByteStream interface { | ||
| io.ReaderAt | ||
| Stat() (os.FileInfo, error) | ||
| } | ||
|
|
||
| type largeFileUploadTask[T serialization.Parsable] struct { | ||
| uploadSession UploadSession | ||
| adapter abstractions.RequestAdapter | ||
| byteStream ByteStream // *os.File by default implements ByteStream | ||
| maxSlice int64 | ||
| parsableFactory serialization.ParsableFactory | ||
| errorMappings abstractions.ErrorMappings | ||
| } | ||
|
|
||
| func NewLargeFileUploadTask[T serialization.Parsable](adapter abstractions.RequestAdapter, uploadSession UploadSession, byteStream ByteStream, maxSlice int64, parsableFactory serialization.ParsableFactory, errorMappings abstractions.ErrorMappings) LargeFileUploadTask[T] { | ||
| return &largeFileUploadTask[T]{ | ||
| adapter: adapter, | ||
| uploadSession: uploadSession, | ||
| byteStream: byteStream, | ||
| maxSlice: maxSlice, | ||
| parsableFactory: parsableFactory, | ||
| errorMappings: errorMappings, | ||
| } | ||
| } | ||
|
|
||
| // Upload uploads the byteStream in slices and returns the result of the upload | ||
| func (l *largeFileUploadTask[T]) Upload(progress ProgressCallBack) UploadResult[T] { | ||
| result := NewUploadResult[T]() | ||
| slices := l.createUploadSlices() | ||
| maxRetriesPerRequest := 3 | ||
|
|
||
| // slices of errors | ||
| var responseErrors []error | ||
| var itemResponse T | ||
| var location *string | ||
|
|
||
| var wg sync.WaitGroup | ||
| wg.Add(len(slices)) | ||
|
|
||
| for _, slice := range slices { | ||
| uploadSlice := slice | ||
| go func() { | ||
| defer wg.Done() | ||
| response, uploadLocation, err := l.uploadWithRetry(uploadSlice, maxRetriesPerRequest) | ||
| if err != nil { | ||
| responseErrors = append(responseErrors, err) | ||
| } else { | ||
| progress(uploadSlice.RangeEnd, uploadSlice.TotalSessionLength) | ||
| } | ||
| if response != nil { | ||
| itemResponse = response.(T) | ||
| } | ||
| location = uploadLocation | ||
| }() | ||
| } | ||
|
|
||
| wg.Wait() | ||
|
|
||
| if len(responseErrors) > 0 { | ||
| result.SetUploadSucceeded(false) | ||
| result.SetResponseErrors(responseErrors) | ||
| } else { | ||
| result.SetUploadSucceeded(true) | ||
| result.SetUploadSession(l.uploadSession) | ||
| result.SetItemResponse(itemResponse) | ||
| result.SetURI(location) | ||
| } | ||
|
|
||
| return result | ||
| } | ||
|
|
||
| // Resume uploads the byteStream in slices and returns the result of the upload | ||
| func (l *largeFileUploadTask[T]) Resume(progress ProgressCallBack) (UploadResult[T], error) { | ||
| err := l.RefreshUploadStatus() | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| if len(l.uploadSession.GetNextExpectedRanges()) == 0 { | ||
|
rkodev marked this conversation as resolved.
|
||
| return nil, errors.New("UploadSession does not have next expected ranges") | ||
| } | ||
|
|
||
| if l.uploadSession.GetExpirationDateTime().Before(time.Now()) { | ||
| return nil, errors.New("UploadSession has expired") | ||
| } | ||
|
|
||
| return l.Upload(progress), nil | ||
| } | ||
|
|
||
| func (l *largeFileUploadTask[T]) RefreshUploadStatus() error { | ||
| requestInfo := abstractions.NewRequestInformation() | ||
| requestInfo.UrlTemplate = *l.uploadSession.GetUploadUrl() | ||
| requestInfo.Method = abstractions.GET | ||
| requestInfo.Headers.TryAdd("Accept", "application/json") | ||
|
|
||
| result, err := l.adapter.Send(context.Background(), requestInfo, CreateUploadSessionDiscriminator, l.errorMappings) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| sessionResponse := result.(UploadSessionResponse) | ||
|
|
||
| l.uploadSession.SetExpirationDateTime(sessionResponse.GetExpirationDateTime()) | ||
| l.uploadSession.SetNextExpectedRanges(sessionResponse.GetNextExpectedRanges()) | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| // Cancel cancels the upload | ||
| func (l *largeFileUploadTask[T]) Cancel() error { | ||
| requestInfo := abstractions.NewRequestInformationWithMethodAndUrlTemplateAndPathParameters(abstractions.DELETE, *l.uploadSession.GetUploadUrl(), make(map[string]string)) | ||
| err := l.adapter.SendNoContent(context.Background(), requestInfo, l.errorMappings) | ||
| return err | ||
| } | ||
|
|
||
| func (l *largeFileUploadTask[T]) uploadWithRetry(slice uploadSlice[T], maxRetry int) (interface{}, *string, error) { | ||
| retry := 1 | ||
| var parseable interface{} | ||
| var location *string | ||
| var err error | ||
| for retry < maxRetry { | ||
| // store the result of the upload | ||
| parseable, location, err = slice.Upload(l.parsableFactory) // check if successful | ||
| if err != nil { | ||
| if retry >= maxRetry { | ||
| return nil, nil, err | ||
| } | ||
| // backoff before retrying | ||
| time.Sleep(time.Duration(retry) * time.Second) | ||
| } | ||
| retry++ | ||
| } | ||
| return parseable, location, err | ||
| } | ||
|
|
||
| func (l *largeFileUploadTask[T]) getRangesRemaining() []rangePair { | ||
| rangePairs := make([]rangePair, len(l.uploadSession.GetNextExpectedRanges())) | ||
|
|
||
| for i, ranges := range l.uploadSession.GetNextExpectedRanges() { | ||
| rangeValues := strings.Split(ranges, "-") | ||
|
|
||
| var startRange int64 | ||
| if s, err := strconv.ParseInt(rangeValues[0], 10, 64); err == nil { | ||
| startRange = s | ||
| } | ||
|
|
||
| var endRange int64 | ||
| if !stringIsNullOrEmpty(rangeValues[1]) { | ||
| if s, err := strconv.ParseInt(rangeValues[1], 10, 64); err == nil { | ||
| if endRange > l.fileSize() { | ||
| endRange = l.fileSize() - 1 | ||
| } else { | ||
| endRange = s | ||
| } | ||
| } | ||
| } else { | ||
| endRange = l.fileSize() - 1 | ||
| } | ||
|
|
||
| rangePairs[i] = rangePair{ | ||
| Start: startRange, | ||
| End: endRange, | ||
| } | ||
| } | ||
|
|
||
| return rangePairs | ||
| } | ||
|
|
||
| // returns the size of a byteStream | ||
| func (l *largeFileUploadTask[T]) fileSize() int64 { | ||
| fileInfo, _ := l.byteStream.Stat() | ||
| return fileInfo.Size() | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.