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

View File

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

View File

@ -1,24 +1,35 @@
package exchange package exchange
import ( import (
"bytes"
"context" "context"
"io" "fmt"
"github.com/alcionai/clues" "github.com/alcionai/clues"
"github.com/microsoftgraph/msgraph-sdk-go/models" "github.com/microsoftgraph/msgraph-sdk-go/models"
"github.com/alcionai/corso/src/internal/common/ptr" "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/internal/connector/support"
"github.com/alcionai/corso/src/pkg/logger" "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 ( const (
// Use large attachment logic for attachments > 3MB // Use large attachment logic for attachments > 3MB
// https://learn.microsoft.com/en-us/graph/outlook-large-attachments // https://learn.microsoft.com/en-us/graph/outlook-large-attachments
largeAttachmentSize = int32(3 * 1024 * 1024) largeAttachmentSize = int32(3 * 1024 * 1024)
attachmentChunkSize = 4 * 1024 * 1024
fileAttachmentOdataValue = "#microsoft.graph.fileAttachment" fileAttachmentOdataValue = "#microsoft.graph.fileAttachment"
itemAttachmentOdataValue = "#microsoft.graph.itemAttachment" itemAttachmentOdataValue = "#microsoft.graph.itemAttachment"
referenceAttachmentOdataValue = "#microsoft.graph.referenceAttachment" referenceAttachmentOdataValue = "#microsoft.graph.referenceAttachment"
@ -43,23 +54,30 @@ func attachmentType(attachment models.Attachmentable) models.AttachmentType {
// uploadAttachment will upload the specified message attachment to M365 // uploadAttachment will upload the specified message attachment to M365
func uploadAttachment( func uploadAttachment(
ctx context.Context, ctx context.Context,
uploader attachmentUploadable, cli attachmentPoster,
userID, containerID, parentItemID string,
attachment models.Attachmentable, attachment models.Attachmentable,
) error { ) 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 = clues.Add(
ctx, ctx,
"attachment_size", ptr.Val(attachment.GetSize()), "attachment_size", size,
"attachment_id", ptr.Val(attachment.GetId()), "attachment_id", id,
"attachment_name", clues.Hide(ptr.Val(attachment.GetName())), "attachment_name", clues.Hide(name),
"attachment_type", attachmentType, "attachment_type", attachmentType,
"internal_item_type", getItemAttachmentItemType(attachment), "attachment_odata_type", ptr.Val(attachment.GetOdataType()),
"uploader_item_id", uploader.getItemID()) "attachment_outlook_odata_type", getOutlookOdataType(attachment),
"parent_item_id", parentItemID)
logger.Ctx(ctx).Debug("uploading attachment") 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()) { if attachmentType == models.REFERENCE_ATTACHMENTTYPE && ptr.Val(attachment.GetIsInline()) {
logger.Ctx(ctx).Debug("skip uploading inline reference attachment") logger.Ctx(ctx).Debug("skip uploading inline reference attachment")
return nil return nil
@ -69,67 +87,32 @@ func uploadAttachment(
if attachmentType == models.ITEM_ATTACHMENTTYPE { if attachmentType == models.ITEM_ATTACHMENTTYPE {
a, err := support.ToItemAttachment(attachment) a, err := support.ToItemAttachment(attachment)
if err != nil { 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 return nil
} }
attachment = a attachment = a
} }
// For Item/Reference attachments *or* file attachments < 3MB, use the attachments endpoint // for file attachments sized >= 3MB
if attachmentType != models.FILE_ATTACHMENTTYPE || ptr.Val(attachment.GetSize()) < largeAttachmentSize { if attachmentType == models.FILE_ATTACHMENTTYPE && size >= largeAttachmentSize {
return uploader.uploadSmallAttachment(ctx, attachment) _, 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 func getOutlookOdataType(query models.Attachmentable) string {
// 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 := ""
attachment, ok := query.(models.ItemAttachmentable) attachment, ok := query.(models.ItemAttachmentable)
if !ok { if !ok {
return empty return ""
} }
item := attachment.GetItem() item := attachment.GetItem()
if item == nil { if item == nil {
return empty return ""
} }
return ptr.Val(item.GetOdataType()) 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)) assert.NoError(t, err, clues.ToCore(err))
}() }()
info, err := RestoreExchangeContact( info, err := RestoreContact(
ctx, ctx,
exchMock.ContactBytes("Corso TestContact"), exchMock.ContactBytes("Corso TestContact"),
suite.gs, suite.ac.Contacts(),
control.Copy, control.Copy,
folderID, folderID,
userID) userID)
@ -135,9 +135,11 @@ func (suite *RestoreIntgSuite) TestRestoreEvent() {
ctx, flush := tester.NewContext(t) ctx, flush := tester.NewContext(t)
defer flush() defer flush()
info, err := RestoreExchangeEvent( info, err := RestoreEvent(
ctx, ctx,
test.bytes, test.bytes,
suite.ac.Events(),
suite.ac.Events(),
suite.gs, suite.gs,
control.Copy, control.Copy,
calendarID, calendarID,
@ -365,11 +367,12 @@ func (suite *RestoreIntgSuite) TestRestoreExchangeObject() {
defer flush() defer flush()
destination := test.destination(t, ctx) destination := test.destination(t, ctx)
info, err := RestoreExchangeObject( info, err := RestoreItem(
ctx, ctx,
test.bytes, test.bytes,
test.category, test.category,
control.Copy, control.Copy,
suite.ac,
service, service,
destination, destination,
userID, userID,

View File

@ -25,15 +25,24 @@ import (
"github.com/alcionai/corso/src/pkg/services/m365/api" "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 // based on the path.CategoryType. All input params are necessary to perform
// the type-specific restore function. // the type-specific restore function.
func RestoreExchangeObject( func RestoreItem(
ctx context.Context, ctx context.Context,
bits []byte, bits []byte,
category path.CategoryType, category path.CategoryType,
policy control.CollisionPolicy, policy control.CollisionPolicy,
service graph.Servicer, ac api.Client,
gs graph.Servicer,
destination, user string, destination, user string,
errs *fault.Bus, errs *fault.Bus,
) (*details.ExchangeInfo, error) { ) (*details.ExchangeInfo, error) {
@ -43,26 +52,21 @@ func RestoreExchangeObject(
switch category { switch category {
case path.EmailCategory: 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: 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: 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: default:
return nil, clues.Wrap(clues.New(category.String()), "not supported for Exchange restore") return nil, clues.Wrap(clues.New(category.String()), "not supported for Exchange restore")
} }
} }
// RestoreExchangeContact restores a contact to the @bits byte // RestoreContact wraps api.Contacts().PostItem()
// representation of M365 contact object. func RestoreContact(
// @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(
ctx context.Context, ctx context.Context,
bits []byte, bits []byte,
service graph.Servicer, cli itemPoster[models.Contactable],
cp control.CollisionPolicy, cp control.CollisionPolicy,
destination, user string, destination, user string,
) (*details.ExchangeInfo, error) { ) (*details.ExchangeInfo, error) {
@ -73,19 +77,9 @@ func RestoreExchangeContact(
ctx = clues.Add(ctx, "item_id", ptr.Val(contact.GetId())) ctx = clues.Add(ctx, "item_id", ptr.Val(contact.GetId()))
response, err := service.Client(). _, err = cli.PostItem(ctx, user, destination, contact)
Users().
ByUserId(user).
ContactFolders().
ByContactFolderId(destination).
Contacts().
Post(ctx, contact, nil)
if err != nil { if err != nil {
return nil, graph.Wrap(ctx, err, "uploading Contact") return nil, clues.Stack(err)
}
if response == nil {
return nil, clues.New("nil response from post").WithClues(ctx)
} }
info := api.ContactInfo(contact) info := api.ContactInfo(contact)
@ -94,16 +88,13 @@ func RestoreExchangeContact(
return info, nil return info, nil
} }
// RestoreExchangeEvent restores a contact to the @bits byte // RestoreEvent wraps api.Events().PostItem()
// representation of M365 event object. func RestoreEvent(
// @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(
ctx context.Context, ctx context.Context,
bits []byte, bits []byte,
service graph.Servicer, itemCli itemPoster[models.Eventable],
attachmentCli attachmentPoster,
gs graph.Servicer,
cp control.CollisionPolicy, cp control.CollisionPolicy,
destination, user string, destination, user string,
errs *fault.Bus, errs *fault.Bus,
@ -127,34 +118,24 @@ func RestoreExchangeEvent(
transformedEvent.SetAttachments([]models.Attachmentable{}) transformedEvent.SetAttachments([]models.Attachmentable{})
} }
response, err := service.Client(). item, err := itemCli.PostItem(ctx, user, destination, event)
Users().
ByUserId(user).
Calendars().
ByCalendarId(destination).
Events().
Post(ctx, transformedEvent, nil)
if err != nil { if err != nil {
return nil, graph.Wrap(ctx, err, "uploading event") return nil, clues.Stack(err)
} }
if response == nil { for _, a := range attached {
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 {
if el.Failure() != nil { if el.Failure() != nil {
break 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) el.AddRecoverable(err)
} }
} }
@ -165,30 +146,27 @@ func RestoreExchangeEvent(
return info, el.Failure() return info, el.Failure()
} }
// RestoreMailMessage utility function to place an exchange.Mail // RestoreMessage wraps api.Mail().PostItem(), handling attachment creation along the way
// message into the user's M365 Exchange account. func RestoreMessage(
// @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(
ctx context.Context, ctx context.Context,
bits []byte, bits []byte,
service graph.Servicer, itemCli itemPoster[models.Messageable],
attachmentCli attachmentPoster,
gs graph.Servicer,
cp control.CollisionPolicy, cp control.CollisionPolicy,
destination, user string, destination, user string,
errs *fault.Bus, errs *fault.Bus,
) (*details.ExchangeInfo, error) { ) (*details.ExchangeInfo, error) {
// Creates messageable object from original bytes // Creates messageable object from original bytes
originalMessage, err := support.CreateMessageFromBytes(bits) msg, err := support.CreateMessageFromBytes(bits)
if err != nil { if err != nil {
return nil, clues.Wrap(err, "creating mail from bytes").WithClues(ctx) 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 ( var (
clone = support.ToMessage(originalMessage) clone = support.ToMessage(msg)
valueID = MailRestorePropertyTag valueID = MailRestorePropertyTag
enableValue = RestoreCanonicalEnableValue enableValue = RestoreCanonicalEnableValue
) )
@ -225,80 +203,35 @@ func RestoreMailMessage(
clone.SetSingleValueExtendedProperties(svlep) clone.SetSingleValueExtendedProperties(svlep)
if err := SendMailToBackStore(ctx, service, user, destination, clone, errs); err != nil { attached := clone.GetAttachments()
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()
// Item.Attachments --> HasAttachments doesn't always have a value populated when deserialized // Item.Attachments --> HasAttachments doesn't always have a value populated when deserialized
message.SetAttachments([]models.Attachmentable{}) clone.SetAttachments([]models.Attachmentable{})
response, err := service.Client(). item, err := itemCli.PostItem(ctx, user, destination, clone)
Users().
ByUserId(user).
MailFolders().
ByMailFolderId(destination).
Messages().
Post(ctx, message, nil)
if err != nil { if err != nil {
return graph.Wrap(ctx, err, "restoring mail") return nil, graph.Wrap(ctx, err, "restoring mail message")
} }
if response == nil { el := errs.Local()
return clues.New("nil response from post").WithClues(ctx)
}
var ( for _, a := range attached {
el = errs.Local()
id = ptr.Val(response.GetId())
uploader = &mailAttachmentUploader{
userID: user,
folderID: destination,
itemID: id,
service: service,
}
)
for _, attachment := range attached {
if el.Failure() != nil { if el.Failure() != nil {
break return nil, el.Failure()
} }
if err := uploadAttachment(ctx, uploader, attachment); err != nil { err := uploadAttachment(
if ptr.Val(attachment.GetOdataType()) == "#microsoft.graph.itemAttachment" { ctx,
name := ptr.Val(attachment.GetName()) 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). logger.CtxErr(ctx, err).
With("attachment_name", name). With("attachment_name", name).
@ -308,20 +241,18 @@ func SendMailToBackStore(
} }
el.AddRecoverable(clues.Wrap(err, "uploading mail attachment")) 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. // store through GraphAPI.
// @param dest: container destination to M365 func RestoreCollections(
func RestoreExchangeDataCollections(
ctx context.Context, ctx context.Context,
creds account.M365Config, creds account.M365Config,
ac api.Client,
gs graph.Servicer, gs graph.Servicer,
dest control.RestoreDestination, dest control.RestoreDestination,
dcs []data.RestoreCollection, dcs []data.RestoreCollection,
@ -365,7 +296,7 @@ func RestoreExchangeDataCollections(
continue 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) metrics = support.CombineMetrics(metrics, temp)
@ -387,6 +318,7 @@ func RestoreExchangeDataCollections(
// restoreCollection handles restoration of an individual collection. // restoreCollection handles restoration of an individual collection.
func restoreCollection( func restoreCollection(
ctx context.Context, ctx context.Context,
ac api.Client,
gs graph.Servicer, gs graph.Servicer,
dc data.RestoreCollection, dc data.RestoreCollection,
folderID string, folderID string,
@ -444,11 +376,12 @@ func restoreCollection(
byteArray := buf.Bytes() byteArray := buf.Bytes()
info, err := RestoreExchangeObject( info, err := RestoreItem(
ictx, ictx,
byteArray, byteArray,
category, category,
policy, policy,
ac,
gs, gs,
folderID, folderID,
user, user,

View File

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

View File

@ -227,6 +227,8 @@ func (suite *RetryMWIntgSuite) TestRetryMiddleware_RetryRequest_resetBodyAfter50
adpt, err := mockAdapter(suite.creds, mw) adpt, err := mockAdapter(suite.creds, mw)
require.NoError(t, err, clues.ToCore(err)) 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). _, err = NewService(adpt).
Client(). Client().
Users(). Users().

View File

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

View File

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

View File

@ -326,24 +326,3 @@ func GetAllFolders(
return res, el.Failure() 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 // deletes require unique http clients
// https://github.com/alcionai/corso/issues/2707 // 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 { if err != nil {
logger.CtxErr(ictx, err).Errorw("deleting folder") logger.CtxErr(ictx, err).Errorw("deleting folder")
} }

View File

@ -9,7 +9,6 @@ import (
"strings" "strings"
"github.com/alcionai/clues" "github.com/alcionai/clues"
"github.com/microsoftgraph/msgraph-sdk-go/drives"
"github.com/microsoftgraph/msgraph-sdk-go/models" "github.com/microsoftgraph/msgraph-sdk-go/models"
"github.com/alcionai/corso/src/internal/common/ptr" "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 // TODO: @vkamra verify if var session is the desired input
func driveItemWriter( func driveItemWriter(
ctx context.Context, ctx context.Context,
service graph.Servicer, gs graph.Servicer,
driveID, itemID string, driveID, itemID string,
itemSize int64, itemSize int64,
) (io.Writer, error) { ) (io.Writer, error) {
session := drives.NewItemItemsItemCreateUploadSessionPostRequestBody()
ctx = clues.Add(ctx, "upload_item_id", itemID) ctx = clues.Add(ctx, "upload_item_id", itemID)
r, err := service.Client(). r, err := api.PostDriveItem(ctx, gs, driveID, itemID)
Drives().
ByDriveId(driveID).
Items().
ByDriveItemId(itemID).
CreateUploadSession().
Post(ctx, session, nil)
if err != nil { 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 iw, nil
return graph.NewLargeItemWriter(itemID, url, itemSize), nil
} }
// constructWebURL helper function for recreating the webURL // constructWebURL helper function for recreating the webURL

View File

@ -154,7 +154,7 @@ func (suite *ItemIntegrationSuite) TestItemWriter() {
srv := suite.service 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)) require.NoError(t, err, clues.ToCore(err))
newFolderName := tester.DefaultTestRestoreDestination("folder").ContainerName newFolderName := tester.DefaultTestRestoreDestination("folder").ContainerName
@ -233,7 +233,7 @@ func (suite *ItemIntegrationSuite) TestDriveGetFolder() {
srv := suite.service 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)) require.NoError(t, err, clues.ToCore(err))
// Lookup a folder that doesn't exist // Lookup a folder that doesn't exist

View File

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

View File

@ -1,8 +1,10 @@
package api package api
import ( import (
"fmt"
"strings" "strings"
"github.com/alcionai/clues"
"github.com/microsoftgraph/msgraph-sdk-go/models" "github.com/microsoftgraph/msgraph-sdk-go/models"
"github.com/alcionai/corso/src/internal/common/ptr" "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:") 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. // CreateContactFolder makes a contact folder with the displayName of folderName.
@ -72,40 +72,29 @@ func (c Contacts) DeleteContainer(
return nil return nil
} }
// GetItem retrieves a Contactable item. // prefer GetContainerByID where possible.
func (c Contacts) GetItem( // use this only in cases where the models.ContactFolderable
// is required.
func (c Contacts) GetFolder(
ctx context.Context, ctx context.Context,
user, itemID string, userID, containerID string,
immutableIDs bool, ) (models.ContactFolderable, error) {
_ *fault.Bus, // no attachments to iterate over, so this goes unused service, err := c.Service()
) (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 { 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{ config := &users.ItemContactFoldersContactFolderItemRequestBuilderGetRequestConfiguration{
QueryParameters: &users.ItemContactFoldersContactFolderItemRequestBuilderGetQueryParameters{ QueryParameters: &users.ItemContactFoldersContactFolderItemRequestBuilderGetQueryParameters{
Select: idAnd(displayName, parentFolderID), Select: idAnd(displayName, parentFolderID),
}, },
} }
resp, err := c.Stable.Client(). resp, err := service.Client().
Users(). Users().
ByUserId(userID). ByUserId(userID).
ContactFolders(). ContactFolders().
ByContactFolderId(dirID). ByContactFolderId(containerID).
Get(ctx, config) Get(ctx, config)
if err != nil { if err != nil {
return nil, graph.Stack(ctx, err) return nil, graph.Stack(ctx, err)
@ -114,6 +103,41 @@ func (c Contacts) GetContainerByID(
return resp, nil 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 // EnumerateContainers iterates through all of the users current
// contacts folders, converting each to a graph.CacheFolder, and calling // contacts folders, converting each to a graph.CacheFolder, and calling
// fn(cf) on each one. // fn(cf) on each one.
@ -187,6 +211,77 @@ func (c Contacts) EnumerateContainers(
return el.Failure() 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 // item pager
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@ -9,46 +9,12 @@ import (
"github.com/microsoftgraph/msgraph-sdk-go/models" "github.com/microsoftgraph/msgraph-sdk-go/models"
"github.com/alcionai/corso/src/internal/connector/graph" "github.com/alcionai/corso/src/internal/connector/graph"
"github.com/alcionai/corso/src/pkg/account"
) )
// generic drive item getter // ---------------------------------------------------------------------------
func GetDriveItem( // Drives
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
}
func GetUsersDrive( func GetUsersDrive(
ctx context.Context, ctx context.Context,
@ -89,7 +55,11 @@ func GetDriveRoot(
srv graph.Servicer, srv graph.Servicer,
driveID string, driveID string,
) (models.DriveItemable, error) { ) (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 { if err != nil {
return nil, graph.Wrap(ctx, err, "getting drive root") return nil, graph.Wrap(ctx, err, "getting drive root")
} }
@ -97,6 +67,109 @@ func GetDriveRoot(
return root, nil 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" const itemByPathRawURLFmt = "https://graph.microsoft.com/v1.0/drives/%s/items/%s:/%s"
var ErrFolderNotFound = clues.New("folder not found") 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. // GetFolderByName will lookup the specified folder by name within the parentFolderID folder.
func GetFolderByName( func GetFolderByName(
ctx context.Context, ctx context.Context,
service graph.Servicer, srv graph.Servicer,
driveID, parentFolderID, folder string, driveID, parentFolderID, folder string,
) (models.DriveItemable, error) { ) (models.DriveItemable, error) {
// The `Children().Get()` API doesn't yet support $filter, so using that to find a folder // 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 // 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 // - which allows us to lookup an item by its path relative to the parent ID
rawURL := fmt.Sprintf(itemByPathRawURLFmt, driveID, parentFolderID, folder) 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) foundItem, err := builder.Get(ctx, nil)
if err != nil { if err != nil {
@ -132,6 +205,30 @@ func GetFolderByName(
return foundItem, nil 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( func PostItemPermissionUpdate(
ctx context.Context, ctx context.Context,
service graph.Servicer, service graph.Servicer,
@ -153,3 +250,29 @@ func PostItemPermissionUpdate(
return itm, nil 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 package api
import ( import (
"bytes"
"context" "context"
"fmt" "fmt"
"io"
"time" "time"
"github.com/alcionai/clues" "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 // 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 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, ctx context.Context,
userID, containerID string, userID, containerID string,
) (graph.Container, error) { ) (models.Calendarable, error) {
service, err := c.Service() service, err := c.Service()
if err != nil { if err != nil {
return nil, graph.Stack(ctx, err) return nil, graph.Stack(ctx, err)
@ -89,14 +94,27 @@ func (c Events) GetContainerByID(
}, },
} }
cal, err := service.Client(). resp, err := service.Client().
Users(). Users().
ByUserId(userID). ByUserId(userID).
Calendars(). Calendars().
ByCalendarId(containerID). ByCalendarId(containerID).
Get(ctx, config) Get(ctx, config)
if err != nil { 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 return graph.CalendarDisplayable{Calendarable: cal}, nil
@ -141,57 +159,33 @@ func (c Events) GetContainerByName(
return cal, nil return cal, nil
} }
// GetItem retrieves an Eventable item. func (c Events) PatchCalendar(
func (c Events) GetItem(
ctx context.Context, ctx context.Context,
user, itemID string, userID, containerID string,
immutableIDs bool, body models.Calendarable,
errs *fault.Bus, ) error {
) (serialization.Parsable, *details.ExchangeInfo, error) { service, err := c.Service()
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 { if err != nil {
return nil, nil, graph.Stack(ctx, err) return graph.Stack(ctx, err)
} }
if ptr.Val(event.GetHasAttachments()) || HasAttachments(event.GetBody()) { _, err = service.Client().
config := &users.ItemEventsItemAttachmentsRequestBuilderGetRequestConfiguration{ Users().
QueryParameters: &users.ItemEventsItemAttachmentsRequestBuilderGetQueryParameters{ ByUserId(userID).
Expand: []string{"microsoft.graph.itemattachment/item"}, Calendars().
}, ByCalendarId(containerID).
Headers: newPreferHeaders(preferPageSize(maxNonDeltaPageSize), preferImmutableIDs(immutableIDs)), Patch(ctx, body, nil)
} if err != nil {
return graph.Wrap(ctx, err, "patching event calendar")
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 return nil
} }
// ---------------------------------------------------------------------------
// container pager
// ---------------------------------------------------------------------------
// EnumerateContainers iterates through all of the users current // EnumerateContainers iterates through all of the users current
// calendars, converting each to a graph.CacheFolder, and // calendars, converting each to a graph.CacheFolder, and
// calling fn(cf) on each one. // calling fn(cf) on each one.
@ -272,6 +266,176 @@ const (
eventBetaDeltaURLTemplate = "https://graph.microsoft.com/beta/users/%s/calendars/%s/events/delta" 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 // item pager
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@ -1,8 +1,10 @@
package api package api
import ( import (
"bytes"
"context" "context"
"fmt" "fmt"
"io"
"github.com/alcionai/clues" "github.com/alcionai/clues"
"github.com/microsoft/kiota-abstractions-go/serialization" "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 // 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 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, ctx context.Context,
userID, dirID string, userID, containerID string,
) (graph.Container, error) { ) (models.MailFolderable, error) {
service, err := c.Service() service, err := c.Service()
if err != nil { if err != nil {
return nil, graph.Stack(ctx, err) return nil, graph.Stack(ctx, err)
@ -132,7 +137,7 @@ func (c Mail) GetContainerByID(
Users(). Users().
ByUserId(userID). ByUserId(userID).
MailFolders(). MailFolders().
ByMailFolderId(dirID). ByMailFolderId(containerID).
Get(ctx, config) Get(ctx, config)
if err != nil { if err != nil {
return nil, graph.Stack(ctx, err) return nil, graph.Stack(ctx, err)
@ -141,6 +146,175 @@ func (c Mail) GetContainerByID(
return resp, nil 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 // GetItem retrieves a Messageable item. If the item contains an attachment, that
// attachment is also downloaded. // attachment is also downloaded.
func (c Mail) GetItem( func (c Mail) GetItem(
@ -265,109 +439,126 @@ func (c Mail) GetItem(
return mail, MailInfo(mail, size), nil return mail, MailInfo(mail, size), nil
} }
type mailFolderPager struct { func (c Mail) PostItem(
service graph.Servicer ctx context.Context,
builder *users.ItemMailFoldersRequestBuilder userID, containerID string,
} body models.Messageable,
) (models.Messageable, error) {
func NewMailFolderPager(service graph.Servicer, user string) mailFolderPager { service, err := c.Service()
// 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 { if err != nil {
return nil, graph.Stack(ctx, err) return nil, graph.Stack(ctx, err)
} }
return page, nil itm, err := service.
} Client().
Users().
func (p *mailFolderPager) setNext(nextLink string) { ByUserId(userID).
p.builder = users.NewItemMailFoldersRequestBuilder(nextLink, p.service.Adapter()) MailFolders().
} ByMailFolderId(containerID).
Messages().
func (p *mailFolderPager) valuesIn(pl PageLinker) ([]models.MailFolderable, error) { Post(ctx, body, nil)
// Ideally this should be `users.ItemMailFoldersResponseable`, but if err != nil {
// that is not a thing as stable returns different result return nil, graph.Wrap(ctx, err, "creating mail message")
page, ok := pl.(models.MailFolderCollectionResponseable)
if !ok {
return nil, clues.New("converting to ItemMailFoldersResponseable")
} }
return page.GetValue(), nil if itm == nil {
return nil, clues.New("nil response mail message creation").WithClues(ctx)
}
return itm, nil
} }
// EnumerateContainers iterates through all of the users current func (c Mail) DeleteItem(
// 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, ctx context.Context,
userID, baseDirID string, userID, itemID string,
fn func(graph.CachedContainer) error, ) error {
errs *fault.Bus, // 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 { ) error {
service, err := c.Service() service, err := c.Service()
if err != nil { if err != nil {
return graph.Stack(ctx, err) return graph.Stack(ctx, err)
} }
el := errs.Local() _, err = service.
Client().
pgr := NewMailFolderPager(service, userID) Users().
ByUserId(userID).
for { MailFolders().
if el.Failure() != nil { ByMailFolderId(containerID).
break Messages().
} ByMessageId(parentItemID).
Attachments().
page, err := pgr.getPage(ctx) Post(ctx, body, nil)
if err != nil { if err != nil {
return graph.Stack(ctx, err) return graph.Wrap(ctx, err, "uploading small mail attachment")
}
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() 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 nil, clues.Wrap(err, "serializing attachment content").WithClues(ctx)
}
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")
}
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
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------