## Description Added retry handling for delta queries in OneDrive. Also, bumping time timeout for graph api calls from 90s to 3m as we were seeing client timeouts for graph api calls. ~Haven't added retry for every request in exchange as I'm hoping https://github.com/alcionai/corso/issues/2287 will be a better way to handle this.~ ## 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 - [ ] 🤖 Test - [ ] 💻 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.--> - [x] 💪 Manual - [ ] ⚡ Unit test - [ ] 💚 E2E
231 lines
6.6 KiB
Go
231 lines
6.6 KiB
Go
package onedrive
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
|
|
msdrives "github.com/microsoftgraph/msgraph-sdk-go/drives"
|
|
"github.com/microsoftgraph/msgraph-sdk-go/models"
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/alcionai/corso/src/internal/connector/graph"
|
|
"github.com/alcionai/corso/src/internal/connector/support"
|
|
"github.com/alcionai/corso/src/internal/connector/uploadsession"
|
|
"github.com/alcionai/corso/src/internal/version"
|
|
"github.com/alcionai/corso/src/pkg/backup/details"
|
|
"github.com/alcionai/corso/src/pkg/logger"
|
|
)
|
|
|
|
const (
|
|
// downloadUrlKey is used to find the download URL in a
|
|
// DriveItem response
|
|
downloadURLKey = "@microsoft.graph.downloadUrl"
|
|
)
|
|
|
|
// generic drive item getter
|
|
func getDriveItem(
|
|
ctx context.Context,
|
|
srv graph.Servicer,
|
|
driveID, itemID string,
|
|
) (models.DriveItemable, error) {
|
|
return srv.Client().DrivesById(driveID).ItemsById(itemID).Get(ctx, nil)
|
|
}
|
|
|
|
// sharePointItemReader will return a io.ReadCloser for the specified item
|
|
// It crafts this by querying M365 for a download URL for the item
|
|
// and using a http client to initialize a reader
|
|
func sharePointItemReader(
|
|
hc *http.Client,
|
|
item models.DriveItemable,
|
|
) (details.ItemInfo, io.ReadCloser, error) {
|
|
resp, err := downloadItem(hc, item)
|
|
if err != nil {
|
|
return details.ItemInfo{}, nil, errors.Wrap(err, "downloading item")
|
|
}
|
|
|
|
dii := details.ItemInfo{
|
|
SharePoint: sharePointItemInfo(item, *item.GetSize()),
|
|
}
|
|
|
|
return dii, resp.Body, nil
|
|
}
|
|
|
|
// oneDriveItemReader will return a io.ReadCloser for the specified item
|
|
// It crafts this by querying M365 for a download URL for the item
|
|
// and using a http client to initialize a reader
|
|
func oneDriveItemReader(
|
|
hc *http.Client,
|
|
item models.DriveItemable,
|
|
) (details.ItemInfo, io.ReadCloser, error) {
|
|
resp, err := downloadItem(hc, item)
|
|
if err != nil {
|
|
return details.ItemInfo{}, nil, errors.Wrap(err, "downloading item")
|
|
}
|
|
|
|
dii := details.ItemInfo{
|
|
OneDrive: oneDriveItemInfo(item, *item.GetSize()),
|
|
}
|
|
|
|
return dii, resp.Body, nil
|
|
}
|
|
|
|
func downloadItem(hc *http.Client, item models.DriveItemable) (*http.Response, error) {
|
|
url, ok := item.GetAdditionalData()[downloadURLKey].(*string)
|
|
if !ok {
|
|
return nil, fmt.Errorf("extracting file url: file %s", *item.GetId())
|
|
}
|
|
|
|
req, err := http.NewRequest(http.MethodGet, *url, nil)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "new request")
|
|
}
|
|
|
|
//nolint:lll
|
|
// Decorate the traffic
|
|
// See https://learn.microsoft.com/en-us/sharepoint/dev/general-development/how-to-avoid-getting-throttled-or-blocked-in-sharepoint-online#how-to-decorate-your-http-traffic
|
|
req.Header.Set("User-Agent", "ISV|Alcion|Corso/"+version.Version)
|
|
|
|
resp, err := hc.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if (resp.StatusCode / 100) == 2 {
|
|
return resp, nil
|
|
}
|
|
|
|
if resp.StatusCode == http.StatusTooManyRequests {
|
|
return resp, graph.Err429TooManyRequests
|
|
}
|
|
|
|
if resp.StatusCode == http.StatusUnauthorized {
|
|
return resp, graph.Err401Unauthorized
|
|
}
|
|
|
|
if resp.StatusCode == http.StatusInternalServerError {
|
|
return resp, graph.Err500InternalServerError
|
|
}
|
|
|
|
if resp.StatusCode == http.StatusServiceUnavailable {
|
|
return resp, graph.Err503ServiceUnavailable
|
|
}
|
|
|
|
return resp, errors.New("non-2xx http response: " + resp.Status)
|
|
}
|
|
|
|
// oneDriveItemInfo will populate a details.OneDriveInfo struct
|
|
// with properties from the drive item. ItemSize is specified
|
|
// separately for restore processes because the local itemable
|
|
// doesn't have its size value updated as a side effect of creation,
|
|
// and kiota drops any SetSize update.
|
|
func oneDriveItemInfo(di models.DriveItemable, itemSize int64) *details.OneDriveInfo {
|
|
var email, parent string
|
|
|
|
if di.GetCreatedBy() != nil && di.GetCreatedBy().GetUser() != nil {
|
|
// User is sometimes not available when created via some
|
|
// external applications (like backup/restore solutions)
|
|
ed, ok := di.GetCreatedBy().GetUser().GetAdditionalData()["email"]
|
|
if ok {
|
|
email = *ed.(*string)
|
|
}
|
|
}
|
|
|
|
if di.GetParentReference() != nil && di.GetParentReference().GetName() != nil {
|
|
// EndPoint is not always populated from external apps
|
|
parent = *di.GetParentReference().GetName()
|
|
}
|
|
|
|
return &details.OneDriveInfo{
|
|
ItemType: details.OneDriveItem,
|
|
ItemName: *di.GetName(),
|
|
Created: *di.GetCreatedDateTime(),
|
|
Modified: *di.GetLastModifiedDateTime(),
|
|
DriveName: parent,
|
|
Size: itemSize,
|
|
Owner: email,
|
|
}
|
|
}
|
|
|
|
// sharePointItemInfo will populate a details.SharePointInfo struct
|
|
// with properties from the drive item. ItemSize is specified
|
|
// separately for restore processes because the local itemable
|
|
// doesn't have its size value updated as a side effect of creation,
|
|
// and kiota drops any SetSize update.
|
|
// TODO: Update drive name during Issue #2071
|
|
func sharePointItemInfo(di models.DriveItemable, itemSize int64) *details.SharePointInfo {
|
|
var (
|
|
id, parent, url string
|
|
reference = di.GetParentReference()
|
|
)
|
|
|
|
// TODO: we rely on this info for details/restore lookups,
|
|
// so if it's nil we have an issue, and will need an alternative
|
|
// way to source the data.
|
|
gsi := di.GetSharepointIds()
|
|
if gsi != nil {
|
|
if gsi.GetSiteId() != nil {
|
|
id = *gsi.GetSiteId()
|
|
}
|
|
|
|
if gsi.GetSiteUrl() != nil {
|
|
url = *gsi.GetSiteUrl()
|
|
}
|
|
}
|
|
|
|
if reference != nil {
|
|
parent = *reference.GetDriveId()
|
|
|
|
if reference.GetName() != nil {
|
|
// EndPoint is not always populated from external apps
|
|
temp := *reference.GetName()
|
|
temp = strings.TrimSpace(temp)
|
|
|
|
if temp != "" {
|
|
parent = temp
|
|
}
|
|
}
|
|
}
|
|
|
|
return &details.SharePointInfo{
|
|
ItemType: details.OneDriveItem,
|
|
ItemName: *di.GetName(),
|
|
Created: *di.GetCreatedDateTime(),
|
|
Modified: *di.GetLastModifiedDateTime(),
|
|
DriveName: parent,
|
|
Size: itemSize,
|
|
Owner: id,
|
|
WebURL: url,
|
|
}
|
|
}
|
|
|
|
// driveItemWriter is used to initialize and return an io.Writer to upload data for the specified item
|
|
// It does so by creating an upload session and using that URL to initialize an `itemWriter`
|
|
// TODO: @vkamra verify if var session is the desired input
|
|
func driveItemWriter(
|
|
ctx context.Context,
|
|
service graph.Servicer,
|
|
driveID, itemID string,
|
|
itemSize int64,
|
|
) (io.Writer, error) {
|
|
session := msdrives.NewItemItemsItemCreateUploadSessionPostRequestBody()
|
|
|
|
r, err := service.Client().DrivesById(driveID).ItemsById(itemID).CreateUploadSession().Post(ctx, session, nil)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(
|
|
err,
|
|
"failed to create upload session for item %s. details: %s",
|
|
itemID,
|
|
support.ConnectorStackErrorTrace(err),
|
|
)
|
|
}
|
|
|
|
url := *r.GetUploadUrl()
|
|
|
|
logger.Ctx(ctx).Debugf("Created an upload session for item %s. URL: %s", itemID, url)
|
|
|
|
return uploadsession.NewWriter(itemID, url, itemSize), nil
|
|
}
|