## Description If a drive item goes over its 1 hour jwt expiration to download the backing file, re-fetch the item and use the new download url to get the file. ## Does this PR need a docs update or release note? - [x] ⛔ No ## Type of change - [x] 🌻 Feature ## Issue(s) * #2267 ## Test Plan - [x] 💪 Manual
227 lines
6.4 KiB
Go
227 lines
6.4 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.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
|
|
}
|