GC: Restore: Split restore data into separate file. (#857)

## Description
Places `exchange.Restore` logic into single file
<!-- Insert PR description-->

## Type of change


- [x] 🐹 Trivial/Minor

## Issue(s)

<!-- Can reference multiple issues. Use one of the following "magic words" - "closes, fixes" to auto-close the Github issue. -->
* #<issue>

## Test Plan

<!-- How will this be tested prior to merging.-->
- [ ] 💪 Manual
- [x]  Unit test
- [ ] 💚 E2E
This commit is contained in:
Danny 2022-09-14 19:22:52 -04:00 committed by GitHub
parent 76a4f5531c
commit f8a10b4de6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 261 additions and 227 deletions

View File

@ -32,10 +32,6 @@ var (
const (
collectionChannelBufferSize = 1000
numberOfRetries = 4
// RestorePropertyTag defined:
// https://docs.microsoft.com/en-us/office/client-developer/outlook/mapi/pidtagmessageflags-canonical-property
RestorePropertyTag = "Integer 0x0E07"
RestoreCanonicalEnableValue = "4"
)
// Collection implements the interface from data.Collection

View File

@ -0,0 +1,29 @@
package exchange
// exchange_vars.go is package level collection of interfaces and
// constants that are used within the exchange.
// Legacy Value Tags and constants are used to override certain values within
// M365 objects.
// Master Property Value Document:
// https://interoperability.blob.core.windows.net/files/MS-OXPROPS/%5bMS-OXPROPS%5d.pdf
const (
// MailRestorePropertyTag inhibits exchange.Mail.Message from being "resent" through the server.
// DEFINED: Section 2.791 PidTagMessageFlags
MailRestorePropertyTag = "Integer 0x0E07"
// RestoreCanonicalEnableValue marks message as sent via RopSubmitMessage
// Defined: https://interoperability.blob.core.windows.net/files/MS-OXCMSG/%5bMS-OXCMSG%5d.pdf
// Section: 2.2.1.6 PidTagMessageFlags Property
//nolint:lll
// Additional Information: https://docs.microsoft.com/en-us/office/client-developer/outlook/mapi/pidtagmessageflags-canonical-property
RestoreCanonicalEnableValue = "4"
// MailSendTimeOverrideProperty allows for send time to be updated.
// Section: 2.635 PidTagClientSubmitTime
MailSendDateTimeOverrideProperty = "SystemTime 0x0039"
// MailReceiveDateTimeOverrideProperty allows receive date time to be updated.
// Section: 2.789 PidTagMessageDeliveryTime
MailReceiveDateTimeOverriveProperty = "SystemTime 0x0E06"
)

View File

@ -1,7 +1,6 @@
package exchange
import (
"context"
"fmt"
"strings"
@ -11,13 +10,9 @@ import (
"github.com/microsoftgraph/msgraph-sdk-go/models"
"github.com/pkg/errors"
"github.com/alcionai/corso/src/internal/common"
"github.com/alcionai/corso/src/internal/connector/graph"
"github.com/alcionai/corso/src/internal/connector/support"
"github.com/alcionai/corso/src/internal/path"
"github.com/alcionai/corso/src/pkg/account"
"github.com/alcionai/corso/src/pkg/control"
"github.com/alcionai/corso/src/pkg/logger"
"github.com/alcionai/corso/src/pkg/selectors"
)
@ -374,221 +369,3 @@ func SetupExchangeCollectionVars(scope selectors.ExchangeScope) (
return nil, nil, nil, errors.New("exchange scope option not supported")
}
// GetRestoreContainer utility function to create
// an unique folder for the restore process
// @param category: input from fullPath()[2]
// that defines the application the folder is created in.
func GetRestoreContainer(
service graph.Service,
user string,
category path.CategoryType,
) (string, error) {
name := fmt.Sprintf("Corso_Restore_%s", common.FormatNow(common.SimpleDateTimeFormat))
option := categoryToOptionIdentifier(category)
folderID, err := GetContainerID(service, name, user, option)
if err == nil {
return *folderID, nil
}
// Experienced error other than folder does not exist
if !errors.Is(err, ErrFolderNotFound) {
return "", support.WrapAndAppend(user, err, err)
}
switch option {
case messages:
fold, err := CreateMailFolder(service, user, name)
if err != nil {
return "", support.WrapAndAppend(user, err, err)
}
return *fold.GetId(), nil
case contacts:
fold, err := CreateContactFolder(service, user, name)
if err != nil {
return "", support.WrapAndAppend(user, err, err)
}
return *fold.GetId(), nil
case events:
calendar, err := CreateCalendar(service, user, name)
if err != nil {
return "", support.WrapAndAppend(user, err, err)
}
return *calendar.GetId(), nil
default:
return "", fmt.Errorf("category: %s not supported for folder creation", option)
}
}
func RestoreExchangeObject(
ctx context.Context,
bits []byte,
category path.CategoryType,
policy control.CollisionPolicy,
service graph.Service,
destination, user string,
) error {
if policy != control.Copy {
return fmt.Errorf("restore policy: %s not supported", policy)
}
setting := categoryToOptionIdentifier(category)
switch setting {
case messages:
return RestoreMailMessage(ctx, bits, service, control.Copy, destination, user)
case contacts:
return RestoreExchangeContact(ctx, bits, service, control.Copy, destination, user)
case events:
return RestoreExchangeEvent(ctx, bits, service, control.Copy, destination, user)
default:
return fmt.Errorf("type: %s not supported for exchange restore", category)
}
}
// 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(
ctx context.Context,
bits []byte,
service graph.Service,
cp control.CollisionPolicy,
destination, user string,
) error {
contact, err := support.CreateContactFromBytes(bits)
if err != nil {
return err
}
response, err := service.Client().UsersById(user).ContactFoldersById(destination).Contacts().Post(contact)
if err != nil {
return errors.Wrap(err, support.ConnectorStackErrorTrace(err))
}
if response == nil {
return errors.New("msgraph contact post fail: REST response not received")
}
return nil
}
func RestoreExchangeEvent(
ctx context.Context,
bits []byte,
service graph.Service,
cp control.CollisionPolicy,
destination, user string,
) error {
event, err := support.CreateEventFromBytes(bits)
if err != nil {
return err
}
response, err := service.Client().UsersById(user).CalendarsById(destination).Events().Post(event)
if err != nil {
return errors.Wrap(err, support.ConnectorStackErrorTrace(err))
}
if response == nil {
return errors.New("msgraph event post fail: REST response not received")
}
return nil
}
// 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(
ctx context.Context,
bits []byte,
service graph.Service,
cp control.CollisionPolicy,
destination,
user string,
) error {
// Creates messageable object from original bytes
originalMessage, err := support.CreateMessageFromBytes(bits)
if err != nil {
return errors.Wrapf(err, "restore mail message rcvd: %v", bits)
}
// Sets fields from original message from storage
clone := support.ToMessage(originalMessage)
valueID := RestorePropertyTag
enableValue := RestoreCanonicalEnableValue
// Set Extended Properties:
// 1st: No transmission
// 2nd: Send Date
// 3rd: Recv Date
sv1 := models.NewSingleValueLegacyExtendedProperty()
sv1.SetId(&valueID)
sv1.SetValue(&enableValue)
sv2 := models.NewSingleValueLegacyExtendedProperty()
sendPropertyValue := common.FormatLegacyTime(*clone.GetSentDateTime())
sendPropertyTag := "SystemTime 0x0039"
sv2.SetId(&sendPropertyTag)
sv2.SetValue(&sendPropertyValue)
sv3 := models.NewSingleValueLegacyExtendedProperty()
recvPropertyValue := common.FormatLegacyTime(*clone.GetReceivedDateTime())
recvPropertyTag := "SystemTime 0x0E06"
sv3.SetId(&recvPropertyTag)
sv3.SetValue(&recvPropertyValue)
svlep := []models.SingleValueLegacyExtendedPropertyable{sv1, sv2, sv3}
clone.SetSingleValueExtendedProperties(svlep)
// Switch workflow based on collision policy
switch cp {
default:
logger.Ctx(ctx).DPanicw("unrecognized restore policy; defaulting to copy",
"policy", cp)
fallthrough
case control.Copy:
return SendMailToBackStore(service, user, destination, clone)
}
}
// 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(service graph.Service, user, destination string, message models.Messageable) error {
var (
sentMessage models.Messageable
err error
)
for count := 0; count < numberOfRetries; count++ {
sentMessage, err = service.Client().UsersById(user).MailFoldersById(destination).Messages().Post(message)
if err == nil && sentMessage != nil {
break
}
}
if err != nil {
return support.WrapAndAppend(": "+support.ConnectorStackErrorTrace(err), err, nil)
}
if sentMessage == nil {
return errors.New("message not Sent: blocked by server")
}
if err != nil {
return support.WrapAndAppend(": "+support.ConnectorStackErrorTrace(err), err, nil)
}
return nil
}

View File

@ -0,0 +1,232 @@
package exchange
import (
"context"
"fmt"
"github.com/microsoftgraph/msgraph-sdk-go/models"
"github.com/pkg/errors"
"github.com/alcionai/corso/src/internal/common"
"github.com/alcionai/corso/src/internal/connector/graph"
"github.com/alcionai/corso/src/internal/connector/support"
"github.com/alcionai/corso/src/internal/path"
"github.com/alcionai/corso/src/pkg/control"
"github.com/alcionai/corso/src/pkg/logger"
)
// GetRestoreContainer utility function to create
// an unique folder for the restore process
// @param category: input from fullPath()[2]
// that defines the application the folder is created in.
func GetRestoreContainer(
service graph.Service,
user string,
category path.CategoryType,
) (string, error) {
name := fmt.Sprintf("Corso_Restore_%s", common.FormatNow(common.SimpleDateTimeFormat))
option := categoryToOptionIdentifier(category)
folderID, err := GetContainerID(service, name, user, option)
if err == nil {
return *folderID, nil
}
// Experienced error other than folder does not exist
if !errors.Is(err, ErrFolderNotFound) {
return "", support.WrapAndAppend(user, err, err)
}
switch option {
case messages:
fold, err := CreateMailFolder(service, user, name)
if err != nil {
return "", support.WrapAndAppend(user, err, err)
}
return *fold.GetId(), nil
case contacts:
fold, err := CreateContactFolder(service, user, name)
if err != nil {
return "", support.WrapAndAppend(user, err, err)
}
return *fold.GetId(), nil
case events:
calendar, err := CreateCalendar(service, user, name)
if err != nil {
return "", support.WrapAndAppend(user, err, err)
}
return *calendar.GetId(), nil
default:
return "", fmt.Errorf("category: %s not supported for folder creation", option)
}
}
// RestoreExchangeObject 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(
ctx context.Context,
bits []byte,
category path.CategoryType,
policy control.CollisionPolicy,
service graph.Service,
destination, user string,
) error {
if policy != control.Copy {
return fmt.Errorf("restore policy: %s not supported", policy)
}
setting := categoryToOptionIdentifier(category)
switch setting {
case messages:
return RestoreMailMessage(ctx, bits, service, control.Copy, destination, user)
case contacts:
return RestoreExchangeContact(ctx, bits, service, control.Copy, destination, user)
case events:
return RestoreExchangeEvent(ctx, bits, service, control.Copy, destination, user)
default:
return fmt.Errorf("type: %s not supported for exchange restore", category)
}
}
// 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(
ctx context.Context,
bits []byte,
service graph.Service,
cp control.CollisionPolicy,
destination, user string,
) error {
contact, err := support.CreateContactFromBytes(bits)
if err != nil {
return err
}
response, err := service.Client().UsersById(user).ContactFoldersById(destination).Contacts().Post(contact)
if err != nil {
return errors.Wrap(err, support.ConnectorStackErrorTrace(err))
}
if response == nil {
return errors.New("msgraph contact post fail: REST response not received")
}
return 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(
ctx context.Context,
bits []byte,
service graph.Service,
cp control.CollisionPolicy,
destination, user string,
) error {
event, err := support.CreateEventFromBytes(bits)
if err != nil {
return err
}
response, err := service.Client().UsersById(user).CalendarsById(destination).Events().Post(event)
if err != nil {
return errors.Wrap(err, support.ConnectorStackErrorTrace(err))
}
if response == nil {
return errors.New("msgraph event post fail: REST response not received")
}
return nil
}
// 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(
ctx context.Context,
bits []byte,
service graph.Service,
cp control.CollisionPolicy,
destination,
user string,
) error {
// Creates messageable object from original bytes
originalMessage, err := support.CreateMessageFromBytes(bits)
if err != nil {
return err
}
// Sets fields from original message from storage
clone := support.ToMessage(originalMessage)
valueID := MailRestorePropertyTag
enableValue := RestoreCanonicalEnableValue
// Set Extended Properties:
// 1st: No transmission
// 2nd: Send Date
// 3rd: Recv Date
sv1 := models.NewSingleValueLegacyExtendedProperty()
sv1.SetId(&valueID)
sv1.SetValue(&enableValue)
sv2 := models.NewSingleValueLegacyExtendedProperty()
sendPropertyValue := common.FormatLegacyTime(*clone.GetSentDateTime())
sendPropertyTag := MailSendDateTimeOverrideProperty
sv2.SetId(&sendPropertyTag)
sv2.SetValue(&sendPropertyValue)
sv3 := models.NewSingleValueLegacyExtendedProperty()
recvPropertyValue := common.FormatLegacyTime(*clone.GetReceivedDateTime())
recvPropertyTag := MailReceiveDateTimeOverriveProperty
sv3.SetId(&recvPropertyTag)
sv3.SetValue(&recvPropertyValue)
svlep := []models.SingleValueLegacyExtendedPropertyable{sv1, sv2, sv3}
clone.SetSingleValueExtendedProperties(svlep)
// Switch workflow based on collision policy
switch cp {
default:
logger.Ctx(ctx).DPanicw("unrecognized restore policy; defaulting to copy",
"policy", cp)
fallthrough
case control.Copy:
return SendMailToBackStore(service, user, destination, clone)
}
}
// 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(service graph.Service, user, destination string, message models.Messageable) error {
sentMessage, err := service.Client().UsersById(user).MailFoldersById(destination).Messages().Post(message)
if err != nil {
return support.WrapAndAppend(": "+support.ConnectorStackErrorTrace(err), err, nil)
}
if sentMessage == nil {
return errors.New("message not Sent: blocked by server")
}
if err != nil {
return support.WrapAndAppend(": "+support.ConnectorStackErrorTrace(err), err, nil)
}
return nil
}