From 7181e2ef90eedcfe7451c9a96949a6978163a8c3 Mon Sep 17 00:00:00 2001 From: Keepers Date: Wed, 24 May 2023 15:45:07 -0600 Subject: [PATCH] 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_entry: No #### Type of change - [x] :broom: Tech Debt/Cleanup #### Issue(s) * #1996 #### Test Plan - [x] :zap: Unit test - [x] :green_heart: E2E --- src/cmd/purge/purge.go | 6 +- src/internal/connector/data_collections.go | 2 +- src/internal/connector/exchange/attachment.go | 97 ++--- .../exchange/attachment_uploadable.go | 144 ------- .../connector/exchange/restore_test.go | 11 +- .../connector/exchange/service_restore.go | 209 ++++------ src/internal/connector/graph/consts.go | 2 + .../connector/graph/middleware_test.go | 2 + .../connector/graph_connector_helper_test.go | 6 +- .../graph_connector_onedrive_test.go | 5 +- src/internal/connector/onedrive/drive.go | 21 - src/internal/connector/onedrive/drive_test.go | 2 +- src/internal/connector/onedrive/item.go | 20 +- src/internal/connector/onedrive/item_test.go | 4 +- src/internal/connector/onedrive/permission.go | 21 +- src/internal/connector/sharepoint/queries.go | 28 -- .../operations/backup_integration_test.go | 160 +++----- src/pkg/services/m365/api/attachments.go | 26 ++ src/pkg/services/m365/api/contacts.go | 139 +++++-- src/pkg/services/m365/api/drive.go | 205 ++++++++-- src/pkg/services/m365/api/events.go | 260 +++++++++--- src/pkg/services/m365/api/mail.go | 373 +++++++++++++----- 22 files changed, 1013 insertions(+), 730 deletions(-) delete mode 100644 src/internal/connector/exchange/attachment_uploadable.go delete mode 100644 src/internal/connector/sharepoint/queries.go diff --git a/src/cmd/purge/purge.go b/src/cmd/purge/purge.go index 337ea6f46..70948a2b3 100644 --- a/src/cmd/purge/purge.go +++ b/src/cmd/purge/purge.go @@ -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) diff --git a/src/internal/connector/data_collections.go b/src/internal/connector/data_collections.go index dbfe882e0..e642418a5 100644 --- a/src/internal/connector/data_collections.go +++ b/src/internal/connector/data_collections.go @@ -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: diff --git a/src/internal/connector/exchange/attachment.go b/src/internal/connector/exchange/attachment.go index 07fb0e7dd..640bb04e7 100644 --- a/src/internal/connector/exchange/attachment.go +++ b/src/internal/connector/exchange/attachment.go @@ -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()) diff --git a/src/internal/connector/exchange/attachment_uploadable.go b/src/internal/connector/exchange/attachment_uploadable.go deleted file mode 100644 index 1423f56df..000000000 --- a/src/internal/connector/exchange/attachment_uploadable.go +++ /dev/null @@ -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 -} diff --git a/src/internal/connector/exchange/restore_test.go b/src/internal/connector/exchange/restore_test.go index eb0a16ac2..78892f361 100644 --- a/src/internal/connector/exchange/restore_test.go +++ b/src/internal/connector/exchange/restore_test.go @@ -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, diff --git a/src/internal/connector/exchange/service_restore.go b/src/internal/connector/exchange/service_restore.go index 4ddf369c2..5286652fa 100644 --- a/src/internal/connector/exchange/service_restore.go +++ b/src/internal/connector/exchange/service_restore.go @@ -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, diff --git a/src/internal/connector/graph/consts.go b/src/internal/connector/graph/consts.go index 32a549e8c..0438ab10c 100644 --- a/src/internal/connector/graph/consts.go +++ b/src/internal/connector/graph/consts.go @@ -7,6 +7,8 @@ import ( "github.com/alcionai/corso/src/pkg/path" ) +const AttachmentChunkSize = 4 * 1024 * 1024 + // --------------------------------------------------------------------------- // item response AdditionalData // --------------------------------------------------------------------------- diff --git a/src/internal/connector/graph/middleware_test.go b/src/internal/connector/graph/middleware_test.go index b5ca4b3af..15faf7a7a 100644 --- a/src/internal/connector/graph/middleware_test.go +++ b/src/internal/connector/graph/middleware_test.go @@ -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(). diff --git a/src/internal/connector/graph_connector_helper_test.go b/src/internal/connector/graph_connector_helper_test.go index 4776c93f8..a38b5236b 100644 --- a/src/internal/connector/graph_connector_helper_test.go +++ b/src/internal/connector/graph_connector_helper_test.go @@ -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 } diff --git a/src/internal/connector/graph_connector_onedrive_test.go b/src/internal/connector/graph_connector_onedrive_test.go index d63b8a96b..859ed6ea0 100644 --- a/src/internal/connector/graph_connector_onedrive_test.go +++ b/src/internal/connector/graph_connector_onedrive_test.go @@ -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()) } diff --git a/src/internal/connector/onedrive/drive.go b/src/internal/connector/onedrive/drive.go index 6a09a9d00..e75b38cd9 100644 --- a/src/internal/connector/onedrive/drive.go +++ b/src/internal/connector/onedrive/drive.go @@ -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 -} diff --git a/src/internal/connector/onedrive/drive_test.go b/src/internal/connector/onedrive/drive_test.go index 4becbec31..8b8ddafb6 100644 --- a/src/internal/connector/onedrive/drive_test.go +++ b/src/internal/connector/onedrive/drive_test.go @@ -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") } diff --git a/src/internal/connector/onedrive/item.go b/src/internal/connector/onedrive/item.go index 2c9046ebf..52e8d1ef7 100644 --- a/src/internal/connector/onedrive/item.go +++ b/src/internal/connector/onedrive/item.go @@ -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 diff --git a/src/internal/connector/onedrive/item_test.go b/src/internal/connector/onedrive/item_test.go index 3d5124526..a30037b43 100644 --- a/src/internal/connector/onedrive/item_test.go +++ b/src/internal/connector/onedrive/item_test.go @@ -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 diff --git a/src/internal/connector/onedrive/permission.go b/src/internal/connector/onedrive/permission.go index 6bd142f5d..5f7eaa998 100644 --- a/src/internal/connector/onedrive/permission.go +++ b/src/internal/connector/onedrive/permission.go @@ -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) } } diff --git a/src/internal/connector/sharepoint/queries.go b/src/internal/connector/sharepoint/queries.go deleted file mode 100644 index 867165075..000000000 --- a/src/internal/connector/sharepoint/queries.go +++ /dev/null @@ -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 -} diff --git a/src/internal/operations/backup_integration_test.go b/src/internal/operations/backup_integration_test.go index eaf308be5..3f8c5f242 100644 --- a/src/internal/operations/backup_integration_test.go +++ b/src/internal/operations/backup_integration_test.go @@ -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)) diff --git a/src/pkg/services/m365/api/attachments.go b/src/pkg/services/m365/api/attachments.go index e5125a64a..6023f29bb 100644 --- a/src/pkg/services/m365/api/attachments.go +++ b/src/pkg/services/m365/api/attachments.go @@ -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 +} diff --git a/src/pkg/services/m365/api/contacts.go b/src/pkg/services/m365/api/contacts.go index 03abc2018..1bfdad984 100644 --- a/src/pkg/services/m365/api/contacts.go +++ b/src/pkg/services/m365/api/contacts.go @@ -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 // --------------------------------------------------------------------------- diff --git a/src/pkg/services/m365/api/drive.go b/src/pkg/services/m365/api/drive.go index 8201f51e3..81efa1936 100644 --- a/src/pkg/services/m365/api/drive.go +++ b/src/pkg/services/m365/api/drive.go @@ -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 +} diff --git a/src/pkg/services/m365/api/events.go b/src/pkg/services/m365/api/events.go index fe534579a..f0e749f81 100644 --- a/src/pkg/services/m365/api/events.go +++ b/src/pkg/services/m365/api/events.go @@ -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,57 +159,33 @@ 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(). - 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()) + _, err = service.Client(). + Users(). + ByUserId(userID). + Calendars(). + ByCalendarId(containerID). + Patch(ctx, body, nil) + if err != nil { + return graph.Wrap(ctx, err, "patching event calendar") } - return event, EventInfo(event), nil + return nil } +// --------------------------------------------------------------------------- +// container pager +// --------------------------------------------------------------------------- + // EnumerateContainers iterates through all of the users current // calendars, converting each to a graph.CacheFolder, and // calling fn(cf) on each one. @@ -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 // --------------------------------------------------------------------------- diff --git a/src/pkg/services/m365/api/mail.go b/src/pkg/services/m365/api/mail.go index 30a96cb4e..f48ffce4b 100644 --- a/src/pkg/services/m365/api/mail.go +++ b/src/pkg/services/m365/api/mail.go @@ -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 -} - -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") + 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") } - 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 -// 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) - 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) + _, err = service. + Client(). + Users(). + ByUserId(userID). + MailFolders(). + ByMailFolderId(containerID). + Messages(). + ByMessageId(parentItemID). + Attachments(). + Post(ctx, body, nil) + if err != nil { + return graph.Wrap(ctx, err, "uploading small mail attachment") } - 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 } // ---------------------------------------------------------------------------