move ad-hoc drive api calls into m365/api (#3451)

ensure that all drive-baased graph client calls exist in the m365/api package, not defined ad-hoc throughout the codebase.

---

#### Does this PR need a docs update or release note?

- [x]  No

#### Type of change

- [x] 🧹 Tech Debt/Cleanup

#### Issue(s)

* #1996

#### Test Plan

- [x]  Unit test
- [x] 💚 E2E
This commit is contained in:
Keepers 2023-05-24 15:45:07 -06:00 committed by GitHub
parent be4032aec9
commit 7181e2ef90
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 1013 additions and 730 deletions

View File

@ -21,6 +21,7 @@ import (
"github.com/alcionai/corso/src/pkg/fault"
"github.com/alcionai/corso/src/pkg/logger"
"github.com/alcionai/corso/src/pkg/services/m365"
"github.com/alcionai/corso/src/pkg/services/m365/api"
)
var purgeCmd = &cobra.Command{
@ -190,12 +191,11 @@ func purgeOneDriveFolders(
return clues.New("non-OneDrive item")
}
return onedrive.DeleteItem(
return api.DeleteDriveItem(
ctx,
gs,
*driveFolder.GetParentReference().GetDriveId(),
*f.GetId(),
)
*f.GetId())
}
return purgeFolders(ctx, gc, boundary, "OneDrive Folders", uid, getter, deleter)

View File

@ -233,7 +233,7 @@ func (gc *GraphConnector) ConsumeRestoreCollections(
switch sels.Service {
case selectors.ServiceExchange:
status, err = exchange.RestoreExchangeDataCollections(ctx, creds, gc.Service, dest, dcs, deets, errs)
status, err = exchange.RestoreCollections(ctx, creds, gc.Discovery, gc.Service, dest, dcs, deets, errs)
case selectors.ServiceOneDrive:
status, err = onedrive.RestoreCollections(ctx, creds, backupVersion, gc.Service, dest, opts, dcs, deets, errs)
case selectors.ServiceSharePoint:

View File

@ -1,24 +1,35 @@
package exchange
import (
"bytes"
"context"
"io"
"fmt"
"github.com/alcionai/clues"
"github.com/microsoftgraph/msgraph-sdk-go/models"
"github.com/alcionai/corso/src/internal/common/ptr"
"github.com/alcionai/corso/src/internal/connector/graph"
"github.com/alcionai/corso/src/internal/connector/support"
"github.com/alcionai/corso/src/pkg/logger"
)
type attachmentPoster interface {
PostSmallAttachment(
ctx context.Context,
userID, containerID, itemID string,
body models.Attachmentable,
) error
PostLargeAttachment(
ctx context.Context,
userID, containerID, itemID, name string,
size int64,
body models.Attachmentable,
) (models.UploadSessionable, error)
}
const (
// Use large attachment logic for attachments > 3MB
// https://learn.microsoft.com/en-us/graph/outlook-large-attachments
largeAttachmentSize = int32(3 * 1024 * 1024)
attachmentChunkSize = 4 * 1024 * 1024
fileAttachmentOdataValue = "#microsoft.graph.fileAttachment"
itemAttachmentOdataValue = "#microsoft.graph.itemAttachment"
referenceAttachmentOdataValue = "#microsoft.graph.referenceAttachment"
@ -43,23 +54,30 @@ func attachmentType(attachment models.Attachmentable) models.AttachmentType {
// uploadAttachment will upload the specified message attachment to M365
func uploadAttachment(
ctx context.Context,
uploader attachmentUploadable,
cli attachmentPoster,
userID, containerID, parentItemID string,
attachment models.Attachmentable,
) error {
attachmentType := attachmentType(attachment)
var (
attachmentType = attachmentType(attachment)
id = ptr.Val(attachment.GetId())
name = ptr.Val(attachment.GetName())
size = ptr.Val(attachment.GetSize())
)
ctx = clues.Add(
ctx,
"attachment_size", ptr.Val(attachment.GetSize()),
"attachment_id", ptr.Val(attachment.GetId()),
"attachment_name", clues.Hide(ptr.Val(attachment.GetName())),
"attachment_size", size,
"attachment_id", id,
"attachment_name", clues.Hide(name),
"attachment_type", attachmentType,
"internal_item_type", getItemAttachmentItemType(attachment),
"uploader_item_id", uploader.getItemID())
"attachment_odata_type", ptr.Val(attachment.GetOdataType()),
"attachment_outlook_odata_type", getOutlookOdataType(attachment),
"parent_item_id", parentItemID)
logger.Ctx(ctx).Debug("uploading attachment")
// Reference attachments that are inline() do not need to be recreated. The contents are part of the body.
// reference attachments that are inline() do not need to be recreated. The contents are part of the body.
if attachmentType == models.REFERENCE_ATTACHMENTTYPE && ptr.Val(attachment.GetIsInline()) {
logger.Ctx(ctx).Debug("skip uploading inline reference attachment")
return nil
@ -69,67 +87,32 @@ func uploadAttachment(
if attachmentType == models.ITEM_ATTACHMENTTYPE {
a, err := support.ToItemAttachment(attachment)
if err != nil {
logger.CtxErr(ctx, err).Info("item attachment restore not supported for this type. skipping upload.")
logger.CtxErr(ctx, err).Info(fmt.Sprintf("item attachment type not supported: %v", attachmentType))
return nil
}
attachment = a
}
// For Item/Reference attachments *or* file attachments < 3MB, use the attachments endpoint
if attachmentType != models.FILE_ATTACHMENTTYPE || ptr.Val(attachment.GetSize()) < largeAttachmentSize {
return uploader.uploadSmallAttachment(ctx, attachment)
// for file attachments sized >= 3MB
if attachmentType == models.FILE_ATTACHMENTTYPE && size >= largeAttachmentSize {
_, err := cli.PostLargeAttachment(ctx, userID, containerID, parentItemID, name, int64(size), attachment)
return err
}
return uploadLargeAttachment(ctx, uploader, attachment)
// for all other attachments
return cli.PostSmallAttachment(ctx, userID, containerID, parentItemID, attachment)
}
// uploadLargeAttachment will upload the specified attachment by creating an upload session and
// doing a chunked upload
func uploadLargeAttachment(
ctx context.Context,
uploader attachmentUploadable,
attachment models.Attachmentable,
) error {
bs, err := GetAttachmentBytes(attachment)
if err != nil {
return clues.Stack(err).WithClues(ctx)
}
size := int64(len(bs))
session, err := uploader.uploadSession(ctx, ptr.Val(attachment.GetName()), size)
if err != nil {
return clues.Stack(err).WithClues(ctx)
}
url := ptr.Val(session.GetUploadUrl())
aw := graph.NewLargeItemWriter(uploader.getItemID(), url, size)
logger.Ctx(ctx).Debugw("uploading large attachment", "attachment_url", graph.LoggableURL(url))
// Upload the stream data
copyBuffer := make([]byte, attachmentChunkSize)
_, err = io.CopyBuffer(aw, bytes.NewReader(bs), copyBuffer)
if err != nil {
return clues.Wrap(err, "uploading large attachment").WithClues(ctx)
}
return nil
}
func getItemAttachmentItemType(query models.Attachmentable) string {
empty := ""
func getOutlookOdataType(query models.Attachmentable) string {
attachment, ok := query.(models.ItemAttachmentable)
if !ok {
return empty
return ""
}
item := attachment.GetItem()
if item == nil {
return empty
return ""
}
return ptr.Val(item.GetOdataType())

View File

@ -1,144 +0,0 @@
package exchange
import (
"context"
"github.com/microsoftgraph/msgraph-sdk-go/models"
"github.com/microsoftgraph/msgraph-sdk-go/users"
"github.com/alcionai/corso/src/internal/connector/graph"
)
// attachmentUploadable represents structs that are able to upload small attachments directly to an item or use an
// upload session to connect large attachments to their corresponding M365 item.
type attachmentUploadable interface {
uploadSmallAttachment(ctx context.Context, attachment models.Attachmentable) error
uploadSession(ctx context.Context, attachName string, attachSize int64) (models.UploadSessionable, error)
// getItemID returns the M365ID of the item associated with the attachment
getItemID() string
}
var (
_ attachmentUploadable = &mailAttachmentUploader{}
_ attachmentUploadable = &eventAttachmentUploader{}
)
// mailAttachmentUploader is a struct that is able to upload attachments for exchange.Mail objects
type mailAttachmentUploader struct {
userID string
folderID string
itemID string
service graph.Servicer
}
func (mau *mailAttachmentUploader) getItemID() string {
return mau.itemID
}
func (mau *mailAttachmentUploader) uploadSmallAttachment(ctx context.Context, attach models.Attachmentable) error {
_, err := mau.service.Client().
Users().
ByUserId(mau.userID).
MailFolders().
ByMailFolderId(mau.folderID).
Messages().
ByMessageId(mau.itemID).
Attachments().
Post(ctx, attach, nil)
if err != nil {
return graph.Stack(ctx, err)
}
return nil
}
func (mau *mailAttachmentUploader) uploadSession(
ctx context.Context,
attachmentName string,
attachmentSize int64,
) (models.UploadSessionable, error) {
session := users.NewItemMailFoldersItemMessagesItemAttachmentsCreateUploadSessionPostRequestBody()
session.SetAttachmentItem(makeSessionAttachment(attachmentName, attachmentSize))
r, err := mau.
service.
Client().
Users().
ByUserId(mau.userID).
MailFolders().
ByMailFolderId(mau.folderID).
Messages().
ByMessageId(mau.itemID).
Attachments().
CreateUploadSession().
Post(ctx, session, nil)
if err != nil {
return nil, graph.Wrap(ctx, err, "uploading mail attachment")
}
return r, nil
}
// eventAttachmentUploader is a struct capable of uploading attachments for exchange.Event objects
type eventAttachmentUploader struct {
userID string
calendarID string
itemID string
service graph.Servicer
}
func (eau *eventAttachmentUploader) getItemID() string {
return eau.itemID
}
func (eau *eventAttachmentUploader) uploadSmallAttachment(ctx context.Context, attach models.Attachmentable) error {
_, err := eau.service.Client().
Users().
ByUserId(eau.userID).
Calendars().
ByCalendarId(eau.calendarID).
Events().
ByEventId(eau.itemID).
Attachments().
Post(ctx, attach, nil)
if err != nil {
return graph.Stack(ctx, err)
}
return nil
}
func (eau *eventAttachmentUploader) uploadSession(
ctx context.Context,
attachmentName string,
attachmentSize int64,
) (models.UploadSessionable, error) {
session := users.NewItemCalendarEventsItemAttachmentsCreateUploadSessionPostRequestBody()
session.SetAttachmentItem(makeSessionAttachment(attachmentName, attachmentSize))
r, err := eau.service.Client().
Users().
ByUserId(eau.userID).
Calendars().
ByCalendarId(eau.calendarID).
Events().
ByEventId(eau.itemID).
Attachments().
CreateUploadSession().
Post(ctx, session, nil)
if err != nil {
return nil, graph.Wrap(ctx, err, "uploading event attachment")
}
return r, nil
}
func makeSessionAttachment(name string, size int64) *models.AttachmentItem {
attItem := models.NewAttachmentItem()
attType := models.FILE_ATTACHMENTTYPE
attItem.SetAttachmentType(&attType)
attItem.SetName(&name)
attItem.SetSize(&size)
return attItem
}

View File

@ -79,10 +79,10 @@ func (suite *RestoreIntgSuite) TestRestoreContact() {
assert.NoError(t, err, clues.ToCore(err))
}()
info, err := RestoreExchangeContact(
info, err := RestoreContact(
ctx,
exchMock.ContactBytes("Corso TestContact"),
suite.gs,
suite.ac.Contacts(),
control.Copy,
folderID,
userID)
@ -135,9 +135,11 @@ func (suite *RestoreIntgSuite) TestRestoreEvent() {
ctx, flush := tester.NewContext(t)
defer flush()
info, err := RestoreExchangeEvent(
info, err := RestoreEvent(
ctx,
test.bytes,
suite.ac.Events(),
suite.ac.Events(),
suite.gs,
control.Copy,
calendarID,
@ -365,11 +367,12 @@ func (suite *RestoreIntgSuite) TestRestoreExchangeObject() {
defer flush()
destination := test.destination(t, ctx)
info, err := RestoreExchangeObject(
info, err := RestoreItem(
ctx,
test.bytes,
test.category,
control.Copy,
suite.ac,
service,
destination,
userID,

View File

@ -25,15 +25,24 @@ import (
"github.com/alcionai/corso/src/pkg/services/m365/api"
)
// RestoreExchangeObject directs restore pipeline towards restore function
type itemPoster[T any] interface {
PostItem(
ctx context.Context,
userID, dirID string,
body T,
) (T, error)
}
// RestoreItem directs restore pipeline towards restore function
// based on the path.CategoryType. All input params are necessary to perform
// the type-specific restore function.
func RestoreExchangeObject(
func RestoreItem(
ctx context.Context,
bits []byte,
category path.CategoryType,
policy control.CollisionPolicy,
service graph.Servicer,
ac api.Client,
gs graph.Servicer,
destination, user string,
errs *fault.Bus,
) (*details.ExchangeInfo, error) {
@ -43,26 +52,21 @@ func RestoreExchangeObject(
switch category {
case path.EmailCategory:
return RestoreMailMessage(ctx, bits, service, control.Copy, destination, user, errs)
return RestoreMessage(ctx, bits, ac.Mail(), ac.Mail(), gs, control.Copy, destination, user, errs)
case path.ContactsCategory:
return RestoreExchangeContact(ctx, bits, service, control.Copy, destination, user)
return RestoreContact(ctx, bits, ac.Contacts(), control.Copy, destination, user)
case path.EventsCategory:
return RestoreExchangeEvent(ctx, bits, service, control.Copy, destination, user, errs)
return RestoreEvent(ctx, bits, ac.Events(), ac.Events(), gs, control.Copy, destination, user, errs)
default:
return nil, clues.Wrap(clues.New(category.String()), "not supported for Exchange restore")
}
}
// RestoreExchangeContact restores a contact to the @bits byte
// representation of M365 contact object.
// @destination M365 ID representing a M365 Contact_Folder
// Returns an error if the input bits do not parse into a models.Contactable object
// or if an error is encountered sending data to the M365 account.
// Post details: https://docs.microsoft.com/en-us/graph/api/user-post-contacts?view=graph-rest-1.0&tabs=go
func RestoreExchangeContact(
// RestoreContact wraps api.Contacts().PostItem()
func RestoreContact(
ctx context.Context,
bits []byte,
service graph.Servicer,
cli itemPoster[models.Contactable],
cp control.CollisionPolicy,
destination, user string,
) (*details.ExchangeInfo, error) {
@ -73,19 +77,9 @@ func RestoreExchangeContact(
ctx = clues.Add(ctx, "item_id", ptr.Val(contact.GetId()))
response, err := service.Client().
Users().
ByUserId(user).
ContactFolders().
ByContactFolderId(destination).
Contacts().
Post(ctx, contact, nil)
_, err = cli.PostItem(ctx, user, destination, contact)
if err != nil {
return nil, graph.Wrap(ctx, err, "uploading Contact")
}
if response == nil {
return nil, clues.New("nil response from post").WithClues(ctx)
return nil, clues.Stack(err)
}
info := api.ContactInfo(contact)
@ -94,16 +88,13 @@ func RestoreExchangeContact(
return info, nil
}
// RestoreExchangeEvent restores a contact to the @bits byte
// representation of M365 event object.
// @param destination is the M365 ID representing Calendar that will receive the event.
// Returns an error if input byte array doesn't parse into models.Eventable object
// or if an error occurs during sending data to M365 account.
// Post details: https://docs.microsoft.com/en-us/graph/api/user-post-events?view=graph-rest-1.0&tabs=http
func RestoreExchangeEvent(
// RestoreEvent wraps api.Events().PostItem()
func RestoreEvent(
ctx context.Context,
bits []byte,
service graph.Servicer,
itemCli itemPoster[models.Eventable],
attachmentCli attachmentPoster,
gs graph.Servicer,
cp control.CollisionPolicy,
destination, user string,
errs *fault.Bus,
@ -127,34 +118,24 @@ func RestoreExchangeEvent(
transformedEvent.SetAttachments([]models.Attachmentable{})
}
response, err := service.Client().
Users().
ByUserId(user).
Calendars().
ByCalendarId(destination).
Events().
Post(ctx, transformedEvent, nil)
item, err := itemCli.PostItem(ctx, user, destination, event)
if err != nil {
return nil, graph.Wrap(ctx, err, "uploading event")
return nil, clues.Stack(err)
}
if response == nil {
return nil, clues.New("nil response from post").WithClues(ctx)
}
uploader := &eventAttachmentUploader{
calendarID: destination,
userID: user,
service: service,
itemID: ptr.Val(response.GetId()),
}
for _, attach := range attached {
for _, a := range attached {
if el.Failure() != nil {
break
}
if err := uploadAttachment(ctx, uploader, attach); err != nil {
err := uploadAttachment(
ctx,
attachmentCli,
user,
destination,
ptr.Val(item.GetId()),
a)
if err != nil {
el.AddRecoverable(err)
}
}
@ -165,30 +146,27 @@ func RestoreExchangeEvent(
return info, el.Failure()
}
// RestoreMailMessage utility function to place an exchange.Mail
// message into the user's M365 Exchange account.
// @param bits - byte array representation of exchange.Message from Corso backstore
// @param service - connector to M365 graph
// @param cp - collision policy that directs restore workflow
// @param destination - M365 Folder ID. Verified and sent by higher function. `copy` policy can use directly
func RestoreMailMessage(
// RestoreMessage wraps api.Mail().PostItem(), handling attachment creation along the way
func RestoreMessage(
ctx context.Context,
bits []byte,
service graph.Servicer,
itemCli itemPoster[models.Messageable],
attachmentCli attachmentPoster,
gs graph.Servicer,
cp control.CollisionPolicy,
destination, user string,
errs *fault.Bus,
) (*details.ExchangeInfo, error) {
// Creates messageable object from original bytes
originalMessage, err := support.CreateMessageFromBytes(bits)
msg, err := support.CreateMessageFromBytes(bits)
if err != nil {
return nil, clues.Wrap(err, "creating mail from bytes").WithClues(ctx)
}
ctx = clues.Add(ctx, "item_id", ptr.Val(originalMessage.GetId()))
ctx = clues.Add(ctx, "item_id", ptr.Val(msg.GetId()))
var (
clone = support.ToMessage(originalMessage)
clone = support.ToMessage(msg)
valueID = MailRestorePropertyTag
enableValue = RestoreCanonicalEnableValue
)
@ -225,80 +203,35 @@ func RestoreMailMessage(
clone.SetSingleValueExtendedProperties(svlep)
if err := SendMailToBackStore(ctx, service, user, destination, clone, errs); err != nil {
return nil, err
}
info := api.MailInfo(clone, int64(len(bits)))
return info, nil
}
// GetAttachmentBytes is a helper to retrieve the attachment content from a models.Attachmentable
func GetAttachmentBytes(attachment models.Attachmentable) ([]byte, error) {
bi, err := attachment.GetBackingStore().Get("contentBytes")
if err != nil {
return nil, err
}
bts, ok := bi.([]byte)
if !ok {
return nil, clues.New(fmt.Sprintf("unexpected type for attachment content: %T", bi))
}
return bts, nil
}
// SendMailToBackStore function for transporting in-memory messageable item to M365 backstore
// @param user string represents M365 ID of user within the tenant
// @param destination represents M365 ID of a folder within the users's space
// @param message is a models.Messageable interface from "github.com/microsoftgraph/msgraph-sdk-go/models"
func SendMailToBackStore(
ctx context.Context,
service graph.Servicer,
user, destination string,
message models.Messageable,
errs *fault.Bus,
) error {
attached := message.GetAttachments()
attached := clone.GetAttachments()
// Item.Attachments --> HasAttachments doesn't always have a value populated when deserialized
message.SetAttachments([]models.Attachmentable{})
clone.SetAttachments([]models.Attachmentable{})
response, err := service.Client().
Users().
ByUserId(user).
MailFolders().
ByMailFolderId(destination).
Messages().
Post(ctx, message, nil)
item, err := itemCli.PostItem(ctx, user, destination, clone)
if err != nil {
return graph.Wrap(ctx, err, "restoring mail")
return nil, graph.Wrap(ctx, err, "restoring mail message")
}
if response == nil {
return clues.New("nil response from post").WithClues(ctx)
}
el := errs.Local()
var (
el = errs.Local()
id = ptr.Val(response.GetId())
uploader = &mailAttachmentUploader{
userID: user,
folderID: destination,
itemID: id,
service: service,
}
)
for _, attachment := range attached {
for _, a := range attached {
if el.Failure() != nil {
break
return nil, el.Failure()
}
if err := uploadAttachment(ctx, uploader, attachment); err != nil {
if ptr.Val(attachment.GetOdataType()) == "#microsoft.graph.itemAttachment" {
name := ptr.Val(attachment.GetName())
err := uploadAttachment(
ctx,
attachmentCli,
user,
destination,
ptr.Val(item.GetId()),
a)
if err != nil {
// FIXME: I don't know why we're swallowing this error case.
// It needs investigation: https://github.com/alcionai/corso/issues/3498
if ptr.Val(a.GetOdataType()) == "#microsoft.graph.itemAttachment" {
name := ptr.Val(a.GetName())
logger.CtxErr(ctx, err).
With("attachment_name", name).
@ -308,20 +241,18 @@ func SendMailToBackStore(
}
el.AddRecoverable(clues.Wrap(err, "uploading mail attachment"))
break
}
}
return el.Failure()
return api.MailInfo(clone, int64(len(bits))), el.Failure()
}
// RestoreExchangeDataCollections restores M365 objects in data.RestoreCollection to MSFT
// RestoreCollections restores M365 objects in data.RestoreCollection to MSFT
// store through GraphAPI.
// @param dest: container destination to M365
func RestoreExchangeDataCollections(
func RestoreCollections(
ctx context.Context,
creds account.M365Config,
ac api.Client,
gs graph.Servicer,
dest control.RestoreDestination,
dcs []data.RestoreCollection,
@ -365,7 +296,7 @@ func RestoreExchangeDataCollections(
continue
}
temp, canceled := restoreCollection(ctx, gs, dc, containerID, policy, deets, errs)
temp, canceled := restoreCollection(ctx, ac, gs, dc, containerID, policy, deets, errs)
metrics = support.CombineMetrics(metrics, temp)
@ -387,6 +318,7 @@ func RestoreExchangeDataCollections(
// restoreCollection handles restoration of an individual collection.
func restoreCollection(
ctx context.Context,
ac api.Client,
gs graph.Servicer,
dc data.RestoreCollection,
folderID string,
@ -444,11 +376,12 @@ func restoreCollection(
byteArray := buf.Bytes()
info, err := RestoreExchangeObject(
info, err := RestoreItem(
ictx,
byteArray,
category,
policy,
ac,
gs,
folderID,
user,

View File

@ -7,6 +7,8 @@ import (
"github.com/alcionai/corso/src/pkg/path"
)
const AttachmentChunkSize = 4 * 1024 * 1024
// ---------------------------------------------------------------------------
// item response AdditionalData
// ---------------------------------------------------------------------------

View File

@ -227,6 +227,8 @@ func (suite *RetryMWIntgSuite) TestRetryMiddleware_RetryRequest_resetBodyAfter50
adpt, err := mockAdapter(suite.creds, mw)
require.NoError(t, err, clues.ToCore(err))
// no api package needed here, this is a mocked request that works
// independent of the query.
_, err = NewService(adpt).
Client().
Users().

View File

@ -16,7 +16,6 @@ import (
"golang.org/x/exp/slices"
"github.com/alcionai/corso/src/internal/common/ptr"
"github.com/alcionai/corso/src/internal/connector/exchange"
"github.com/alcionai/corso/src/internal/connector/onedrive"
"github.com/alcionai/corso/src/internal/connector/onedrive/metadata"
"github.com/alcionai/corso/src/internal/connector/support"
@ -25,6 +24,7 @@ import (
"github.com/alcionai/corso/src/pkg/fault"
"github.com/alcionai/corso/src/pkg/path"
"github.com/alcionai/corso/src/pkg/selectors"
"github.com/alcionai/corso/src/pkg/services/m365/api"
)
func testElementsMatch[T any](
@ -118,12 +118,12 @@ func attachmentEqual(
expected models.Attachmentable,
got models.Attachmentable,
) bool {
expectedData, err := exchange.GetAttachmentBytes(expected)
expectedData, err := api.GetAttachmentContent(expected)
if err != nil {
return false
}
gotData, err := exchange.GetAttachmentBytes(got)
gotData, err := api.GetAttachmentContent(got)
if err != nil {
return false
}

View File

@ -21,6 +21,7 @@ import (
"github.com/alcionai/corso/src/pkg/account"
"github.com/alcionai/corso/src/pkg/control"
"github.com/alcionai/corso/src/pkg/path"
"github.com/alcionai/corso/src/pkg/services/m365/api"
)
var (
@ -54,9 +55,9 @@ func mustGetDefaultDriveID(
switch backupService {
case path.OneDriveService:
d, err = service.Client().Users().ByUserId(resourceOwner).Drive().Get(ctx, nil)
d, err = api.GetUsersDrive(ctx, service, resourceOwner)
case path.SharePointService:
d, err = service.Client().Sites().BySiteId(resourceOwner).Drive().Get(ctx, nil)
d, err = api.GetSitesDefaultDrive(ctx, service, resourceOwner)
default:
assert.FailNowf(t, "unknown service type %s", backupService.String())
}

View File

@ -326,24 +326,3 @@ func GetAllFolders(
return res, el.Failure()
}
// deletes require unique http clients
// https://github.com/alcionai/corso/issues/2707
func DeleteItem(
ctx context.Context,
gs graph.Servicer,
driveID string,
itemID string,
) error {
err := gs.Client().
Drives().
ByDriveId(driveID).
Items().
ByDriveItemId(itemID).
Delete(ctx, nil)
if err != nil {
return graph.Wrap(ctx, err, "deleting item").With("item_id", itemID)
}
return nil
}

View File

@ -337,7 +337,7 @@ func (suite *OneDriveIntgSuite) TestCreateGetDeleteFolder() {
// deletes require unique http clients
// https://github.com/alcionai/corso/issues/2707
err := DeleteItem(ictx, loadTestService(t), driveID, id)
err := api.DeleteDriveItem(ictx, loadTestService(t), driveID, id)
if err != nil {
logger.CtxErr(ictx, err).Errorw("deleting folder")
}

View File

@ -9,7 +9,6 @@ import (
"strings"
"github.com/alcionai/clues"
"github.com/microsoftgraph/msgraph-sdk-go/drives"
"github.com/microsoftgraph/msgraph-sdk-go/models"
"github.com/alcionai/corso/src/internal/common/ptr"
@ -337,29 +336,20 @@ func sharePointItemInfo(di models.DriveItemable, itemSize int64) *details.ShareP
// TODO: @vkamra verify if var session is the desired input
func driveItemWriter(
ctx context.Context,
service graph.Servicer,
gs graph.Servicer,
driveID, itemID string,
itemSize int64,
) (io.Writer, error) {
session := drives.NewItemItemsItemCreateUploadSessionPostRequestBody()
ctx = clues.Add(ctx, "upload_item_id", itemID)
r, err := service.Client().
Drives().
ByDriveId(driveID).
Items().
ByDriveItemId(itemID).
CreateUploadSession().
Post(ctx, session, nil)
r, err := api.PostDriveItem(ctx, gs, driveID, itemID)
if err != nil {
return nil, graph.Wrap(ctx, err, "creating item upload session")
return nil, clues.Stack(err)
}
logger.Ctx(ctx).Debug("created an upload session")
iw := graph.NewLargeItemWriter(itemID, ptr.Val(r.GetUploadUrl()), itemSize)
url := ptr.Val(r.GetUploadUrl())
return graph.NewLargeItemWriter(itemID, url, itemSize), nil
return iw, nil
}
// constructWebURL helper function for recreating the webURL

View File

@ -154,7 +154,7 @@ func (suite *ItemIntegrationSuite) TestItemWriter() {
srv := suite.service
root, err := srv.Client().Drives().ByDriveId(test.driveID).Root().Get(ctx, nil)
root, err := api.GetDriveRoot(ctx, srv, test.driveID)
require.NoError(t, err, clues.ToCore(err))
newFolderName := tester.DefaultTestRestoreDestination("folder").ContainerName
@ -233,7 +233,7 @@ func (suite *ItemIntegrationSuite) TestDriveGetFolder() {
srv := suite.service
root, err := srv.Client().Drives().ByDriveId(test.driveID).Root().Get(ctx, nil)
root, err := api.GetDriveRoot(ctx, srv, test.driveID)
require.NoError(t, err, clues.ToCore(err))
// Lookup a folder that doesn't exist

View File

@ -155,27 +155,20 @@ func UpdatePermissions(
// https://github.com/alcionai/corso/issues/2707
// this is bad citizenship, and could end up consuming a lot of
// system resources if servicers leak client connections (sockets, etc).
a, err := graph.CreateAdapter(creds.AzureTenantID, creds.AzureClientID, creds.AzureClientSecret)
if err != nil {
return graph.Wrap(ictx, err, "creating delete client")
}
pid, ok := oldPermIDToNewID[p.ID]
if !ok {
return clues.New("no new permission id").WithClues(ctx)
}
err = graph.NewService(a).
Client().
Drives().
ByDriveId(driveID).
Items().
ByDriveItemId(itemID).
Permissions().
ByPermissionId(pid).
Delete(graph.ConsumeNTokens(ictx, graph.PermissionsLC), nil)
err := api.DeleteDriveItemPermission(
ictx,
creds,
driveID,
itemID,
pid)
if err != nil {
return graph.Wrap(ictx, err, "removing permissions")
return clues.Stack(err)
}
}

View File

@ -1,28 +0,0 @@
package sharepoint
import (
"context"
"github.com/microsoft/kiota-abstractions-go/serialization"
"github.com/microsoftgraph/msgraph-sdk-go/sites"
"github.com/alcionai/corso/src/internal/connector/graph"
)
// GetAllSitesForTenant makes a GraphQuery request retrieving all sites in the tenant.
// Due to restrictions in filter capabilities for site queries, the returned iterable
// will contain all personal sites for all users in the org.
func GetAllSitesForTenant(ctx context.Context, gs graph.Servicer) (serialization.Parsable, error) {
options := &sites.SitesRequestBuilderGetRequestConfiguration{
QueryParameters: &sites.SitesRequestBuilderGetQueryParameters{
Select: []string{"id", "name", "weburl"},
},
}
ss, err := gs.Client().Sites().Get(ctx, options)
if err != nil {
return nil, graph.Wrap(ctx, err, "getting sites")
}
return ss, nil
}

View File

@ -983,14 +983,7 @@ func testExchangeContinuousBackups(suite *BackupOpIntegrationSuite, toggles cont
body := users.NewItemMailFoldersItemMovePostRequestBody()
body.SetDestinationId(ptr.To(to.containerID))
_, err := gc.Service.
Client().
Users().
ByUserId(uidn.ID()).
MailFolders().
ByMailFolderId(from.containerID).
Move().
Post(ctx, body, nil)
err := ac.Mail().MoveContainer(ctx, uidn.ID(), from.containerID, body)
require.NoError(t, err, clues.ToCore(err))
newLoc := expectDeets.MoveLocation(cat.String(), from.locRef, to.locRef)
@ -1083,7 +1076,6 @@ func testExchangeContinuousBackups(suite *BackupOpIntegrationSuite, toggles cont
name: "rename a folder",
updateUserData: func(t *testing.T) {
for category, d := range dataset {
cli := gc.Service.Client().Users().ByUserId(uidn.ID())
containerID := d.dests[container3].containerID
newLoc := containerRename
@ -1103,34 +1095,28 @@ func testExchangeContinuousBackups(suite *BackupOpIntegrationSuite, toggles cont
switch category {
case path.EmailCategory:
cmf := cli.MailFolders().ByMailFolderId(containerID)
body, err := cmf.Get(ctx, nil)
require.NoError(t, err, "getting mail folder", clues.ToCore(err))
body, err := ac.Mail().GetFolder(ctx, uidn.ID(), containerID)
require.NoError(t, err, clues.ToCore(err))
body.SetDisplayName(&containerRename)
_, err = cmf.Patch(ctx, body, nil)
require.NoError(t, err, "updating mail folder name", clues.ToCore(err))
err = ac.Mail().PatchFolder(ctx, uidn.ID(), containerID, body)
require.NoError(t, err, clues.ToCore(err))
case path.ContactsCategory:
ccf := cli.ContactFolders().ByContactFolderId(containerID)
body, err := ccf.Get(ctx, nil)
require.NoError(t, err, "getting contact folder", clues.ToCore(err))
body, err := ac.Contacts().GetFolder(ctx, uidn.ID(), containerID)
require.NoError(t, err, clues.ToCore(err))
body.SetDisplayName(&containerRename)
_, err = ccf.Patch(ctx, body, nil)
require.NoError(t, err, "updating contact folder name", clues.ToCore(err))
err = ac.Contacts().PatchFolder(ctx, uidn.ID(), containerID, body)
require.NoError(t, err, clues.ToCore(err))
case path.EventsCategory:
cbi := cli.Calendars().ByCalendarId(containerID)
body, err := cbi.Get(ctx, nil)
require.NoError(t, err, "getting calendar", clues.ToCore(err))
body, err := ac.Events().GetCalendar(ctx, uidn.ID(), containerID)
require.NoError(t, err, clues.ToCore(err))
body.SetName(&containerRename)
_, err = cbi.Patch(ctx, body, nil)
require.NoError(t, err, "updating calendar name", clues.ToCore(err))
err = ac.Events().PatchCalendar(ctx, uidn.ID(), containerID, body)
require.NoError(t, err, clues.ToCore(err))
}
}
},
@ -1146,16 +1132,15 @@ func testExchangeContinuousBackups(suite *BackupOpIntegrationSuite, toggles cont
updateUserData: func(t *testing.T) {
for category, d := range dataset {
containerID := d.dests[container1].containerID
cli := gc.Service.Client().Users().ByUserId(uidn.ID())
switch category {
case path.EmailCategory:
_, itemData := generateItemData(t, category, uidn.ID(), mailDBF)
body, err := support.CreateMessageFromBytes(itemData)
require.NoError(t, err, "transforming mail bytes to messageable", clues.ToCore(err))
require.NoErrorf(t, err, "transforming mail bytes to messageable: %+v", clues.ToCore(err))
itm, err := cli.MailFolders().ByMailFolderId(containerID).Messages().Post(ctx, body, nil)
require.NoError(t, err, "posting email item", clues.ToCore(err))
itm, err := ac.Mail().PostItem(ctx, uidn.ID(), containerID, body)
require.NoError(t, err, clues.ToCore(err))
expectDeets.AddItem(
category.String(),
@ -1165,10 +1150,10 @@ func testExchangeContinuousBackups(suite *BackupOpIntegrationSuite, toggles cont
case path.ContactsCategory:
_, itemData := generateItemData(t, category, uidn.ID(), contactDBF)
body, err := support.CreateContactFromBytes(itemData)
require.NoError(t, err, "transforming contact bytes to contactable", clues.ToCore(err))
require.NoErrorf(t, err, "transforming contact bytes to contactable: %+v", clues.ToCore(err))
itm, err := cli.ContactFolders().ByContactFolderId(containerID).Contacts().Post(ctx, body, nil)
require.NoError(t, err, "posting contact item", clues.ToCore(err))
itm, err := ac.Contacts().PostItem(ctx, uidn.ID(), containerID, body)
require.NoError(t, err, clues.ToCore(err))
expectDeets.AddItem(
category.String(),
@ -1178,10 +1163,10 @@ func testExchangeContinuousBackups(suite *BackupOpIntegrationSuite, toggles cont
case path.EventsCategory:
_, itemData := generateItemData(t, category, uidn.ID(), eventDBF)
body, err := support.CreateEventFromBytes(itemData)
require.NoError(t, err, "transforming event bytes to eventable", clues.ToCore(err))
require.NoErrorf(t, err, "transforming event bytes to eventable: %+v", clues.ToCore(err))
itm, err := cli.Calendars().ByCalendarId(containerID).Events().Post(ctx, body, nil)
require.NoError(t, err, "posting events item", clues.ToCore(err))
itm, err := ac.Events().PostItem(ctx, uidn.ID(), containerID, body)
require.NoError(t, err, clues.ToCore(err))
expectDeets.AddItem(
category.String(),
@ -1200,7 +1185,6 @@ func testExchangeContinuousBackups(suite *BackupOpIntegrationSuite, toggles cont
updateUserData: func(t *testing.T) {
for category, d := range dataset {
containerID := d.dests[container1].containerID
cli := gc.Service.Client().Users().ByUserId(uidn.ID())
switch category {
case path.EmailCategory:
@ -1208,7 +1192,7 @@ func testExchangeContinuousBackups(suite *BackupOpIntegrationSuite, toggles cont
require.NoError(t, err, "getting message ids", clues.ToCore(err))
require.NotEmpty(t, ids, "message ids in folder")
err = cli.Messages().ByMessageId(ids[0]).Delete(ctx, nil)
err = ac.Mail().DeleteItem(ctx, uidn.ID(), ids[0])
require.NoError(t, err, "deleting email item", clues.ToCore(err))
expectDeets.RemoveItem(
@ -1221,7 +1205,7 @@ func testExchangeContinuousBackups(suite *BackupOpIntegrationSuite, toggles cont
require.NoError(t, err, "getting contact ids", clues.ToCore(err))
require.NotEmpty(t, ids, "contact ids in folder")
err = cli.Contacts().ByContactId(ids[0]).Delete(ctx, nil)
err = ac.Contacts().DeleteItem(ctx, uidn.ID(), ids[0])
require.NoError(t, err, "deleting contact item", clues.ToCore(err))
expectDeets.RemoveItem(
@ -1234,7 +1218,7 @@ func testExchangeContinuousBackups(suite *BackupOpIntegrationSuite, toggles cont
require.NoError(t, err, "getting event ids", clues.ToCore(err))
require.NotEmpty(t, ids, "event ids in folder")
err = cli.Calendars().ByCalendarId(ids[0]).Delete(ctx, nil)
err = ac.Events().DeleteItem(ctx, uidn.ID(), ids[0])
require.NoError(t, err, "deleting calendar", clues.ToCore(err))
expectDeets.RemoveItem(
@ -1666,14 +1650,12 @@ func runDriveIncrementalTest(
{
name: "update contents of a file",
updateFiles: func(t *testing.T) {
_, err := gc.Service.
Client().
Drives().
ByDriveId(driveID).
Items().
ByDriveItemId(ptr.Val(newFile.GetId())).
Content().
Put(ctx, []byte("new content"), nil)
err := api.PutDriveItemContent(
ctx,
gc.Service,
driveID,
ptr.Val(newFile.GetId()),
[]byte("new content"))
require.NoErrorf(t, err, "updating file contents: %v", clues.ToCore(err))
// no expectedDeets: neither file id nor location changed
},
@ -1692,13 +1674,12 @@ func runDriveIncrementalTest(
parentRef.SetId(&container)
driveItem.SetParentReference(parentRef)
_, err := gc.Service.
Client().
Drives().
ByDriveId(driveID).
Items().
ByDriveItemId(ptr.Val(newFile.GetId())).
Patch(ctx, driveItem, nil)
err := api.PatchDriveItem(
ctx,
gc.Service,
driveID,
ptr.Val(newFile.GetId()),
driveItem)
require.NoError(t, err, "renaming file %v", clues.ToCore(err))
},
itemsRead: 1, // .data file for newitem
@ -1716,13 +1697,12 @@ func runDriveIncrementalTest(
parentRef.SetId(&dest)
driveItem.SetParentReference(parentRef)
_, err := gc.Service.
Client().
Drives().
ByDriveId(driveID).
Items().
ByDriveItemId(ptr.Val(newFile.GetId())).
Patch(ctx, driveItem, nil)
err := api.PatchDriveItem(
ctx,
gc.Service,
driveID,
ptr.Val(newFile.GetId()),
driveItem)
require.NoErrorf(t, err, "moving file between folders %v", clues.ToCore(err))
expectDeets.MoveItem(
@ -1737,15 +1717,11 @@ func runDriveIncrementalTest(
{
name: "delete file",
updateFiles: func(t *testing.T) {
// deletes require unique http clients
// https://github.com/alcionai/corso/issues/2707
err = newDeleteServicer(t).
Client().
Drives().
ByDriveId(driveID).
Items().
ByDriveItemId(ptr.Val(newFile.GetId())).
Delete(ctx, nil)
err := api.DeleteDriveItem(
ctx,
newDeleteServicer(t),
driveID,
ptr.Val(newFile.GetId()))
require.NoErrorf(t, err, "deleting file %v", clues.ToCore(err))
expectDeets.RemoveItem(driveID, makeLocRef(container2), ptr.Val(newFile.GetId()))
@ -1765,13 +1741,12 @@ func runDriveIncrementalTest(
parentRef.SetId(&parent)
driveItem.SetParentReference(parentRef)
_, err := gc.Service.
Client().
Drives().
ByDriveId(driveID).
Items().
ByDriveItemId(child).
Patch(ctx, driveItem, nil)
err := api.PatchDriveItem(
ctx,
gc.Service,
driveID,
child,
driveItem)
require.NoError(t, err, "moving folder", clues.ToCore(err))
expectDeets.MoveLocation(
@ -1794,13 +1769,12 @@ func runDriveIncrementalTest(
parentRef.SetId(&parent)
driveItem.SetParentReference(parentRef)
_, err := gc.Service.
Client().
Drives().
ByDriveId(driveID).
Items().
ByDriveItemId(child).
Patch(ctx, driveItem, nil)
err := api.PatchDriveItem(
ctx,
gc.Service,
driveID,
child,
driveItem)
require.NoError(t, err, "renaming folder", clues.ToCore(err))
containerIDs[containerRename] = containerIDs[container2]
@ -1817,15 +1791,11 @@ func runDriveIncrementalTest(
name: "delete a folder",
updateFiles: func(t *testing.T) {
container := containerIDs[containerRename]
// deletes require unique http clients
// https://github.com/alcionai/corso/issues/2707
err = newDeleteServicer(t).
Client().
Drives().
ByDriveId(driveID).
Items().
ByDriveItemId(container).
Delete(ctx, nil)
err := api.DeleteDriveItem(
ctx,
newDeleteServicer(t),
driveID,
container)
require.NoError(t, err, "deleting folder", clues.ToCore(err))
expectDeets.RemoveLocation(driveID, makeLocRef(container1, containerRename))

View File

@ -1,8 +1,10 @@
package api
import (
"fmt"
"strings"
"github.com/alcionai/clues"
"github.com/microsoftgraph/msgraph-sdk-go/models"
"github.com/alcionai/corso/src/internal/common/ptr"
@ -23,3 +25,27 @@ func HasAttachments(body models.ItemBodyable) bool {
return strings.Contains(ptr.Val(body.GetContent()), "src=\"cid:")
}
func makeSessionAttachment(name string, size int64) *models.AttachmentItem {
attItem := models.NewAttachmentItem()
attType := models.FILE_ATTACHMENTTYPE
attItem.SetAttachmentType(&attType)
attItem.SetName(&name)
attItem.SetSize(&size)
return attItem
}
func GetAttachmentContent(attachment models.Attachmentable) ([]byte, error) {
ibs, err := attachment.GetBackingStore().Get("contentBytes")
if err != nil {
return nil, err
}
bs, ok := ibs.([]byte)
if !ok {
return nil, clues.New(fmt.Sprintf("unexpected type for attachment content: %T", ibs))
}
return bs, nil
}

View File

@ -31,7 +31,7 @@ type Contacts struct {
}
// ---------------------------------------------------------------------------
// methods
// containers
// ---------------------------------------------------------------------------
// CreateContactFolder makes a contact folder with the displayName of folderName.
@ -72,40 +72,29 @@ func (c Contacts) DeleteContainer(
return nil
}
// GetItem retrieves a Contactable item.
func (c Contacts) GetItem(
// prefer GetContainerByID where possible.
// use this only in cases where the models.ContactFolderable
// is required.
func (c Contacts) GetFolder(
ctx context.Context,
user, itemID string,
immutableIDs bool,
_ *fault.Bus, // no attachments to iterate over, so this goes unused
) (serialization.Parsable, *details.ExchangeInfo, error) {
options := &users.ItemContactsContactItemRequestBuilderGetRequestConfiguration{
Headers: newPreferHeaders(preferImmutableIDs(immutableIDs)),
}
cont, err := c.Stable.Client().Users().ByUserId(user).Contacts().ByContactId(itemID).Get(ctx, options)
userID, containerID string,
) (models.ContactFolderable, error) {
service, err := c.Service()
if err != nil {
return nil, nil, graph.Stack(ctx, err)
return nil, graph.Stack(ctx, err)
}
return cont, ContactInfo(cont), nil
}
func (c Contacts) GetContainerByID(
ctx context.Context,
userID, dirID string,
) (graph.Container, error) {
config := &users.ItemContactFoldersContactFolderItemRequestBuilderGetRequestConfiguration{
QueryParameters: &users.ItemContactFoldersContactFolderItemRequestBuilderGetQueryParameters{
Select: idAnd(displayName, parentFolderID),
},
}
resp, err := c.Stable.Client().
resp, err := service.Client().
Users().
ByUserId(userID).
ContactFolders().
ByContactFolderId(dirID).
ByContactFolderId(containerID).
Get(ctx, config)
if err != nil {
return nil, graph.Stack(ctx, err)
@ -114,6 +103,41 @@ func (c Contacts) GetContainerByID(
return resp, nil
}
// interface-compliant wrapper of GetFolder
func (c Contacts) GetContainerByID(
ctx context.Context,
userID, dirID string,
) (graph.Container, error) {
return c.GetFolder(ctx, userID, dirID)
}
func (c Contacts) PatchFolder(
ctx context.Context,
userID, containerID string,
body models.ContactFolderable,
) error {
service, err := c.Service()
if err != nil {
return graph.Stack(ctx, err)
}
_, err = service.Client().
Users().
ByUserId(userID).
ContactFolders().
ByContactFolderId(containerID).
Patch(ctx, body, nil)
if err != nil {
return graph.Wrap(ctx, err, "patching contact folder")
}
return nil
}
// ---------------------------------------------------------------------------
// container pager
// ---------------------------------------------------------------------------
// EnumerateContainers iterates through all of the users current
// contacts folders, converting each to a graph.CacheFolder, and calling
// fn(cf) on each one.
@ -187,6 +211,77 @@ func (c Contacts) EnumerateContainers(
return el.Failure()
}
// ---------------------------------------------------------------------------
// items
// ---------------------------------------------------------------------------
// GetItem retrieves a Contactable item.
func (c Contacts) GetItem(
ctx context.Context,
user, itemID string,
immutableIDs bool,
_ *fault.Bus, // no attachments to iterate over, so this goes unused
) (serialization.Parsable, *details.ExchangeInfo, error) {
options := &users.ItemContactsContactItemRequestBuilderGetRequestConfiguration{
Headers: newPreferHeaders(preferImmutableIDs(immutableIDs)),
}
cont, err := c.Stable.Client().Users().ByUserId(user).Contacts().ByContactId(itemID).Get(ctx, options)
if err != nil {
return nil, nil, graph.Stack(ctx, err)
}
return cont, ContactInfo(cont), nil
}
func (c Contacts) PostItem(
ctx context.Context,
userID, containerID string,
body models.Contactable,
) (models.Contactable, error) {
service, err := c.Service()
if err != nil {
return nil, graph.Stack(ctx, err)
}
itm, err := service.Client().
Users().
ByUserId(userID).
ContactFolders().
ByContactFolderId(containerID).
Contacts().
Post(ctx, body, nil)
if err != nil {
return nil, graph.Wrap(ctx, err, "creating contact")
}
return itm, nil
}
func (c Contacts) DeleteItem(
ctx context.Context,
userID, itemID string,
) error {
// deletes require unique http clients
// https://github.com/alcionai/corso/issues/2707
service, err := c.Service()
if err != nil {
return graph.Stack(ctx, err)
}
err = service.Client().
Users().
ByUserId(userID).
Contacts().
ByContactId(itemID).
Delete(ctx, nil)
if err != nil {
return graph.Wrap(ctx, err, "deleting contact")
}
return nil
}
// ---------------------------------------------------------------------------
// item pager
// ---------------------------------------------------------------------------

View File

@ -9,46 +9,12 @@ import (
"github.com/microsoftgraph/msgraph-sdk-go/models"
"github.com/alcionai/corso/src/internal/connector/graph"
"github.com/alcionai/corso/src/pkg/account"
)
// generic drive item getter
func GetDriveItem(
ctx context.Context,
srv graph.Servicer,
driveID, itemID string,
) (models.DriveItemable, error) {
di, err := srv.Client().
Drives().
ByDriveId(driveID).
Items().
ByDriveItemId(itemID).
Get(ctx, nil)
if err != nil {
return nil, graph.Wrap(ctx, err, "getting item")
}
return di, nil
}
func GetItemPermission(
ctx context.Context,
service graph.Servicer,
driveID, itemID string,
) (models.PermissionCollectionResponseable, error) {
perm, err := service.
Client().
Drives().
ByDriveId(driveID).
Items().
ByDriveItemId(itemID).
Permissions().
Get(ctx, nil)
if err != nil {
return nil, graph.Wrap(ctx, err, "getting item metadata").With("item_id", itemID)
}
return perm, nil
}
// ---------------------------------------------------------------------------
// Drives
// ---------------------------------------------------------------------------
func GetUsersDrive(
ctx context.Context,
@ -89,7 +55,11 @@ func GetDriveRoot(
srv graph.Servicer,
driveID string,
) (models.DriveItemable, error) {
root, err := srv.Client().Drives().ByDriveId(driveID).Root().Get(ctx, nil)
root, err := srv.Client().
Drives().
ByDriveId(driveID).
Root().
Get(ctx, nil)
if err != nil {
return nil, graph.Wrap(ctx, err, "getting drive root")
}
@ -97,6 +67,109 @@ func GetDriveRoot(
return root, nil
}
// ---------------------------------------------------------------------------
// Drive Items
// ---------------------------------------------------------------------------
// generic drive item getter
func GetDriveItem(
ctx context.Context,
srv graph.Servicer,
driveID, itemID string,
) (models.DriveItemable, error) {
di, err := srv.Client().
Drives().
ByDriveId(driveID).
Items().
ByDriveItemId(itemID).
Get(ctx, nil)
if err != nil {
return nil, graph.Wrap(ctx, err, "getting item")
}
return di, nil
}
func PostDriveItem(
ctx context.Context,
srv graph.Servicer,
driveID, itemID string,
) (models.UploadSessionable, error) {
session := drives.NewItemItemsItemCreateUploadSessionPostRequestBody()
r, err := srv.Client().
Drives().
ByDriveId(driveID).
Items().
ByDriveItemId(itemID).
CreateUploadSession().
Post(ctx, session, nil)
if err != nil {
return nil, graph.Wrap(ctx, err, "uploading drive item")
}
return r, nil
}
func PatchDriveItem(
ctx context.Context,
srv graph.Servicer,
driveID, itemID string,
item models.DriveItemable,
) error {
_, err := srv.Client().
Drives().
ByDriveId(driveID).
Items().
ByDriveItemId(itemID).
Patch(ctx, item, nil)
if err != nil {
return graph.Wrap(ctx, err, "patching drive item")
}
return nil
}
func PutDriveItemContent(
ctx context.Context,
srv graph.Servicer,
driveID, itemID string,
content []byte,
) error {
_, err := srv.Client().
Drives().
ByDriveId(driveID).
Items().
ByDriveItemId(itemID).
Content().
Put(ctx, content, nil)
if err != nil {
return graph.Wrap(ctx, err, "uploading drive item content")
}
return nil
}
// deletes require unique http clients
// https://github.com/alcionai/corso/issues/2707
func DeleteDriveItem(
ctx context.Context,
gs graph.Servicer,
driveID, itemID string,
) error {
err := gs.Client().
Drives().
ByDriveId(driveID).
Items().
ByDriveItemId(itemID).
Delete(ctx, nil)
if err != nil {
return graph.Wrap(ctx, err, "deleting item").With("item_id", itemID)
}
return nil
}
const itemByPathRawURLFmt = "https://graph.microsoft.com/v1.0/drives/%s/items/%s:/%s"
var ErrFolderNotFound = clues.New("folder not found")
@ -104,7 +177,7 @@ var ErrFolderNotFound = clues.New("folder not found")
// GetFolderByName will lookup the specified folder by name within the parentFolderID folder.
func GetFolderByName(
ctx context.Context,
service graph.Servicer,
srv graph.Servicer,
driveID, parentFolderID, folder string,
) (models.DriveItemable, error) {
// The `Children().Get()` API doesn't yet support $filter, so using that to find a folder
@ -113,7 +186,7 @@ func GetFolderByName(
// https://learn.microsoft.com/en-us/graph/onedrive-addressing-driveitems#path-based-addressing
// - which allows us to lookup an item by its path relative to the parent ID
rawURL := fmt.Sprintf(itemByPathRawURLFmt, driveID, parentFolderID, folder)
builder := drives.NewItemItemsDriveItemItemRequestBuilder(rawURL, service.Adapter())
builder := drives.NewItemItemsDriveItemItemRequestBuilder(rawURL, srv.Adapter())
foundItem, err := builder.Get(ctx, nil)
if err != nil {
@ -132,6 +205,30 @@ func GetFolderByName(
return foundItem, nil
}
// ---------------------------------------------------------------------------
// Permissions
// ---------------------------------------------------------------------------
func GetItemPermission(
ctx context.Context,
service graph.Servicer,
driveID, itemID string,
) (models.PermissionCollectionResponseable, error) {
perm, err := service.
Client().
Drives().
ByDriveId(driveID).
Items().
ByDriveItemId(itemID).
Permissions().
Get(ctx, nil)
if err != nil {
return nil, graph.Wrap(ctx, err, "getting item metadata").With("item_id", itemID)
}
return perm, nil
}
func PostItemPermissionUpdate(
ctx context.Context,
service graph.Servicer,
@ -153,3 +250,29 @@ func PostItemPermissionUpdate(
return itm, nil
}
func DeleteDriveItemPermission(
ctx context.Context,
creds account.M365Config,
driveID, itemID, permissionID string,
) error {
a, err := graph.CreateAdapter(creds.AzureTenantID, creds.AzureClientID, creds.AzureClientSecret)
if err != nil {
return graph.Wrap(ctx, err, "creating adapter to delete item permission")
}
err = graph.NewService(a).
Client().
Drives().
ByDriveId(driveID).
Items().
ByDriveItemId(itemID).
Permissions().
ByPermissionId(permissionID).
Delete(graph.ConsumeNTokens(ctx, graph.PermissionsLC), nil)
if err != nil {
return graph.Wrap(ctx, err, "deleting drive item permission")
}
return nil
}

View File

@ -1,8 +1,10 @@
package api
import (
"bytes"
"context"
"fmt"
"io"
"time"
"github.com/alcionai/clues"
@ -33,7 +35,7 @@ type Events struct {
}
// ---------------------------------------------------------------------------
// methods
// containers
// ---------------------------------------------------------------------------
// CreateCalendar makes an event Calendar with the name in the user's M365 exchange account
@ -74,10 +76,13 @@ func (c Events) DeleteContainer(
return nil
}
func (c Events) GetContainerByID(
// prefer GetContainerByID where possible.
// use this only in cases where the models.Calendarable
// is required.
func (c Events) GetCalendar(
ctx context.Context,
userID, containerID string,
) (graph.Container, error) {
) (models.Calendarable, error) {
service, err := c.Service()
if err != nil {
return nil, graph.Stack(ctx, err)
@ -89,14 +94,27 @@ func (c Events) GetContainerByID(
},
}
cal, err := service.Client().
resp, err := service.Client().
Users().
ByUserId(userID).
Calendars().
ByCalendarId(containerID).
Get(ctx, config)
if err != nil {
return nil, graph.Stack(ctx, err).WithClues(ctx)
return nil, graph.Stack(ctx, err)
}
return resp, nil
}
// interface-compliant wrapper of GetCalendar
func (c Events) GetContainerByID(
ctx context.Context,
userID, dirID string,
) (graph.Container, error) {
cal, err := c.GetCalendar(ctx, userID, dirID)
if err != nil {
return nil, err
}
return graph.CalendarDisplayable{Calendarable: cal}, nil
@ -141,56 +159,32 @@ func (c Events) GetContainerByName(
return cal, nil
}
// GetItem retrieves an Eventable item.
func (c Events) GetItem(
func (c Events) PatchCalendar(
ctx context.Context,
user, itemID string,
immutableIDs bool,
errs *fault.Bus,
) (serialization.Parsable, *details.ExchangeInfo, error) {
var (
err error
event models.Eventable
config = &users.ItemEventsEventItemRequestBuilderGetRequestConfiguration{
Headers: newPreferHeaders(preferImmutableIDs(immutableIDs)),
}
)
event, err = c.Stable.Client().
Users().
ByUserId(user).
Events().
ByEventId(itemID).
Get(ctx, config)
userID, containerID string,
body models.Calendarable,
) error {
service, err := c.Service()
if err != nil {
return nil, nil, graph.Stack(ctx, err)
return graph.Stack(ctx, err)
}
if ptr.Val(event.GetHasAttachments()) || HasAttachments(event.GetBody()) {
config := &users.ItemEventsItemAttachmentsRequestBuilderGetRequestConfiguration{
QueryParameters: &users.ItemEventsItemAttachmentsRequestBuilderGetQueryParameters{
Expand: []string{"microsoft.graph.itemattachment/item"},
},
Headers: newPreferHeaders(preferPageSize(maxNonDeltaPageSize), preferImmutableIDs(immutableIDs)),
}
attached, err := c.LargeItem.
Client().
_, err = service.Client().
Users().
ByUserId(user).
Events().
ByEventId(itemID).
Attachments().
Get(ctx, config)
ByUserId(userID).
Calendars().
ByCalendarId(containerID).
Patch(ctx, body, nil)
if err != nil {
return nil, nil, graph.Wrap(ctx, err, "event attachment download")
return graph.Wrap(ctx, err, "patching event calendar")
}
event.SetAttachments(attached.GetValue())
return nil
}
return event, EventInfo(event), nil
}
// ---------------------------------------------------------------------------
// container pager
// ---------------------------------------------------------------------------
// EnumerateContainers iterates through all of the users current
// calendars, converting each to a graph.CacheFolder, and
@ -272,6 +266,176 @@ const (
eventBetaDeltaURLTemplate = "https://graph.microsoft.com/beta/users/%s/calendars/%s/events/delta"
)
// ---------------------------------------------------------------------------
// items
// ---------------------------------------------------------------------------
// GetItem retrieves an Eventable item.
func (c Events) GetItem(
ctx context.Context,
user, itemID string,
immutableIDs bool,
errs *fault.Bus,
) (serialization.Parsable, *details.ExchangeInfo, error) {
var (
err error
event models.Eventable
config = &users.ItemEventsEventItemRequestBuilderGetRequestConfiguration{
Headers: newPreferHeaders(preferImmutableIDs(immutableIDs)),
}
)
event, err = c.Stable.Client().
Users().
ByUserId(user).
Events().
ByEventId(itemID).
Get(ctx, config)
if err != nil {
return nil, nil, graph.Stack(ctx, err)
}
if ptr.Val(event.GetHasAttachments()) || HasAttachments(event.GetBody()) {
config := &users.ItemEventsItemAttachmentsRequestBuilderGetRequestConfiguration{
QueryParameters: &users.ItemEventsItemAttachmentsRequestBuilderGetQueryParameters{
Expand: []string{"microsoft.graph.itemattachment/item"},
},
Headers: newPreferHeaders(preferPageSize(maxNonDeltaPageSize), preferImmutableIDs(immutableIDs)),
}
attached, err := c.LargeItem.
Client().
Users().
ByUserId(user).
Events().
ByEventId(itemID).
Attachments().
Get(ctx, config)
if err != nil {
return nil, nil, graph.Wrap(ctx, err, "event attachment download")
}
event.SetAttachments(attached.GetValue())
}
return event, EventInfo(event), nil
}
func (c Events) PostItem(
ctx context.Context,
userID, containerID string,
body models.Eventable,
) (models.Eventable, error) {
service, err := c.Service()
if err != nil {
return nil, graph.Stack(ctx, err)
}
itm, err := service.Client().
Users().
ByUserId(userID).
Calendars().
ByCalendarId(containerID).
Events().
Post(ctx, body, nil)
if err != nil {
return nil, graph.Wrap(ctx, err, "creating calendar event")
}
return itm, nil
}
func (c Events) DeleteItem(
ctx context.Context,
userID, itemID string,
) error {
// deletes require unique http clients
// https://github.com/alcionai/corso/issues/2707
service, err := c.Service()
if err != nil {
return graph.Stack(ctx, err)
}
err = service.Client().
Users().
ByUserId(userID).
Events().
ByEventId(itemID).
Delete(ctx, nil)
if err != nil {
return graph.Wrap(ctx, err, "deleting calendar event")
}
return nil
}
func (c Events) PostSmallAttachment(
ctx context.Context,
userID, containerID, parentItemID string,
body models.Attachmentable,
) error {
service, err := c.Service()
if err != nil {
return graph.Stack(ctx, err)
}
_, err = service.Client().
Users().
ByUserId(userID).
Calendars().
ByCalendarId(containerID).
Events().
ByEventId(parentItemID).
Attachments().
Post(ctx, body, nil)
if err != nil {
return graph.Wrap(ctx, err, "uploading small event attachment")
}
return nil
}
func (c Events) PostLargeAttachment(
ctx context.Context,
userID, containerID, parentItemID, name string,
size int64,
body models.Attachmentable,
) (models.UploadSessionable, error) {
bs, err := GetAttachmentContent(body)
if err != nil {
return nil, clues.Wrap(err, "serializing attachment content").WithClues(ctx)
}
session := users.NewItemCalendarEventsItemAttachmentsCreateUploadSessionPostRequestBody()
session.SetAttachmentItem(makeSessionAttachment(name, size))
us, err := c.LargeItem.
Client().
Users().
ByUserId(userID).
Calendars().
ByCalendarId(containerID).
Events().
ByEventId(parentItemID).
Attachments().
CreateUploadSession().
Post(ctx, session, nil)
if err != nil {
return nil, graph.Wrap(ctx, err, "uploading large event attachment")
}
url := ptr.Val(us.GetUploadUrl())
w := graph.NewLargeItemWriter(parentItemID, url, size)
copyBuffer := make([]byte, graph.AttachmentChunkSize)
_, err = io.CopyBuffer(w, bytes.NewReader(bs), copyBuffer)
if err != nil {
return nil, clues.Wrap(err, "buffering large attachment content").WithClues(ctx)
}
return us, nil
}
// ---------------------------------------------------------------------------
// item pager
// ---------------------------------------------------------------------------

View File

@ -1,8 +1,10 @@
package api
import (
"bytes"
"context"
"fmt"
"io"
"github.com/alcionai/clues"
"github.com/microsoft/kiota-abstractions-go/serialization"
@ -36,7 +38,7 @@ type Mail struct {
}
// ---------------------------------------------------------------------------
// methods
// containers
// ---------------------------------------------------------------------------
// CreateMailFolder makes a mail folder iff a folder of the same name does not exist
@ -113,10 +115,13 @@ func (c Mail) DeleteContainer(
return nil
}
func (c Mail) GetContainerByID(
// prefer GetContainerByID where possible.
// use this only in cases where the models.MailFolderable
// is required.
func (c Mail) GetFolder(
ctx context.Context,
userID, dirID string,
) (graph.Container, error) {
userID, containerID string,
) (models.MailFolderable, error) {
service, err := c.Service()
if err != nil {
return nil, graph.Stack(ctx, err)
@ -132,7 +137,7 @@ func (c Mail) GetContainerByID(
Users().
ByUserId(userID).
MailFolders().
ByMailFolderId(dirID).
ByMailFolderId(containerID).
Get(ctx, config)
if err != nil {
return nil, graph.Stack(ctx, err)
@ -141,6 +146,175 @@ func (c Mail) GetContainerByID(
return resp, nil
}
// interface-compliant wrapper of GetFolder
func (c Mail) GetContainerByID(
ctx context.Context,
userID, dirID string,
) (graph.Container, error) {
return c.GetFolder(ctx, userID, dirID)
}
func (c Mail) MoveContainer(
ctx context.Context,
userID, containerID string,
body users.ItemMailFoldersItemMovePostRequestBodyable,
) error {
service, err := c.Service()
if err != nil {
return graph.Stack(ctx, err)
}
_, err = service.
Client().
Users().
ByUserId(userID).
MailFolders().
ByMailFolderId(containerID).
Move().
Post(ctx, body, nil)
if err != nil {
return graph.Wrap(ctx, err, "moving mail folder")
}
return nil
}
func (c Mail) PatchFolder(
ctx context.Context,
userID, containerID string,
body models.MailFolderable,
) error {
service, err := c.Service()
if err != nil {
return graph.Stack(ctx, err)
}
_, err = service.Client().
Users().
ByUserId(userID).
MailFolders().
ByMailFolderId(containerID).
Patch(ctx, body, nil)
if err != nil {
return graph.Wrap(ctx, err, "patching mail folder")
}
return nil
}
// ---------------------------------------------------------------------------
// container pager
// ---------------------------------------------------------------------------
type mailFolderPager struct {
service graph.Servicer
builder *users.ItemMailFoldersRequestBuilder
}
func NewMailFolderPager(service graph.Servicer, user string) mailFolderPager {
// v1.0 non delta /mailFolders endpoint does not return any of the nested folders
rawURL := fmt.Sprintf(mailFoldersBetaURLTemplate, user)
builder := users.NewItemMailFoldersRequestBuilder(rawURL, service.Adapter())
return mailFolderPager{service, builder}
}
func (p *mailFolderPager) getPage(ctx context.Context) (PageLinker, error) {
page, err := p.builder.Get(ctx, nil)
if err != nil {
return nil, graph.Stack(ctx, err)
}
return page, nil
}
func (p *mailFolderPager) setNext(nextLink string) {
p.builder = users.NewItemMailFoldersRequestBuilder(nextLink, p.service.Adapter())
}
func (p *mailFolderPager) valuesIn(pl PageLinker) ([]models.MailFolderable, error) {
// Ideally this should be `users.ItemMailFoldersResponseable`, but
// that is not a thing as stable returns different result
page, ok := pl.(models.MailFolderCollectionResponseable)
if !ok {
return nil, clues.New("converting to ItemMailFoldersResponseable")
}
return page.GetValue(), nil
}
// EnumerateContainers iterates through all of the users current
// mail folders, converting each to a graph.CacheFolder, and calling
// fn(cf) on each one.
// Folder hierarchy is represented in its current state, and does
// not contain historical data.
func (c Mail) EnumerateContainers(
ctx context.Context,
userID, baseDirID string,
fn func(graph.CachedContainer) error,
errs *fault.Bus,
) error {
service, err := c.Service()
if err != nil {
return graph.Stack(ctx, err)
}
el := errs.Local()
pgr := NewMailFolderPager(service, userID)
for {
if el.Failure() != nil {
break
}
page, err := pgr.getPage(ctx)
if err != nil {
return graph.Stack(ctx, err)
}
resp, err := pgr.valuesIn(page)
if err != nil {
return graph.Stack(ctx, err)
}
for _, fold := range resp {
if el.Failure() != nil {
break
}
if err := graph.CheckIDNameAndParentFolderID(fold); err != nil {
errs.AddRecoverable(graph.Stack(ctx, err).Label(fault.LabelForceNoBackupCreation))
continue
}
fctx := clues.Add(
ctx,
"container_id", ptr.Val(fold.GetId()),
"container_name", ptr.Val(fold.GetDisplayName()))
temp := graph.NewCacheFolder(fold, nil, nil)
if err := fn(&temp); err != nil {
errs.AddRecoverable(graph.Stack(fctx, err).Label(fault.LabelForceNoBackupCreation))
continue
}
}
link, ok := ptr.ValOK(page.GetOdataNextLink())
if !ok {
break
}
pgr.setNext(link)
}
return el.Failure()
}
// ---------------------------------------------------------------------------
// items
// ---------------------------------------------------------------------------
// GetItem retrieves a Messageable item. If the item contains an attachment, that
// attachment is also downloaded.
func (c Mail) GetItem(
@ -265,109 +439,126 @@ func (c Mail) GetItem(
return mail, MailInfo(mail, size), nil
}
type mailFolderPager struct {
service graph.Servicer
builder *users.ItemMailFoldersRequestBuilder
}
func NewMailFolderPager(service graph.Servicer, user string) mailFolderPager {
// v1.0 non delta /mailFolders endpoint does not return any of the nested folders
rawURL := fmt.Sprintf(mailFoldersBetaURLTemplate, user)
builder := users.NewItemMailFoldersRequestBuilder(rawURL, service.Adapter())
return mailFolderPager{service, builder}
}
func (p *mailFolderPager) getPage(ctx context.Context) (PageLinker, error) {
page, err := p.builder.Get(ctx, nil)
func (c Mail) PostItem(
ctx context.Context,
userID, containerID string,
body models.Messageable,
) (models.Messageable, error) {
service, err := c.Service()
if err != nil {
return nil, graph.Stack(ctx, err)
}
return page, nil
itm, err := service.
Client().
Users().
ByUserId(userID).
MailFolders().
ByMailFolderId(containerID).
Messages().
Post(ctx, body, nil)
if err != nil {
return nil, graph.Wrap(ctx, err, "creating mail message")
}
func (p *mailFolderPager) setNext(nextLink string) {
p.builder = users.NewItemMailFoldersRequestBuilder(nextLink, p.service.Adapter())
if itm == nil {
return nil, clues.New("nil response mail message creation").WithClues(ctx)
}
func (p *mailFolderPager) valuesIn(pl PageLinker) ([]models.MailFolderable, error) {
// Ideally this should be `users.ItemMailFoldersResponseable`, but
// that is not a thing as stable returns different result
page, ok := pl.(models.MailFolderCollectionResponseable)
if !ok {
return nil, clues.New("converting to ItemMailFoldersResponseable")
return itm, nil
}
return page.GetValue(), nil
}
// EnumerateContainers iterates through all of the users current
// mail folders, converting each to a graph.CacheFolder, and calling
// fn(cf) on each one.
// Folder hierarchy is represented in its current state, and does
// not contain historical data.
func (c Mail) EnumerateContainers(
func (c Mail) DeleteItem(
ctx context.Context,
userID, baseDirID string,
fn func(graph.CachedContainer) error,
errs *fault.Bus,
userID, itemID string,
) error {
// deletes require unique http clients
// https://github.com/alcionai/corso/issues/2707
service, err := c.Service()
if err != nil {
return graph.Stack(ctx, err)
}
err = service.
Client().
Users().
ByUserId(userID).
Messages().
ByMessageId(itemID).
Delete(ctx, nil)
if err != nil {
return graph.Wrap(ctx, err, "deleting mail message")
}
return nil
}
func (c Mail) PostSmallAttachment(
ctx context.Context,
userID, containerID, parentItemID string,
body models.Attachmentable,
) error {
service, err := c.Service()
if err != nil {
return graph.Stack(ctx, err)
}
el := errs.Local()
pgr := NewMailFolderPager(service, userID)
for {
if el.Failure() != nil {
break
}
page, err := pgr.getPage(ctx)
_, err = service.
Client().
Users().
ByUserId(userID).
MailFolders().
ByMailFolderId(containerID).
Messages().
ByMessageId(parentItemID).
Attachments().
Post(ctx, body, nil)
if err != nil {
return graph.Stack(ctx, err)
return graph.Wrap(ctx, err, "uploading small mail attachment")
}
resp, err := pgr.valuesIn(page)
return nil
}
func (c Mail) PostLargeAttachment(
ctx context.Context,
userID, containerID, parentItemID, name string,
size int64,
body models.Attachmentable,
) (models.UploadSessionable, error) {
bs, err := GetAttachmentContent(body)
if err != nil {
return graph.Stack(ctx, err)
return nil, clues.Wrap(err, "serializing attachment content").WithClues(ctx)
}
for _, fold := range resp {
if el.Failure() != nil {
break
session := users.NewItemMailFoldersItemMessagesItemAttachmentsCreateUploadSessionPostRequestBody()
session.SetAttachmentItem(makeSessionAttachment(name, size))
us, err := c.LargeItem.
Client().
Users().
ByUserId(userID).
MailFolders().
ByMailFolderId(containerID).
Messages().
ByMessageId(parentItemID).
Attachments().
CreateUploadSession().
Post(ctx, session, nil)
if err != nil {
return nil, graph.Wrap(ctx, err, "uploading large mail attachment")
}
if err := graph.CheckIDNameAndParentFolderID(fold); err != nil {
errs.AddRecoverable(graph.Stack(ctx, err).Label(fault.LabelForceNoBackupCreation))
continue
url := ptr.Val(us.GetUploadUrl())
w := graph.NewLargeItemWriter(parentItemID, url, size)
copyBuffer := make([]byte, graph.AttachmentChunkSize)
_, err = io.CopyBuffer(w, bytes.NewReader(bs), copyBuffer)
if err != nil {
return nil, clues.Wrap(err, "buffering large attachment content").WithClues(ctx)
}
fctx := clues.Add(
ctx,
"container_id", ptr.Val(fold.GetId()),
"container_name", ptr.Val(fold.GetDisplayName()))
temp := graph.NewCacheFolder(fold, nil, nil)
if err := fn(&temp); err != nil {
errs.AddRecoverable(graph.Stack(fctx, err).Label(fault.LabelForceNoBackupCreation))
continue
}
}
link, ok := ptr.ValOK(page.GetOdataNextLink())
if !ok {
break
}
pgr.setNext(link)
}
return el.Failure()
return us, nil
}
// ---------------------------------------------------------------------------