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:
parent
d30f6963c0
commit
bb7e48a82e
@ -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() {
|
||||||
|
|||||||
@ -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
|
||||||
|
iterativeFunction func(
|
||||||
|
container map[string]graph.Container,
|
||||||
|
aFilter string,
|
||||||
|
errUpdater func(string, error)) func(any) bool
|
||||||
transformer absser.ParsableFactory
|
transformer absser.ParsableFactory
|
||||||
folderNames map[string]struct{}
|
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
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)
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)...)
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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,
|
switch category {
|
||||||
error,
|
case path.ContactsCategory:
|
||||||
) {
|
return GetAllContactFolders(ctx, qp, gs)
|
||||||
if scope.IncludesCategory(selectors.ExchangeMail) {
|
case path.EmailCategory:
|
||||||
return nil, nil, nil, errors.New("mail no longer supported this way")
|
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.ExchangeContact) {
|
// PopulateExchangeContainerResolver gets a folder resolver if one is available for
|
||||||
return models.CreateContactFolderCollectionResponseFromDiscriminatorValue,
|
|
||||||
[]GraphQuery{GetAllContactFolderNamesForUser, GetDefaultContactFolderForUser},
|
|
||||||
IterateSelectAllContactsForCollections,
|
|
||||||
nil
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
// 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(
|
||||||
|
qp.Credentials.AzureTenantID,
|
||||||
|
qp.User,
|
||||||
|
category,
|
||||||
|
false,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "resolving folder ID")
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
return p.ToDataLayerExchangePathForCategory(
|
if dirPath == nil && category == path.EmailCategory {
|
||||||
tenantID,
|
return nil, false // Only true for root mail folder
|
||||||
user,
|
|
||||||
category,
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getCollectionPath(
|
directory = pb.String()
|
||||||
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).
|
switch category {
|
||||||
ToDataLayerExchangePathForCategory(
|
case path.EmailCategory:
|
||||||
qp.Credentials.AzureTenantID,
|
return dirPath, qp.Scope.Matches(selectors.ExchangeMailFolder, directory)
|
||||||
qp.User,
|
case path.ContactsCategory:
|
||||||
category,
|
return dirPath, qp.Scope.Matches(selectors.ExchangeContactFolder, directory)
|
||||||
false,
|
case path.EventsCategory:
|
||||||
)
|
return dirPath, qp.Scope.Matches(selectors.ExchangeEventCalendar, directory)
|
||||||
if err1 == nil {
|
default:
|
||||||
return aPath, nil
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil,
|
|
||||||
support.WrapAndAppend(
|
|
||||||
fmt.Sprintf(
|
|
||||||
"both path generate functions failed for %s:%s:%s",
|
|
||||||
qp.User,
|
|
||||||
category,
|
|
||||||
directory),
|
|
||||||
err,
|
|
||||||
err1,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func AddItemsToCollection(
|
func AddItemsToCollection(
|
||||||
|
|||||||
@ -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)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
|
||||||
|
|
||||||
// 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 (
|
|
||||||
isEnabled bool
|
|
||||||
service graph.Service
|
|
||||||
)
|
|
||||||
|
|
||||||
return func(pageItem any) bool {
|
|
||||||
if !isEnabled {
|
|
||||||
// Create Collections based on qp.Scope
|
|
||||||
err := CollectFolders(ctx, qp, collections, statusUpdater, resolver)
|
|
||||||
if err != nil {
|
|
||||||
errUpdater(
|
|
||||||
qp.User,
|
|
||||||
errors.Wrap(err, support.ConnectorStackErrorTrace(err)),
|
|
||||||
)
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
service, err = createService(qp.Credentials, qp.FailFast)
|
|
||||||
if err != nil {
|
|
||||||
errUpdater(qp.User, err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
isEnabled = true
|
|
||||||
}
|
|
||||||
|
|
||||||
pageItem = CreateCalendarDisplayable(pageItem)
|
|
||||||
|
|
||||||
calendar, ok := pageItem.(graph.Displayable)
|
|
||||||
if !ok {
|
|
||||||
errUpdater(
|
|
||||||
qp.User,
|
|
||||||
fmt.Errorf("unable to parse pageItem into CalendarDisplayable: %T", pageItem),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
// selector parameters.
|
|
||||||
func CollectionsFromResolver(
|
|
||||||
ctx context.Context,
|
|
||||||
qp graph.QueryParams,
|
|
||||||
resolver graph.ContainerResolver,
|
|
||||||
statusUpdater support.StatusUpdater,
|
|
||||||
collections map[string]*Collection,
|
|
||||||
) error {
|
) error {
|
||||||
option, category, notMatcher := getCategoryAndValidation(qp.Scope)
|
var (
|
||||||
|
category = graph.ScopeToPathCategory(qp.Scope)
|
||||||
for _, item := range resolver.Items() {
|
collectionType = categoryToOptionIdentifier(category)
|
||||||
pathString := item.Path().String()
|
errs error
|
||||||
// Skip the root folder for mail which has an empty path.
|
|
||||||
if len(pathString) == 0 || notMatcher(&pathString) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
completePath, err := item.Path().ToDataLayerExchangePathForCategory(
|
|
||||||
qp.Credentials.AzureTenantID,
|
|
||||||
qp.User,
|
|
||||||
category,
|
|
||||||
false,
|
|
||||||
)
|
)
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "resolving collection item path")
|
|
||||||
}
|
|
||||||
|
|
||||||
|
for _, c := range resolver.Items() {
|
||||||
|
dirPath, ok := pathAndMatch(qp, category, c)
|
||||||
|
if ok {
|
||||||
|
// Create only those that match
|
||||||
service, err := createService(qp.Credentials, qp.FailFast)
|
service, err := createService(qp.Credentials, qp.FailFast)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "making service instance")
|
errs = support.WrapAndAppend(
|
||||||
}
|
qp.User+" failed to create service during FilterContainerAndFillCollection",
|
||||||
|
|
||||||
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,
|
err,
|
||||||
)
|
errs)
|
||||||
|
|
||||||
return true
|
if qp.FailFast {
|
||||||
|
return errs
|
||||||
}
|
}
|
||||||
|
|
||||||
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(
|
edc := NewCollection(
|
||||||
qp.User,
|
qp.User,
|
||||||
dirPath,
|
dirPath,
|
||||||
contacts,
|
collectionType,
|
||||||
service,
|
service,
|
||||||
statusUpdater,
|
statusUpdater,
|
||||||
)
|
)
|
||||||
|
collections[*c.GetId()] = &edc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
listOfIDs, err := ReturnContactIDsFromDirectory(ctx, service, qp.User, *folder.GetParentFolderId())
|
for directoryID, col := range collections {
|
||||||
|
fetchFunc, err := getFetchIDFunc(category)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errUpdater(
|
errs = support.WrapAndAppend(
|
||||||
qp.User,
|
qp.User,
|
||||||
err,
|
err,
|
||||||
)
|
errs)
|
||||||
|
|
||||||
return false
|
if qp.FailFast {
|
||||||
|
return errs
|
||||||
}
|
}
|
||||||
|
|
||||||
edc.jobs = append(edc.jobs, listOfIDs...)
|
continue
|
||||||
collections[DefaultContactFolder] = &edc
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if folder.GetDisplayName() == nil {
|
jobs, err := fetchFunc(ctx, col.service, qp.User, directoryID)
|
||||||
// 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 {
|
if err != nil {
|
||||||
errUpdater(
|
errs = support.WrapAndAppend(
|
||||||
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
|
return errs
|
||||||
// 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{}
|
|
||||||
|
|
||||||
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()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
|
||||||
}
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
109
src/internal/connector/graph/cache_container.go
Normal file
109
src/internal/connector/graph/cache_container.go
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 {
|
||||||
errs = multierror.Append(errs, err)
|
return nil, errors.Wrap(err, "filling collections")
|
||||||
}
|
|
||||||
} 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 {
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user