Upload of large attachments were broken previously. Previously we were relying on the size reported by Graph API, but that is not reliable and we were not completing uploads because of that. <!-- PR description--> --- #### Does this PR need a docs update or release note? - [x] ✅ Yes, it's included - [ ] 🕐 Yes, but in a later PR - [ ] ⛔ No #### Type of change <!--- Please check the type of change your PR introduces: ---> - [ ] 🌻 Feature - [x] 🐛 Bugfix - [ ] 🗺️ Documentation - [ ] 🤖 Supportability/Tests - [ ] 💻 CI/Deployment - [ ] 🧹 Tech Debt/Cleanup #### Issue(s) <!-- Can reference multiple issues. Use one of the following "magic words" - "closes, fixes" to auto-close the Github issue. --> * #<issue> #### Test Plan <!-- How will this be tested prior to merging.--> - [ ] 💪 Manual - [ ] ⚡ Unit test - [x] 💚 E2E
106 lines
3.0 KiB
Go
106 lines
3.0 KiB
Go
package graph
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/alcionai/clues"
|
|
|
|
"github.com/alcionai/corso/src/pkg/logger"
|
|
)
|
|
|
|
const (
|
|
contentRangeHeaderKey = "Content-Range"
|
|
// Format for Content-Range is "bytes <start>-<end>/<total>"
|
|
contentRangeHeaderValueFmt = "bytes %d-%d/%d"
|
|
contentLengthHeaderKey = "Content-Length"
|
|
)
|
|
|
|
// Writer implements an io.Writer for a M365
|
|
// UploadSession URL
|
|
type largeItemWriter struct {
|
|
// ID is the id of the item created.
|
|
// Will be available after the upload is complete
|
|
ID string
|
|
// Identifier
|
|
parentID string
|
|
// Upload URL for this item
|
|
url string
|
|
// Tracks how much data will be written
|
|
contentLength int64
|
|
// Last item offset that was written to
|
|
lastWrittenOffset int64
|
|
client httpWrapper
|
|
}
|
|
|
|
func NewLargeItemWriter(parentID, url string, size int64) *largeItemWriter {
|
|
return &largeItemWriter{
|
|
parentID: parentID,
|
|
url: url,
|
|
contentLength: size,
|
|
client: *NewNoTimeoutHTTPWrapper(),
|
|
}
|
|
}
|
|
|
|
// Write will upload the provided data to M365. It sets the `Content-Length` and `Content-Range` headers based on
|
|
// https://docs.microsoft.com/en-us/graph/api/driveitem-createuploadsession
|
|
func (iw *largeItemWriter) Write(p []byte) (int, error) {
|
|
rangeLength := len(p)
|
|
ctx := context.Background()
|
|
|
|
logger.Ctx(ctx).
|
|
Debugf("WRITE for %s. Size:%d, Offset: %d, TotalSize: %d",
|
|
iw.parentID, rangeLength, iw.lastWrittenOffset, iw.contentLength)
|
|
|
|
endOffset := iw.lastWrittenOffset + int64(rangeLength)
|
|
|
|
// PUT the request - set headers `Content-Range`to describe total size and `Content-Length` to describe size of
|
|
// data in the current request
|
|
headers := make(map[string]string)
|
|
headers[contentRangeHeaderKey] = fmt.Sprintf(
|
|
contentRangeHeaderValueFmt,
|
|
iw.lastWrittenOffset,
|
|
endOffset-1,
|
|
iw.contentLength)
|
|
headers[contentLengthHeaderKey] = fmt.Sprintf("%d", rangeLength)
|
|
|
|
resp, err := iw.client.Request(
|
|
ctx,
|
|
http.MethodPut,
|
|
iw.url,
|
|
bytes.NewReader(p),
|
|
headers)
|
|
if err != nil {
|
|
return 0, clues.Wrap(err, "uploading item").With(
|
|
"upload_id", iw.parentID,
|
|
"upload_chunk_size", rangeLength,
|
|
"upload_offset", iw.lastWrittenOffset,
|
|
"upload_size", iw.contentLength)
|
|
}
|
|
|
|
// Update last offset
|
|
iw.lastWrittenOffset = endOffset
|
|
|
|
// Once the upload is complete, we get a Location header in the
|
|
// below format from which we can get the id of the uploaded
|
|
// item. This will only be available after we have uploaded the
|
|
// entire content(based on the size in the req header).
|
|
// https://outlook.office.com/api/v2.0/Users('<user-id>')/Messages('<message-id>')/Attachments('<attachment-id>')
|
|
// Ref: https://learn.microsoft.com/en-us/graph/outlook-large-attachments?tabs=http
|
|
loc := resp.Header.Get("Location")
|
|
if loc != "" {
|
|
splits := strings.Split(loc, "'")
|
|
if len(splits) != 7 || splits[4] != ")/Attachments(" || len(splits[5]) == 0 {
|
|
return 0, clues.New("invalid format for upload completion url").
|
|
With("location", loc)
|
|
}
|
|
|
|
iw.ID = splits[5]
|
|
}
|
|
|
|
return rangeLength, nil
|
|
}
|