GC: Use of graph.ContainerResolver for directory structure and data retrieval (#1134)

## Description

`graph.ContainerResolver` has the capacity to keep the directory structure of the m365 objects that are helpful within a user's account. Leveraging this abstraction allows for a better flow of data from M365 into storage. 

## Type of change


- [x]  : Optimization

## Issue(s)

* closes #1125<issue>
* closes #1122
## Test Plan


- [x]  Unit test
This commit is contained in:
Danny 2022-10-20 08:49:09 -04:00 committed by GitHub
parent d30f6963c0
commit bb7e48a82e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 441 additions and 890 deletions

View File

@ -17,7 +17,6 @@ import (
"github.com/alcionai/corso/src/pkg/account" "github.com/alcionai/corso/src/pkg/account"
"github.com/alcionai/corso/src/pkg/control" "github.com/alcionai/corso/src/pkg/control"
"github.com/alcionai/corso/src/pkg/path" "github.com/alcionai/corso/src/pkg/path"
"github.com/alcionai/corso/src/pkg/selectors"
) )
type ExchangeServiceSuite struct { type ExchangeServiceSuite struct {
@ -233,37 +232,6 @@ func (suite *ExchangeServiceSuite) TestOptionsForContacts() {
} }
} }
// TestSetupExchangeCollection ensures SetupExchangeCollectionVars returns a non-nil variable for
// the following selector types:
// - Mail
// - Contacts
// - Events
func (suite *ExchangeServiceSuite) TestSetupExchangeCollection() {
userID := tester.M365UserID(suite.T())
sel := selectors.NewExchangeBackup()
// Exchange mail uses a different system to fetch items. Right now the old
// function for it will return an error so we know if it gets called.
sel.Include(
sel.ContactFolders([]string{userID}, selectors.Any()),
sel.EventCalendars([]string{userID}, selectors.Any()),
)
eb, err := sel.ToExchangeBackup()
require.NoError(suite.T(), err)
scopes := eb.Scopes()
for _, test := range scopes {
suite.T().Run(test.Category().String(), func(t *testing.T) {
discriminateFunc, graphQuery, iterFunc, err := SetupExchangeCollectionVars(test)
assert.NoError(t, err)
assert.NotNil(t, discriminateFunc)
assert.NotNil(t, graphQuery)
assert.NotNil(t, iterFunc)
})
}
}
// TestGraphQueryFunctions verifies if Query functions APIs // TestGraphQueryFunctions verifies if Query functions APIs
// through Microsoft Graph are functional // through Microsoft Graph are functional
func (suite *ExchangeServiceSuite) TestGraphQueryFunctions() { func (suite *ExchangeServiceSuite) TestGraphQueryFunctions() {

View File

@ -68,10 +68,12 @@ func loadService(t *testing.T) *exchangeService {
return service return service
} }
// TestIterativeFunctions verifies that GraphQuery to Iterate // TestCollectionFunctions verifies ability to gather
// functions are valid for current versioning of msgraph-go-sdk. // containers functions are valid for current versioning of msgraph-go-sdk.
// Tests for mail have been moved to graph_connector_test.go. // Tests for mail have been moved to graph_connector_test.go.
func (suite *ExchangeIteratorSuite) TestIterativeFunctions() { // exchange.Mail uses a sequential delta function.
// TODO: Add exchange.Mail when delta iterator functionality implemented
func (suite *ExchangeIteratorSuite) TestCollectionFunctions() {
ctx, flush := tester.NewContext() ctx, flush := tester.NewContext()
defer flush() defer flush()
@ -93,63 +95,39 @@ func (suite *ExchangeIteratorSuite) TestIterativeFunctions() {
tests := []struct { tests := []struct {
name string name string
queryFunction GraphQuery queryFunc GraphQuery
iterativeFunction GraphIterateFunc
scope selectors.ExchangeScope scope selectors.ExchangeScope
transformer absser.ParsableFactory iterativeFunction func(
folderNames map[string]struct{} container map[string]graph.Container,
aFilter string,
errUpdater func(string, error)) func(any) bool
transformer absser.ParsableFactory
}{ }{
{ {
name: "Contacts Iterative Check", name: "Contacts Iterative Check",
queryFunction: GetAllContactFolderNamesForUser, queryFunc: GetAllContactFolderNamesForUser,
iterativeFunction: IterateSelectAllContactsForCollections,
scope: contactScope[0],
transformer: models.CreateContactFolderCollectionResponseFromDiscriminatorValue, transformer: models.CreateContactFolderCollectionResponseFromDiscriminatorValue,
}, { iterativeFunction: IterativeCollectContactContainers,
name: "Contact Folder Traversal", },
queryFunction: GetAllContactFolderNamesForUser, {
iterativeFunction: IterateSelectAllContactsForCollections,
scope: contactScope[0],
transformer: models.CreateContactFolderCollectionResponseFromDiscriminatorValue,
}, {
name: "Events Iterative Check", name: "Events Iterative Check",
queryFunction: GetAllCalendarNamesForUser, queryFunc: GetAllCalendarNamesForUser,
iterativeFunction: IterateSelectAllEventsFromCalendars,
scope: eventScope[0],
transformer: models.CreateCalendarCollectionResponseFromDiscriminatorValue, transformer: models.CreateCalendarCollectionResponseFromDiscriminatorValue,
}, { iterativeFunction: IterativeCollectCalendarContainers,
name: "Folder Iterative Check Contacts",
queryFunction: GetAllContactFolderNamesForUser,
iterativeFunction: IterateFilterContainersForCollections,
scope: contactScope[0],
transformer: models.CreateContactFolderCollectionResponseFromDiscriminatorValue,
}, {
name: "Default Contacts Folder",
queryFunction: GetDefaultContactFolderForUser,
iterativeFunction: IterateSelectAllContactsForCollections,
scope: contactScope[0],
transformer: models.CreateContactFolderCollectionResponseFromDiscriminatorValue,
}, },
} }
for _, test := range tests { for _, test := range tests {
suite.T().Run(test.name, func(t *testing.T) { suite.T().Run(test.name, func(t *testing.T) {
service := loadService(t) service := loadService(t)
response, err := test.queryFunction(ctx, service, userID) response, err := test.queryFunc(ctx, service, userID)
require.NoError(t, err) require.NoError(t, err)
// Create Iterator // Iterator Creation
pageIterator, err := msgraphgocore.NewPageIterator(response, pageIterator, err := msgraphgocore.NewPageIterator(response,
&service.adapter, &service.adapter,
test.transformer) test.transformer)
require.NoError(t, err) require.NoError(t, err)
qp := graph.QueryParams{
User: userID,
Scope: test.scope,
Credentials: service.credentials,
FailFast: false,
}
// Create collection for iterate test // Create collection for iterate test
collections := make(map[string]*Collection) collections := make(map[string]graph.Container)
var errs error var errs error
errUpdater := func(id string, err error) { errUpdater := func(id string, err error) {
errs = support.WrapAndAppend(id, err, errs) errs = support.WrapAndAppend(id, err, errs)
@ -157,14 +135,7 @@ func (suite *ExchangeIteratorSuite) TestIterativeFunctions() {
// callbackFunc iterates through all models.Messageable and fills exchange.Collection.jobs[] // callbackFunc iterates through all models.Messageable and fills exchange.Collection.jobs[]
// with corresponding item IDs. New collections are created for each directory // with corresponding item IDs. New collections are created for each directory
callbackFunc := test.iterativeFunction( callbackFunc := test.iterativeFunction(
ctx, collections, "", errUpdater)
qp,
errUpdater,
collections,
nil,
nil,
)
iterateError := pageIterator.Iterate(ctx, callbackFunc) iterateError := pageIterator.Iterate(ctx, callbackFunc)
assert.NoError(t, iterateError) assert.NoError(t, iterateError)
assert.NoError(t, errs) assert.NoError(t, errs)

View File

@ -101,7 +101,7 @@ func (mc *mailFolderCache) Populate(
for _, f := range resp.GetValue() { for _, f := range resp.GetValue() {
if err := mc.AddToCache(ctx, f); err != nil { if err := mc.AddToCache(ctx, f); err != nil {
errs = multierror.Append(errs, err) errs = multierror.Append(errs, errors.Wrap(err, "delta fetch"))
continue continue
} }
} }

View File

@ -133,7 +133,7 @@ func (suite *MailFolderCacheUnitSuite) TestCheckRequiredValues() {
for _, test := range table { for _, test := range table {
suite.T().Run(test.name, func(t *testing.T) { suite.T().Run(test.name, func(t *testing.T) {
test.check(t, checkRequiredValues(test.c)) test.check(t, graph.CheckRequiredValues(test.c))
}) })
} }
} }
@ -366,6 +366,7 @@ func (suite *MailFolderCacheIntegrationSuite) TestDeltaFetch() {
require.NoError(t, mfc.Populate(ctx, test.root, test.path...)) require.NoError(t, mfc.Populate(ctx, test.root, test.path...))
p, err := mfc.IDToPath(ctx, testFolderID) p, err := mfc.IDToPath(ctx, testFolderID)
t.Logf("Path: %s\n", p.String())
require.NoError(t, err) require.NoError(t, err)
expectedPath := stdpath.Join(append(test.path, expectedFolderPath)...) expectedPath := stdpath.Join(append(test.path, expectedFolderPath)...)

View File

@ -19,7 +19,6 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/alcionai/corso/src/pkg/path" "github.com/alcionai/corso/src/pkg/path"
"github.com/alcionai/corso/src/pkg/selectors"
) )
//----------------------------------------------------------------------- //-----------------------------------------------------------------------
@ -119,19 +118,6 @@ func categoryToOptionIdentifier(category path.CategoryType) optionIdentifier {
} }
} }
func scopeToOptionIdentifier(selector selectors.ExchangeScope) optionIdentifier {
switch selector.Category() {
case selectors.ExchangeMailFolder, selectors.ExchangeMail:
return messages
case selectors.ExchangeContactFolder, selectors.ExchangeContact:
return contacts
case selectors.ExchangeEventCalendar, selectors.ExchangeEvent:
return events
default:
return unknown
}
}
//--------------------------------------------------------------------------- //---------------------------------------------------------------------------
// exchange.Query Option Section // exchange.Query Option Section
// These functions can be used to filter a response on M365 // These functions can be used to filter a response on M365

View File

@ -5,7 +5,6 @@ import (
"fmt" "fmt"
"github.com/hashicorp/go-multierror" "github.com/hashicorp/go-multierror"
absser "github.com/microsoft/kiota-abstractions-go/serialization"
msgraphsdk "github.com/microsoftgraph/msgraph-sdk-go" msgraphsdk "github.com/microsoftgraph/msgraph-sdk-go"
msgraphgocore "github.com/microsoftgraph/msgraph-sdk-go-core" msgraphgocore "github.com/microsoftgraph/msgraph-sdk-go-core"
"github.com/microsoftgraph/msgraph-sdk-go/models" "github.com/microsoftgraph/msgraph-sdk-go/models"
@ -144,18 +143,18 @@ func GetAllMailFolders(
) ([]graph.CachedContainer, error) { ) ([]graph.CachedContainer, error) {
containers := make([]graph.CachedContainer, 0) containers := make([]graph.CachedContainer, 0)
resolver, err := MaybeGetAndPopulateFolderResolver(ctx, qp, path.EmailCategory) resolver, err := PopulateExchangeContainerResolver(ctx, qp, path.EmailCategory)
if err != nil { if err != nil {
return nil, err return nil, err
} }
for _, c := range resolver.Items() { for _, c := range resolver.Items() {
directories := c.Path().Elements() directory := c.Path().String()
if len(directories) == 0 { if len(directory) == 0 {
continue continue
} }
if qp.Scope.Matches(selectors.ExchangeMailFolder, directories[len(directories)-1]) { if qp.Scope.Matches(selectors.ExchangeMailFolder, directory) {
containers = append(containers, c) containers = append(containers, c)
} }
} }
@ -173,15 +172,15 @@ func GetAllCalendars(
) ([]graph.CachedContainer, error) { ) ([]graph.CachedContainer, error) {
containers := make([]graph.CachedContainer, 0) containers := make([]graph.CachedContainer, 0)
resolver, err := MaybeGetAndPopulateFolderResolver(ctx, qp, path.EventsCategory) resolver, err := PopulateExchangeContainerResolver(ctx, qp, path.EventsCategory)
if err != nil { if err != nil {
return nil, err return nil, err
} }
for _, c := range resolver.Items() { for _, c := range resolver.Items() {
directories := c.Path().Elements() directory := c.Path().String()
if qp.Scope.Matches(selectors.ExchangeEventCalendar, directories[len(directories)-1]) { if qp.Scope.Matches(selectors.ExchangeEventCalendar, directory) {
containers = append(containers, c) containers = append(containers, c)
} }
} }
@ -198,23 +197,22 @@ func GetAllContactFolders(
qp graph.QueryParams, qp graph.QueryParams,
gs graph.Service, gs graph.Service,
) ([]graph.CachedContainer, error) { ) ([]graph.CachedContainer, error) {
var ( var query string
query string
containers = make([]graph.CachedContainer, 0)
)
resolver, err := MaybeGetAndPopulateFolderResolver(ctx, qp, path.ContactsCategory) containers := make([]graph.CachedContainer, 0)
resolver, err := PopulateExchangeContainerResolver(ctx, qp, path.ContactsCategory)
if err != nil { if err != nil {
return nil, err return nil, err
} }
for _, c := range resolver.Items() { for _, c := range resolver.Items() {
directories := c.Path().Elements() directory := c.Path().String()
if len(directories) == 0 { if len(directory) == 0 {
query = DefaultContactFolder query = DefaultContactFolder
} else { } else {
query = directories[len(directories)-1] query = directory
} }
if qp.Scope.Matches(selectors.ExchangeContactFolder, query) { if qp.Scope.Matches(selectors.ExchangeContactFolder, query) {
@ -225,42 +223,30 @@ func GetAllContactFolders(
return containers, err return containers, err
} }
// SetupExchangeCollectionVars is a helper function returns a sets func GetContainers(
// Exchange.Type specific functions based on scope. ctx context.Context,
// The []GraphQuery slice provides fallback queries in the event that qp graph.QueryParams,
// initial queries provide zero results. gs graph.Service,
func SetupExchangeCollectionVars(scope selectors.ExchangeScope) ( ) ([]graph.CachedContainer, error) {
absser.ParsableFactory, category := graph.ScopeToPathCategory(qp.Scope)
[]GraphQuery,
GraphIterateFunc,
error,
) {
if scope.IncludesCategory(selectors.ExchangeMail) {
return nil, nil, nil, errors.New("mail no longer supported this way")
}
if scope.IncludesCategory(selectors.ExchangeContact) { switch category {
return models.CreateContactFolderCollectionResponseFromDiscriminatorValue, case path.ContactsCategory:
[]GraphQuery{GetAllContactFolderNamesForUser, GetDefaultContactFolderForUser}, return GetAllContactFolders(ctx, qp, gs)
IterateSelectAllContactsForCollections, case path.EmailCategory:
nil return GetAllMailFolders(ctx, qp, gs)
case path.EventsCategory:
return GetAllCalendars(ctx, qp, gs)
default:
return nil, fmt.Errorf("path.Category %s not supported", category)
} }
if scope.IncludesCategory(selectors.ExchangeEvent) {
return models.CreateCalendarCollectionResponseFromDiscriminatorValue,
[]GraphQuery{GetAllCalendarNamesForUser},
IterateSelectAllEventsFromCalendars,
nil
}
return nil, nil, nil, errors.New("exchange scope option not supported")
} }
// MaybeGetAndPopulateFolderResolver gets a folder resolver if one is available for // PopulateExchangeContainerResolver gets a folder resolver if one is available for
// this category of data. If one is not available, returns nil so that other // this category of data. If one is not available, returns nil so that other
// logic in the caller can complete as long as they check if the resolver is not // logic in the caller can complete as long as they check if the resolver is not
// nil. If an error occurs populating the resolver, returns an error. // nil. If an error occurs populating the resolver, returns an error.
func MaybeGetAndPopulateFolderResolver( func PopulateExchangeContainerResolver(
ctx context.Context, ctx context.Context,
qp graph.QueryParams, qp graph.QueryParams,
category path.CategoryType, category path.CategoryType,
@ -298,7 +284,7 @@ func MaybeGetAndPopulateFolderResolver(
cacheRoot = DefaultCalendar cacheRoot = DefaultCalendar
default: default:
return nil, nil return nil, fmt.Errorf("ContainerResolver not present for %s type", category)
} }
if err := res.Populate(ctx, cacheRoot); err != nil { if err := res.Populate(ctx, cacheRoot); err != nil {
@ -308,70 +294,43 @@ func MaybeGetAndPopulateFolderResolver(
return res, nil return res, nil
} }
func resolveCollectionPath( func pathAndMatch(qp graph.QueryParams, category path.CategoryType, c graph.CachedContainer) (path.Path, bool) {
ctx context.Context, var (
resolver graph.ContainerResolver, directory string
tenantID, user, folderID string, pb = c.Path()
category path.CategoryType, )
) (path.Path, error) {
if resolver == nil { // Clause ensures that DefaultContactFolder is inspected properly
// Allows caller to default to old-style path. if category == path.ContactsCategory && *c.GetDisplayName() == DefaultContactFolder {
return nil, errors.WithStack(errNilResolver) pb = c.Path().Append(DefaultContactFolder)
} }
p, err := resolver.IDToPath(ctx, folderID) dirPath, err := pb.ToDataLayerExchangePathForCategory(
if err != nil { qp.Credentials.AzureTenantID,
return nil, errors.Wrap(err, "resolving folder ID") qp.User,
}
return p.ToDataLayerExchangePathForCategory(
tenantID,
user,
category, category,
false, false,
) )
} if err != nil {
return nil, false
func getCollectionPath(
ctx context.Context,
qp graph.QueryParams,
resolver graph.ContainerResolver,
directory string,
category path.CategoryType,
) (path.Path, error) {
returnPath, err := resolveCollectionPath(
ctx,
resolver,
qp.Credentials.AzureTenantID,
qp.User,
directory,
category,
)
if err == nil {
return returnPath, nil
} }
aPath, err1 := path.Builder{}.Append(directory). if dirPath == nil && category == path.EmailCategory {
ToDataLayerExchangePathForCategory( return nil, false // Only true for root mail folder
qp.Credentials.AzureTenantID,
qp.User,
category,
false,
)
if err1 == nil {
return aPath, nil
} }
return nil, directory = pb.String()
support.WrapAndAppend(
fmt.Sprintf( switch category {
"both path generate functions failed for %s:%s:%s", case path.EmailCategory:
qp.User, return dirPath, qp.Scope.Matches(selectors.ExchangeMailFolder, directory)
category, case path.ContactsCategory:
directory), return dirPath, qp.Scope.Matches(selectors.ExchangeContactFolder, directory)
err, case path.EventsCategory:
err1, return dirPath, qp.Scope.Matches(selectors.ExchangeEventCalendar, directory)
) default:
return nil, false
}
} }
func AddItemsToCollection( func AddItemsToCollection(

View File

@ -301,6 +301,7 @@ func (suite *ServiceFunctionsIntegrationSuite) TestCollectContainers() {
t := suite.T() t := suite.T()
user := tester.M365UserID(t) user := tester.M365UserID(t)
a := tester.NewM365Account(t) a := tester.NewM365Account(t)
service := loadService(t)
credentials, err := a.M365Config() credentials, err := a.M365Config()
require.NoError(t, err) require.NoError(t, err)
@ -314,39 +315,27 @@ func (suite *ServiceFunctionsIntegrationSuite) TestCollectContainers() {
contains: "Birthdays", contains: "Birthdays",
expectedCount: assert.Greater, expectedCount: assert.Greater,
getScope: func() selectors.ExchangeScope { getScope: func() selectors.ExchangeScope {
sel := selectors.NewExchangeBackup() return selectors.
sel.Include(sel.EventCalendars([]string{user}, selectors.Any())) NewExchangeBackup().
EventCalendars([]string{user}, selectors.Any())[0]
scopes := sel.Scopes()
assert.Equal(t, len(scopes), 1)
return scopes[0]
}, },
}, { }, {
name: "Default Calendar", name: "Default Calendar",
contains: DefaultCalendar, contains: DefaultCalendar,
expectedCount: assert.Equal, expectedCount: assert.Equal,
getScope: func() selectors.ExchangeScope { getScope: func() selectors.ExchangeScope {
sel := selectors.NewExchangeBackup() return selectors.
sel.Include(sel.EventCalendars([]string{user}, []string{DefaultCalendar})) NewExchangeBackup().
EventCalendars([]string{user}, []string{DefaultCalendar})[0]
scopes := sel.Scopes()
assert.Equal(t, len(scopes), 1)
return scopes[0]
}, },
}, { }, {
name: "Default Mail", name: "Default Mail",
contains: DefaultMailFolder, contains: DefaultMailFolder,
expectedCount: assert.Equal, expectedCount: assert.Equal,
getScope: func() selectors.ExchangeScope { getScope: func() selectors.ExchangeScope {
sel := selectors.NewExchangeBackup() return selectors.
sel.Include(sel.MailFolders([]string{user}, []string{DefaultMailFolder})) NewExchangeBackup().
MailFolders([]string{user}, []string{DefaultMailFolder})[0]
scopes := sel.Scopes()
assert.Equal(t, len(scopes), 1)
return scopes[0]
}, },
}, },
} }
@ -359,16 +348,14 @@ func (suite *ServiceFunctionsIntegrationSuite) TestCollectContainers() {
FailFast: failFast, FailFast: failFast,
Credentials: credentials, Credentials: credentials,
} }
collections := make(map[string]*Collection) collections, err := GetContainers(ctx, qp, service)
err := CollectFolders(ctx, qp, collections, nil, nil)
assert.NoError(t, err) assert.NoError(t, err)
test.expectedCount(t, len(collections), containerCount) test.expectedCount(t, len(collections), containerCount)
keys := make([]string, 0, len(collections)) keys := make([]string, 0, len(collections))
for k := range collections { for _, k := range collections {
keys = append(keys, k) keys = append(keys, *k.GetDisplayName())
} }
t.Logf("Collections Made: %v\n", keys)
assert.Contains(t, keys, test.contains) assert.Contains(t, keys, test.contains)
}) })
} }

View File

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"strings" "strings"
multierror "github.com/hashicorp/go-multierror"
msgraphgocore "github.com/microsoftgraph/msgraph-sdk-go-core" msgraphgocore "github.com/microsoftgraph/msgraph-sdk-go-core"
"github.com/microsoftgraph/msgraph-sdk-go/models" "github.com/microsoftgraph/msgraph-sdk-go/models"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -12,436 +13,81 @@ import (
"github.com/alcionai/corso/src/internal/connector/graph" "github.com/alcionai/corso/src/internal/connector/graph"
"github.com/alcionai/corso/src/internal/connector/support" "github.com/alcionai/corso/src/internal/connector/support"
"github.com/alcionai/corso/src/pkg/path" "github.com/alcionai/corso/src/pkg/path"
"github.com/alcionai/corso/src/pkg/selectors"
) )
var errNilResolver = errors.New("nil resolver") // FilterContainersAndFillCollections is a utility function
// that places the M365 object ids belonging to specific directories
// GraphIterateFuncs are iterate functions to be used with the M365 iterators (e.g. msgraphgocore.NewPageIterator) // into a Collection. Messages outside of those directories are omitted.
// @returns a callback func that works with msgraphgocore.PageIterator.Iterate function // @param collection is filled with during this function.
type GraphIterateFunc func( // Supports all exchange applications: Contacts, Events, and Mail
func FilterContainersAndFillCollections(
ctx context.Context, ctx context.Context,
qp graph.QueryParams, qp graph.QueryParams,
errUpdater func(string, error),
collections map[string]*Collection, collections map[string]*Collection,
statusUpdater support.StatusUpdater, statusUpdater support.StatusUpdater,
resolver graph.ContainerResolver, resolver graph.ContainerResolver,
) func(any) bool ) error {
// IterateSelectAllEventsForCollections
// utility function for iterating through events
// and storing events in collections based on
// the calendarID which originates from M365.
// @param pageItem is a CalendarCollectionResponse possessing two populated fields:
// - id - M365 ID
// - Name - Calendar Name
func IterateSelectAllEventsFromCalendars(
ctx context.Context,
qp graph.QueryParams,
errUpdater func(string, error),
collections map[string]*Collection,
statusUpdater support.StatusUpdater,
resolver graph.ContainerResolver,
) func(any) bool {
var ( var (
isEnabled bool category = graph.ScopeToPathCategory(qp.Scope)
service graph.Service collectionType = categoryToOptionIdentifier(category)
errs error
) )
return func(pageItem any) bool { for _, c := range resolver.Items() {
if !isEnabled { dirPath, ok := pathAndMatch(qp, category, c)
// Create Collections based on qp.Scope if ok {
err := CollectFolders(ctx, qp, collections, statusUpdater, resolver) // Create only those that match
service, err := createService(qp.Credentials, qp.FailFast)
if err != nil { if err != nil {
errUpdater( errs = support.WrapAndAppend(
qp.User, qp.User+" failed to create service during FilterContainerAndFillCollection",
errors.Wrap(err, support.ConnectorStackErrorTrace(err)), err,
) errs)
return false if qp.FailFast {
return errs
}
} }
service, err = createService(qp.Credentials, qp.FailFast) edc := NewCollection(
if err != nil {
errUpdater(qp.User, err)
return false
}
isEnabled = true
}
pageItem = CreateCalendarDisplayable(pageItem)
calendar, ok := pageItem.(graph.Displayable)
if !ok {
errUpdater(
qp.User, qp.User,
fmt.Errorf("unable to parse pageItem into CalendarDisplayable: %T", pageItem), dirPath,
collectionType,
service,
statusUpdater,
) )
collections[*c.GetId()] = &edc
} }
if calendar.GetDisplayName() == nil {
return true
}
collection, ok := collections[*calendar.GetDisplayName()]
if !ok {
return true
}
eventIDs, err := ReturnEventIDsFromCalendar(ctx, service, qp.User, *calendar.GetId())
if err != nil {
errUpdater(
qp.User,
errors.Wrap(err, support.ConnectorStackErrorTrace(err)))
return true
}
collection.jobs = append(collection.jobs, eventIDs...)
return true
} }
}
// CollectionsFromResolver returns the set of collections that match the for directoryID, col := range collections {
// selector parameters. fetchFunc, err := getFetchIDFunc(category)
func CollectionsFromResolver( if err != nil {
ctx context.Context, errs = support.WrapAndAppend(
qp graph.QueryParams, qp.User,
resolver graph.ContainerResolver, err,
statusUpdater support.StatusUpdater, errs)
collections map[string]*Collection,
) error { if qp.FailFast {
option, category, notMatcher := getCategoryAndValidation(qp.Scope) return errs
}
for _, item := range resolver.Items() {
pathString := item.Path().String()
// Skip the root folder for mail which has an empty path.
if len(pathString) == 0 || notMatcher(&pathString) {
continue continue
} }
completePath, err := item.Path().ToDataLayerExchangePathForCategory( jobs, err := fetchFunc(ctx, col.service, qp.User, directoryID)
qp.Credentials.AzureTenantID,
qp.User,
category,
false,
)
if err != nil { if err != nil {
return errors.Wrap(err, "resolving collection item path") errs = support.WrapAndAppend(
}
service, err := createService(qp.Credentials, qp.FailFast)
if err != nil {
return errors.Wrap(err, "making service instance")
}
tmp := NewCollection(
qp.User,
completePath,
option,
service,
statusUpdater,
)
collections[*item.GetId()] = &tmp
}
return nil
}
func getCategoryAndValidation(es selectors.ExchangeScope) (
optionIdentifier,
path.CategoryType,
func(namePtr *string) bool,
) {
var (
option = scopeToOptionIdentifier(es)
category path.CategoryType
validate func(namePtr *string) bool
)
switch option {
case messages:
category = path.EmailCategory
validate = func(namePtr *string) bool {
if namePtr == nil {
return true
}
return !es.Matches(selectors.ExchangeMailFolder, *namePtr)
}
case contacts:
category = path.ContactsCategory
validate = func(namePtr *string) bool {
if namePtr == nil {
return true
}
return !es.Matches(selectors.ExchangeContactFolder, *namePtr)
}
case events:
category = path.EventsCategory
validate = func(namePtr *string) bool {
if namePtr == nil {
return true
}
return !es.Matches(selectors.ExchangeEventCalendar, *namePtr)
}
}
return option, category, validate
}
func IterateFilterContainersForCollections(
ctx context.Context,
qp graph.QueryParams,
errUpdater func(string, error),
collections map[string]*Collection,
statusUpdater support.StatusUpdater,
resolver graph.ContainerResolver,
) func(any) bool {
var (
isSet bool
collectPath string
option optionIdentifier
category path.CategoryType
validate func(*string) bool
)
return func(folderItem any) bool {
if !isSet {
option, category, validate = getCategoryAndValidation(qp.Scope)
isSet = true
}
if option == events {
folderItem = CreateCalendarDisplayable(folderItem)
}
folder, ok := folderItem.(graph.Displayable)
if !ok {
errUpdater(qp.User,
fmt.Errorf("unable to convert input of %T for category: %s", folderItem, category.String()),
)
return true
}
if validate(folder.GetDisplayName()) {
return true
}
if option == messages {
collectPath = *folder.GetId()
} else {
collectPath = *folder.GetDisplayName()
}
dirPath, err := getCollectionPath(
ctx,
qp,
resolver,
collectPath,
category,
)
if err != nil {
errUpdater(
"failure converting path during IterateFilterFolderDirectoriesForCollections",
err,
)
return true
}
service, err := createService(qp.Credentials, qp.FailFast)
if err != nil {
errUpdater(
*folder.GetDisplayName(),
errors.Wrap(err, "creating service to iterate filterFolder directories for user: "+qp.User))
return true
}
temp := NewCollection(
qp.User,
dirPath,
option,
service,
statusUpdater,
)
collections[*folder.GetDisplayName()] = &temp
return true
}
}
func IterateSelectAllContactsForCollections(
ctx context.Context,
qp graph.QueryParams,
errUpdater func(string, error),
collections map[string]*Collection,
statusUpdater support.StatusUpdater,
resolver graph.ContainerResolver,
) func(any) bool {
var (
isPrimarySet bool
service graph.Service
)
return func(folderItem any) bool {
folder, ok := folderItem.(models.ContactFolderable)
if !ok {
errUpdater(
qp.User,
errors.New("casting folderItem to models.ContactFolderable"),
)
}
if !isPrimarySet && folder.GetParentFolderId() != nil {
err := CollectFolders(
ctx,
qp,
collections,
statusUpdater,
resolver,
)
if err != nil {
errUpdater(qp.User, err)
return false
}
service, err = createService(qp.Credentials, qp.FailFast)
if err != nil {
errUpdater(
qp.User,
errors.Wrap(err, "unable to create service during IterateSelectAllContactsForCollections"),
)
return true
}
isPrimarySet = true
// Create and Populate Default Contacts folder Collection if true
if qp.Scope.Matches(selectors.ExchangeContactFolder, DefaultContactFolder) {
dirPath, err := path.Builder{}.Append(DefaultContactFolder).ToDataLayerExchangePathForCategory(
qp.Credentials.AzureTenantID,
qp.User,
path.ContactsCategory,
false,
)
if err != nil {
errUpdater(
qp.User,
err,
)
return false
}
edc := NewCollection(
qp.User,
dirPath,
contacts,
service,
statusUpdater,
)
listOfIDs, err := ReturnContactIDsFromDirectory(ctx, service, qp.User, *folder.GetParentFolderId())
if err != nil {
errUpdater(
qp.User,
err,
)
return false
}
edc.jobs = append(edc.jobs, listOfIDs...)
collections[DefaultContactFolder] = &edc
}
}
if folder.GetDisplayName() == nil {
// This should never happen. Skipping to avoid kernel panic
return true
}
collection, ok := collections[*folder.GetDisplayName()]
if !ok {
return true // Not included
}
listOfIDs, err := ReturnContactIDsFromDirectory(ctx, service, qp.User, *folder.GetId())
if err != nil {
errUpdater(
qp.User, qp.User,
err, err,
errs,
) )
return true
} }
collection.jobs = append(collection.jobs, listOfIDs...) col.jobs = append(col.jobs, jobs...)
return true
}
}
// IDistFunc collection of helper functions which return a list of strings
// from a response.
type IDListFunc func(ctx context.Context, gs graph.Service, user, m365ID string) ([]string, error)
// ReturnContactIDsFromDirectory function that returns a list of all the m365IDs of the contacts
// of the targeted directory
func ReturnContactIDsFromDirectory(ctx context.Context, gs graph.Service, user, directoryID string) ([]string, error) {
options, err := optionsForContactFoldersItem([]string{"parentFolderId"})
if err != nil {
return nil, err
} }
stringArray := []string{} return errs
response, err := gs.Client().
UsersById(user).
ContactFoldersById(directoryID).
Contacts().
Get(ctx, options)
if err != nil {
return nil, err
}
pageIterator, err := msgraphgocore.NewPageIterator(
response,
gs.Adapter(),
models.CreateContactCollectionResponseFromDiscriminatorValue,
)
callbackFunc := func(pageItem any) bool {
entry, ok := pageItem.(models.Contactable)
if !ok {
err = errors.New("casting pageItem to models.Contactable")
return false
}
stringArray = append(stringArray, *entry.GetId())
return true
}
if iterateErr := pageIterator.Iterate(ctx, callbackFunc); iterateErr != nil {
return nil, iterateErr
}
if err != nil {
return nil, err
}
return stringArray, nil
} }
func IterativeCollectContactContainers( func IterativeCollectContactContainers(
@ -491,8 +137,25 @@ func IterativeCollectCalendarContainers(
} }
} }
// ReturnEventIDsFromCalendar returns a list of all M365IDs of events of the targeted Calendar. // FetchIDFunc collection of helper functions which return a list of strings
func ReturnEventIDsFromCalendar( // from a response.
type FetchIDFunc func(ctx context.Context, gs graph.Service, user, containerID string) ([]string, error)
func getFetchIDFunc(category path.CategoryType) (FetchIDFunc, error) {
switch category {
case path.EmailCategory:
return FetchMessageIDsFromDirectory, nil
case path.EventsCategory:
return FetchEventIDsFromCalendar, nil
case path.ContactsCategory:
return FetchContactIDsFromDirectory, nil
default:
return nil, fmt.Errorf("category %s not supported by getFetchIDFunc", category)
}
}
// FetchEventIDsFromCalendar returns a list of all M365IDs of events of the targeted Calendar.
func FetchEventIDsFromCalendar(
ctx context.Context, ctx context.Context,
gs graph.Service, gs graph.Service,
user, calendarID string, user, calendarID string,
@ -504,11 +167,7 @@ func ReturnEventIDsFromCalendar(
CalendarsById(calendarID). CalendarsById(calendarID).
Events().Get(ctx, nil) Events().Get(ctx, nil)
if err != nil { if err != nil {
return nil, err return nil, errors.Wrap(err, support.ConnectorStackErrorTrace(err))
}
if err != nil {
return nil, err
} }
pageIterator, err := msgraphgocore.NewPageIterator( pageIterator, err := msgraphgocore.NewPageIterator(
@ -516,27 +175,158 @@ func ReturnEventIDsFromCalendar(
gs.Adapter(), gs.Adapter(),
models.CreateEventCollectionResponseFromDiscriminatorValue, models.CreateEventCollectionResponseFromDiscriminatorValue,
) )
if err != nil {
return nil, errors.Wrap(err, "iterator creation failure during fetchEventIDs")
}
callbackFunc := func(pageItem any) bool { var errs *multierror.Error
entry, ok := pageItem.(models.Eventable)
err = pageIterator.Iterate(ctx, func(pageItem any) bool {
entry, ok := pageItem.(graph.Idable)
if !ok { if !ok {
err = errors.New("casting pageItem to models.Eventable") errs = multierror.Append(errs, errors.New("item without GetId() call"))
return false return true
}
if entry.GetId() == nil {
errs = multierror.Append(errs, errors.New("item with nil ID"))
} }
ids = append(ids, *entry.GetId()) ids = append(ids, *entry.GetId())
return true return true
})
if err != nil {
return nil, errors.Wrap(
err,
support.ConnectorStackErrorTrace(err)+
" :iterateFailure for fetching events from calendar "+calendarID,
)
} }
if iterateErr := pageIterator.Iterate(ctx, callbackFunc); iterateErr != nil { return ids, errs.ErrorOrNil()
return nil, }
errors.Wrap(iterateErr, support.ConnectorStackErrorTrace(err))
}
// FetchContactIDsFromDirectory function that returns a list of all the m365IDs of the contacts
// of the targeted directory
func FetchContactIDsFromDirectory(ctx context.Context, gs graph.Service, user, directoryID string) ([]string, error) {
options, err := optionsForContactFoldersItem([]string{"parentFolderId"})
if err != nil { if err != nil {
return nil, err return nil, err
} }
return ids, nil ids := []string{}
response, err := gs.Client().
UsersById(user).
ContactFoldersById(directoryID).
Contacts().
Get(ctx, options)
if err != nil {
return nil, errors.Wrap(err, support.ConnectorStackErrorTrace(err))
}
pageIterator, err := msgraphgocore.NewPageIterator(
response,
gs.Adapter(),
models.CreateContactCollectionResponseFromDiscriminatorValue,
)
if err != nil {
return nil, errors.Wrap(err, "failure to create iterator during FecthContactIDs")
}
var errs *multierror.Error
err = pageIterator.Iterate(ctx, func(pageItem any) bool {
entry, ok := pageItem.(graph.Idable)
if !ok {
errs = multierror.Append(
errs,
errors.New("casting pageItem to models.Contactable"),
)
return true
}
ids = append(ids, *entry.GetId())
return true
})
if err != nil {
return nil,
errors.Wrap(
err,
support.ConnectorStackErrorTrace(err)+
" :iterate failure during fetching contactIDs from directory "+directoryID,
)
}
return ids, errs.ErrorOrNil()
}
// FetchMessageIDsFromDirectory function that returns a list of all the m365IDs of the exchange.Mail
// of the targeted directory
func FetchMessageIDsFromDirectory(
ctx context.Context,
gs graph.Service,
user, directoryID string,
) ([]string, error) {
ids := []string{}
options, err := optionsForFolderMessages([]string{"id"})
if err != nil {
return nil, errors.Wrap(err, "getting query options")
}
response, err := gs.Client().
UsersById(user).
MailFoldersById(directoryID).
Messages().
Get(ctx, options)
if err != nil {
return nil, errors.Wrap(
errors.Wrap(err, support.ConnectorStackErrorTrace(err)),
"initial folder query",
)
}
pageIter, err := msgraphgocore.NewPageIterator(
response,
gs.Adapter(),
models.CreateMessageCollectionResponseFromDiscriminatorValue,
)
if err != nil {
return nil, errors.Wrap(err, "creating graph iterator")
}
var errs *multierror.Error
err = pageIter.Iterate(ctx, func(pageItem any) bool {
item, ok := pageItem.(graph.Idable)
if !ok {
errs = multierror.Append(errs, errors.New("item without ID function"))
return true
}
if item.GetId() == nil {
errs = multierror.Append(errs, errors.New("item with nil ID"))
return true
}
ids = append(ids, *item.GetId())
return true
})
if err != nil {
return nil, errors.Wrap(
err,
support.ConnectorStackErrorTrace(err)+
" :iterateFailure for fetching messages from directory "+directoryID,
)
}
return ids, errs.ErrorOrNil()
} }

View File

@ -2,15 +2,10 @@ package exchange
import ( import (
"context" "context"
"fmt"
absser "github.com/microsoft/kiota-abstractions-go/serialization" absser "github.com/microsoft/kiota-abstractions-go/serialization"
msgraphgocore "github.com/microsoftgraph/msgraph-sdk-go-core"
"github.com/microsoftgraph/msgraph-sdk-go/models"
"github.com/pkg/errors"
"github.com/alcionai/corso/src/internal/connector/graph" "github.com/alcionai/corso/src/internal/connector/graph"
"github.com/alcionai/corso/src/internal/connector/support"
) )
// GraphQuery represents functions which perform exchange-specific queries // GraphQuery represents functions which perform exchange-specific queries
@ -125,84 +120,3 @@ func RetrieveEventDataForUser(ctx context.Context, gs graph.Service, user, m365I
func RetrieveMessageDataForUser(ctx context.Context, gs graph.Service, user, m365ID string) (absser.Parsable, error) { func RetrieveMessageDataForUser(ctx context.Context, gs graph.Service, user, m365ID string) (absser.Parsable, error) {
return gs.Client().UsersById(user).MessagesById(m365ID).Get(ctx, nil) return gs.Client().UsersById(user).MessagesById(m365ID).Get(ctx, nil)
} }
// CollectFolders is a utility function for creating Collections based off parameters found
// in the ExchangeScope found in the graph.QueryParams
// TODO(ashmrtn): This may not need to do the query if we decide the cache
// should always:
// 1. be passed in
// 2. be populated with all folders for the user
func CollectFolders(
ctx context.Context,
qp graph.QueryParams,
collections map[string]*Collection,
statusUpdater support.StatusUpdater,
resolver graph.ContainerResolver,
) error {
var (
query GraphQuery
transformer absser.ParsableFactory
queryService, err = createService(qp.Credentials, qp.FailFast)
)
if err != nil {
return errors.Wrapf(
err,
"unable to create graph.Service within CollectFolders service for "+qp.User,
)
}
option := scopeToOptionIdentifier(qp.Scope)
switch option {
case messages:
query = GetAllFolderNamesForUser
transformer = models.CreateMailFolderCollectionResponseFromDiscriminatorValue
case contacts:
query = GetAllContactFolderNamesForUser
transformer = models.CreateContactFolderCollectionResponseFromDiscriminatorValue
case events:
query = GetAllCalendarNamesForUser
transformer = models.CreateCalendarCollectionResponseFromDiscriminatorValue
default:
return fmt.Errorf("unsupported option %s used in CollectFolders", option)
}
response, err := query(ctx, queryService, qp.User)
if err != nil {
return fmt.Errorf(
"unable to query mail folder for %s: details: %s",
qp.User,
support.ConnectorStackErrorTrace(err),
)
}
// Iterator required to ensure all potential folders are inspected
// when the breadth of the folder space is large
pageIterator, err := msgraphgocore.NewPageIterator(
response,
&queryService.adapter,
transformer)
if err != nil {
return errors.Wrap(err, "unable to create iterator during mail folder query service")
}
errUpdater := func(id string, e error) {
err = support.WrapAndAppend(id, e, err)
}
callbackFunc := IterateFilterContainersForCollections(
ctx,
qp,
errUpdater,
collections,
statusUpdater,
resolver,
)
iterateFailure := pageIterator.Iterate(ctx, callbackFunc)
if iterateFailure != nil {
err = support.WrapAndAppend(qp.User+" iterate failure", iterateFailure, err)
}
return err
}

View File

@ -520,8 +520,8 @@ func establishMailRestoreLocation(
} }
// establishContactsRestoreLocation creates Contact Folders in sequence // establishContactsRestoreLocation creates Contact Folders in sequence
// and updates the container resolver appropriately. Contact Folders // and updates the container resolver appropriately. Contact Folders are
// are displayed in a flat representation. Therefore, only the root can be populated and all content // displayed in a flat representation. Therefore, only the root can be populated and all content
// must be restored into the root location. // must be restored into the root location.
// @param folders is the list of intended folders from root to leaf (e.g. [root ...]) // @param folders is the list of intended folders from root to leaf (e.g. [root ...])
// @param isNewCache bool representation of whether Populate function needs to be run // @param isNewCache bool representation of whether Populate function needs to be run

View File

@ -0,0 +1,109 @@
package graph
import (
"github.com/microsoftgraph/msgraph-sdk-go/models"
"github.com/pkg/errors"
"github.com/alcionai/corso/src/pkg/path"
)
// CachedContainer is used for local unit tests but also makes it so that this
// code can be broken into generic- and service-specific chunks later on to
// reuse logic in IDToPath.
type CachedContainer interface {
Container
Path() *path.Builder
SetPath(*path.Builder)
}
// checkRequiredValues is a helper function to ensure that
// all the pointers are set prior to being called.
func CheckRequiredValues(c Container) error {
idPtr := c.GetId()
if idPtr == nil || len(*idPtr) == 0 {
return errors.New("folder without ID")
}
ptr := c.GetDisplayName()
if ptr == nil || len(*ptr) == 0 {
return errors.Errorf("folder %s without display name", *idPtr)
}
ptr = c.GetParentFolderId()
if ptr == nil || len(*ptr) == 0 {
return errors.Errorf("folder %s without parent ID", *idPtr)
}
return nil
}
//======================================
// cachedContainer Implementations
//======================================
var _ CachedContainer = &CacheFolder{}
type CacheFolder struct {
Container
p *path.Builder
}
// NewCacheFolder public constructor for struct
func NewCacheFolder(c Container, pb *path.Builder) CacheFolder {
cf := CacheFolder{
Container: c,
p: pb,
}
return cf
}
//=========================================
// Required Functions to satisfy interfaces
//=========================================
func (cf CacheFolder) Path() *path.Builder {
return cf.p
}
func (cf *CacheFolder) SetPath(newPath *path.Builder) {
cf.p = newPath
}
// CalendarDisplayable is a transformative struct that aligns
// models.Calendarable interface with the container interface.
// Calendars do not have the 2 of the
type CalendarDisplayable struct {
models.Calendarable
parentID string
}
// GetDisplayName returns the *string of the calendar name
func (c CalendarDisplayable) GetDisplayName() *string {
return c.GetName()
}
// GetParentFolderId returns the default calendar name address
// EventCalendars have a flat hierarchy and Calendars are rooted
// at the default
//nolint:revive
func (c CalendarDisplayable) GetParentFolderId() *string {
return &c.parentID
}
// CreateCalendarDisplayable helper function to create the
// calendarDisplayable during msgraph-sdk-go iterative process
// @param entry is the input supplied by pageIterator.Iterate()
// @param parentID of Calendar sets. Only populate when used with
// EventCalendarCache
func CreateCalendarDisplayable(entry any, parentID string) *CalendarDisplayable {
calendar, ok := entry.(models.Calendarable)
if !ok {
return nil
}
return &CalendarDisplayable{
Calendarable: calendar,
parentID: parentID,
}
}

View File

@ -53,15 +53,6 @@ type Container interface {
Displayable Displayable
} }
// CachedContainer is used for local unit tests but also makes it so that this
// code can be broken into generic- and service-specific chunks later on to
// reuse logic in IDToPath.
type CachedContainer interface {
Container
Path() *path.Builder
SetPath(*path.Builder)
}
// ContainerResolver houses functions for getting information about containers // ContainerResolver houses functions for getting information about containers
// from remote APIs (i.e. resolve folder paths with Graph API). Resolvers may // from remote APIs (i.e. resolve folder paths with Graph API). Resolvers may
// cache information about containers. // cache information about containers.
@ -83,6 +74,7 @@ type ContainerResolver interface {
PathInCache(pathString string) (string, bool) PathInCache(pathString string) (string, bool)
AddToCache(ctx context.Context, m365Container Container) error AddToCache(ctx context.Context, m365Container Container) error
// Items returns the containers in the cache. // Items returns the containers in the cache.
Items() []CachedContainer Items() []CachedContainer
} }

View File

@ -23,7 +23,6 @@ import (
"github.com/alcionai/corso/src/pkg/backup/details" "github.com/alcionai/corso/src/pkg/backup/details"
"github.com/alcionai/corso/src/pkg/control" "github.com/alcionai/corso/src/pkg/control"
"github.com/alcionai/corso/src/pkg/logger" "github.com/alcionai/corso/src/pkg/logger"
"github.com/alcionai/corso/src/pkg/path"
"github.com/alcionai/corso/src/pkg/selectors" "github.com/alcionai/corso/src/pkg/selectors"
) )
@ -274,112 +273,7 @@ func (gc *GraphConnector) RestoreDataCollections(
return deets, err return deets, err
} }
func scopeToPathCategory(scope selectors.ExchangeScope) path.CategoryType { // createCollections - utility function that retrieves M365
if scope.IncludesCategory(selectors.ExchangeMail) {
return path.EmailCategory
}
if scope.IncludesCategory(selectors.ExchangeContact) {
return path.ContactsCategory
}
if scope.IncludesCategory(selectors.ExchangeEvent) {
return path.EventsCategory
}
return path.UnknownCategory
}
func (gc *GraphConnector) fetchItemsByFolder(
ctx context.Context,
qp graph.QueryParams,
resolver graph.ContainerResolver,
) (map[string]*exchange.Collection, error) {
var errs *multierror.Error
collections := map[string]*exchange.Collection{}
// This gets the collections, but does not get the items in the
// collection.
err := exchange.CollectionsFromResolver(
ctx,
qp,
resolver,
gc.UpdateStatus,
collections,
)
if err != nil {
return nil, errors.Wrap(err, "getting target collections")
}
for id, col := range collections {
// Fetch items for said collection.
err := exchange.AddItemsToCollection(ctx, gc.Service(), qp.User, id, col)
if err != nil {
errs = multierror.Append(errs, errors.Wrapf(
err,
"fetching items for collection %s with ID %s",
col.FullPath().String(),
id,
))
}
}
return collections, errs.ErrorOrNil()
}
func (gc *GraphConnector) legacyFetchItems(
ctx context.Context,
scope selectors.ExchangeScope,
qp graph.QueryParams,
resolver graph.ContainerResolver,
) (map[string]*exchange.Collection, error) {
var (
errs error
errUpdater = func(id string, err error) {
errs = support.WrapAndAppend(id, err, errs)
}
collections = map[string]*exchange.Collection{}
)
transformer, queries, gIter, err := exchange.SetupExchangeCollectionVars(scope)
if err != nil {
return nil, support.WrapAndAppend(gc.Service().Adapter().GetBaseUrl(), err, nil)
}
// queries is assumed to provide fallbacks in case of empty results. Any
// non-zero collection production will break out of the loop.
for _, query := range queries {
response, err := query(ctx, &gc.graphService, qp.User)
if err != nil {
return nil, errors.Wrapf(
err,
"user %s M365 query: %s",
qp.User, support.ConnectorStackErrorTrace(err))
}
pageIterator, err := msgraphgocore.NewPageIterator(response, &gc.graphService.adapter, transformer)
if err != nil {
return nil, err
}
// callbackFunc iterates through all M365 object target and fills exchange.Collection.jobs[]
// with corresponding item M365IDs. New collections are created for each directory.
// Each directory used the M365 Identifier. The use of ID stops collisions betweens users
callbackFunc := gIter(ctx, qp, errUpdater, collections, gc.UpdateStatus, resolver)
if err := pageIterator.Iterate(ctx, callbackFunc); err != nil {
return nil, support.WrapAndAppend(gc.graphService.adapter.GetBaseUrl(), err, errs)
}
if len(collections) > 0 {
break
}
}
return collections, errs
}
// createCollection - utility function that retrieves M365
// IDs through Microsoft Graph API. The selectors.ExchangeScope // IDs through Microsoft Graph API. The selectors.ExchangeScope
// determines the type of collections that are stored. // determines the type of collections that are stored.
// to the GraphConnector struct. // to the GraphConnector struct.
@ -393,7 +287,7 @@ func (gc *GraphConnector) createCollections(
allCollections := make([]*exchange.Collection, 0) allCollections := make([]*exchange.Collection, 0)
// Create collection of ExchangeDataCollection // Create collection of ExchangeDataCollection
for _, user := range users { for _, user := range users {
var collections map[string]*exchange.Collection collections := make(map[string]*exchange.Collection)
qp := graph.QueryParams{ qp := graph.QueryParams{
User: user, User: user,
@ -402,31 +296,24 @@ func (gc *GraphConnector) createCollections(
Credentials: gc.credentials, Credentials: gc.credentials,
} }
// Currently only mail has a folder cache implemented. resolver, err := exchange.PopulateExchangeContainerResolver(
resolver, err := exchange.MaybeGetAndPopulateFolderResolver(
ctx, ctx,
qp, qp,
scopeToPathCategory(scope), graph.ScopeToPathCategory(qp.Scope),
) )
if err != nil { if err != nil {
return nil, errors.Wrap(err, "getting folder cache") return nil, errors.Wrap(err, "getting folder cache")
} }
if scopeToPathCategory(scope) == path.EmailCategory { err = exchange.FilterContainersAndFillCollections(
if resolver == nil { ctx,
return nil, errors.New("unable to create mail folder resolver") qp,
} collections,
gc.UpdateStatus,
resolver)
collections, err = gc.fetchItemsByFolder(ctx, qp, resolver) if err != nil {
if err != nil { return nil, errors.Wrap(err, "filling collections")
errs = multierror.Append(errs, err)
}
} else {
collections, err = gc.legacyFetchItems(ctx, scope, qp, resolver)
// Preserving previous behavior.
if err != nil {
return nil, err // return error if snapshot is incomplete
}
} }
for _, collection := range collections { for _, collection := range collections {

View File

@ -10,7 +10,6 @@ import (
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"github.com/alcionai/corso/src/internal/connector/exchange" "github.com/alcionai/corso/src/internal/connector/exchange"
"github.com/alcionai/corso/src/internal/connector/graph"
"github.com/alcionai/corso/src/internal/connector/mockconnector" "github.com/alcionai/corso/src/internal/connector/mockconnector"
"github.com/alcionai/corso/src/internal/connector/support" "github.com/alcionai/corso/src/internal/connector/support"
"github.com/alcionai/corso/src/internal/data" "github.com/alcionai/corso/src/internal/data"
@ -193,9 +192,10 @@ func (suite *GraphConnectorIntegrationSuite) TestContactSerializationRegression(
{ {
name: "Default Contact Folder", name: "Default Contact Folder",
getCollection: func(t *testing.T) []*exchange.Collection { getCollection: func(t *testing.T) []*exchange.Collection {
sel := selectors.NewExchangeBackup() scope := selectors.
sel.Include(sel.ContactFolders([]string{suite.user}, []string{exchange.DefaultContactFolder})) NewExchangeBackup().
collections, err := connector.createCollections(ctx, sel.Scopes()[0]) ContactFolders([]string{suite.user}, []string{exchange.DefaultContactFolder})[0]
collections, err := connector.createCollections(ctx, scope)
require.NoError(t, err) require.NoError(t, err)
return collections return collections
@ -206,7 +206,7 @@ func (suite *GraphConnectorIntegrationSuite) TestContactSerializationRegression(
for _, test := range tests { for _, test := range tests {
suite.T().Run(test.name, func(t *testing.T) { suite.T().Run(test.name, func(t *testing.T) {
edcs := test.getCollection(t) edcs := test.getCollection(t)
assert.Equal(t, len(edcs), 1) require.Equal(t, len(edcs), 1)
edc := edcs[0] edc := edcs[0]
assert.Equal(t, edc.FullPath().Folder(), exchange.DefaultContactFolder) assert.Equal(t, edc.FullPath().Folder(), exchange.DefaultContactFolder)
streamChannel := edc.Items() streamChannel := edc.Items()
@ -325,7 +325,6 @@ func (suite *GraphConnectorIntegrationSuite) TestMailFetch() {
var ( var (
t = suite.T() t = suite.T()
userID = tester.M365UserID(t) userID = tester.M365UserID(t)
sel = selectors.NewExchangeBackup()
) )
tests := []struct { tests := []struct {
@ -333,11 +332,17 @@ func (suite *GraphConnectorIntegrationSuite) TestMailFetch() {
scope selectors.ExchangeScope scope selectors.ExchangeScope
folderNames map[string]struct{} folderNames map[string]struct{}
}{ }{
{
name: "Mail Iterative Check",
scope: selectors.NewExchangeBackup().MailFolders([]string{userID}, selectors.Any())[0],
folderNames: map[string]struct{}{
exchange.DefaultMailFolder: {},
"Sent Items": {},
},
},
{ {
name: "Folder Iterative Check Mail", name: "Folder Iterative Check Mail",
// Only select specific folders so the test doesn't flake when the CI scope: selectors.NewExchangeBackup().MailFolders(
// cleanup task deletes things.
scope: sel.MailFolders(
[]string{userID}, []string{userID},
[]string{exchange.DefaultMailFolder}, []string{exchange.DefaultMailFolder},
)[0], )[0],
@ -351,25 +356,7 @@ func (suite *GraphConnectorIntegrationSuite) TestMailFetch() {
for _, test := range tests { for _, test := range tests {
suite.T().Run(test.name, func(t *testing.T) { suite.T().Run(test.name, func(t *testing.T) {
qp := graph.QueryParams{ collections, err := gc.createCollections(ctx, test.scope)
User: userID,
Scope: test.scope,
Credentials: gc.credentials,
FailFast: false,
}
resolver, err := exchange.MaybeGetAndPopulateFolderResolver(
ctx,
qp,
scopeToPathCategory(qp.Scope),
)
require.NoError(t, err)
collections, err := gc.fetchItemsByFolder(
ctx,
qp,
resolver,
)
require.NoError(t, err) require.NoError(t, err)
for _, c := range collections { for _, c := range collections {