Compare commits

...

5 Commits

Author SHA1 Message Date
ryanfkeepers
eb48ce06c0 generic drive retrieval for sharepoint
Adapts the graph onedrive library to handle
access to drive data across both onedrive and
sharepoint services.
2022-11-16 13:01:14 -07:00
ryanfkeepers
c97f5ea9a7 migrate containerCache to graph pkg
ContainerCache is a generic resource that can
be moved to the graph package.  Service-
specific code is tied to the `populate` func,
which remains in each service/category.  This
change is a prepatory refactoring to reduce
boilerplate for sharepoint implementation.
2022-11-15 16:46:33 -07:00
ryanfkeepers
92e109d4eb extract scope from qp
Since scopes are service specific, we cannot
easily house them within the graph QueryParam
struct, unless we bloat the struct with all types.
Alternatively, we could add a generic "scope"
with parsers, much like the Selector itself.  But
really, the most simple solution is to only pass
the scope within the tree of service funcs that
use it.
2022-11-14 16:09:03 -07:00
ryanfkeepers
ea65506406 Adds the foundation of sharepoint data coll
boilerplate to graph connector.  To minimize
changes, some dependencies or half-baked
solutions have been listed as TODOs for follow-
up.
2022-11-14 14:09:21 -07:00
ryanfkeepers
67888abad0 connector data coll refactor
A quick code movement before adding the
sharepoint datacollection production so that
we minimize graph_conn file bloat.
2022-11-14 12:45:39 -07:00
32 changed files with 1560 additions and 951 deletions

View File

@ -240,12 +240,12 @@ func purgeMailFolders(
uid string, uid string,
) error { ) error {
getter := func(gs graph.Service, uid, prefix string) ([]purgable, error) { getter := func(gs graph.Service, uid, prefix string) ([]purgable, error) {
params, err := exchangeQueryParamFactory(uid, path.EmailCategory) params, scope, err := exchangeQueryParamFactory(uid, path.EmailCategory)
if err != nil { if err != nil {
return nil, err return nil, err
} }
allFolders, err := exchange.GetAllMailFolders(ctx, *params, gs) allFolders, err := exchange.GetAllMailFolders(ctx, *params, gs, scope)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -276,12 +276,12 @@ func purgeCalendarFolders(
uid string, uid string,
) error { ) error {
getter := func(gs graph.Service, uid, prefix string) ([]purgable, error) { getter := func(gs graph.Service, uid, prefix string) ([]purgable, error) {
params, err := exchangeQueryParamFactory(uid, path.EventsCategory) params, scope, err := exchangeQueryParamFactory(uid, path.EventsCategory)
if err != nil { if err != nil {
return nil, err return nil, err
} }
allCalendars, err := exchange.GetAllCalendars(ctx, *params, gs) allCalendars, err := exchange.GetAllCalendars(ctx, *params, gs, scope)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -312,12 +312,12 @@ func purgeContactFolders(
uid string, uid string,
) error { ) error {
getter := func(gs graph.Service, uid, prefix string) ([]purgable, error) { getter := func(gs graph.Service, uid, prefix string) ([]purgable, error) {
params, err := exchangeQueryParamFactory(uid, path.ContactsCategory) params, scope, err := exchangeQueryParamFactory(uid, path.ContactsCategory)
if err != nil { if err != nil {
return nil, err return nil, err
} }
allContainers, err := exchange.GetAllContactFolders(ctx, *params, gs) allContainers, err := exchange.GetAllContactFolders(ctx, *params, gs, scope)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -518,7 +518,10 @@ func containerFilter(nameContains string, containers []graph.CachedContainer) []
return result return result
} }
func exchangeQueryParamFactory(user string, category path.CategoryType) (*graph.QueryParams, error) { func exchangeQueryParamFactory(
user string,
category path.CategoryType,
) (*graph.QueryParams, selectors.ExchangeScope, error) {
var scope selectors.ExchangeScope var scope selectors.ExchangeScope
switch category { switch category {
@ -529,12 +532,11 @@ func exchangeQueryParamFactory(user string, category path.CategoryType) (*graph.
case path.EventsCategory: case path.EventsCategory:
scope = selectors.NewExchangeBackup().EventCalendars([]string{user}, selectors.Any())[0] scope = selectors.NewExchangeBackup().EventCalendars([]string{user}, selectors.Any())[0]
default: default:
return nil, fmt.Errorf("category %s not supported", category) return nil, scope, fmt.Errorf("category %s not supported", category)
} }
params := &graph.QueryParams{ params := &graph.QueryParams{
User: user, ResourceOwner: user,
Scope: scope,
FailFast: false, FailFast: false,
Credentials: account.M365Config{ Credentials: account.M365Config{
M365: credentials.GetM365(), M365: credentials.GetM365(),
@ -542,5 +544,5 @@ func exchangeQueryParamFactory(user string, category path.CategoryType) (*graph.
}, },
} }
return params, nil return params, scope, nil
} }

View File

@ -0,0 +1,341 @@
package connector
import (
"context"
"fmt"
"strings"
"github.com/hashicorp/go-multierror"
"github.com/pkg/errors"
"github.com/alcionai/corso/src/internal/connector/exchange"
"github.com/alcionai/corso/src/internal/connector/graph"
"github.com/alcionai/corso/src/internal/connector/onedrive"
"github.com/alcionai/corso/src/internal/connector/sharepoint"
"github.com/alcionai/corso/src/internal/connector/support"
"github.com/alcionai/corso/src/internal/data"
D "github.com/alcionai/corso/src/internal/diagnostics"
"github.com/alcionai/corso/src/internal/observe"
"github.com/alcionai/corso/src/pkg/logger"
"github.com/alcionai/corso/src/pkg/path"
"github.com/alcionai/corso/src/pkg/selectors"
)
// ---------------------------------------------------------------------------
// Data Collections
// ---------------------------------------------------------------------------
// DataCollections utility function to launch backup operations for exchange and onedrive
func (gc *GraphConnector) DataCollections(ctx context.Context, sels selectors.Selector) ([]data.Collection, error) {
ctx, end := D.Span(ctx, "gc:dataCollections", D.Index("service", sels.Service.String()))
defer end()
err := verifyBackupInputs(sels, gc.Users)
if err != nil {
return nil, err
}
switch sels.Service {
case selectors.ServiceExchange:
return gc.ExchangeDataCollection(ctx, sels)
case selectors.ServiceOneDrive:
return gc.OneDriveDataCollections(ctx, sels)
case selectors.ServiceSharePoint:
return gc.SharePointDataCollections(ctx, sels)
default:
return nil, errors.Errorf("service %s not supported", sels)
}
}
func verifyBackupInputs(sel selectors.Selector, mapOfUsers map[string]string) error {
var personnel []string
// retrieve users from selectors
switch sel.Service {
case selectors.ServiceExchange:
backup, err := sel.ToExchangeBackup()
if err != nil {
return err
}
for _, scope := range backup.Scopes() {
temp := scope.Get(selectors.ExchangeUser)
personnel = append(personnel, temp...)
}
case selectors.ServiceOneDrive:
backup, err := sel.ToOneDriveBackup()
if err != nil {
return err
}
for _, user := range backup.Scopes() {
temp := user.Get(selectors.OneDriveUser)
personnel = append(personnel, temp...)
}
default:
return errors.New("service %s not supported")
}
// verify personnel
normUsers := map[string]struct{}{}
for k := range mapOfUsers {
normUsers[strings.ToLower(k)] = struct{}{}
}
for _, user := range personnel {
if _, ok := normUsers[strings.ToLower(user)]; !ok {
return fmt.Errorf("%s user not found within tenant", user)
}
}
return nil
}
// ---------------------------------------------------------------------------
// Exchange
// ---------------------------------------------------------------------------
// createExchangeCollections - utility function that retrieves M365
// IDs through Microsoft Graph API. The selectors.ExchangeScope
// determines the type of collections that are retrieved.
func (gc *GraphConnector) createExchangeCollections(
ctx context.Context,
scope selectors.ExchangeScope,
) ([]*exchange.Collection, error) {
var (
errs *multierror.Error
users = scope.Get(selectors.ExchangeUser)
allCollections = make([]*exchange.Collection, 0)
)
// Create collection of ExchangeDataCollection
for _, user := range users {
collections := make(map[string]*exchange.Collection)
qp := graph.QueryParams{
Category: scope.Category().PathType(),
ResourceOwner: user,
FailFast: gc.failFast,
Credentials: gc.credentials,
}
foldersComplete, closer := observe.MessageWithCompletion(fmt.Sprintf("∙ %s - %s:", qp.Category, user))
defer closer()
defer close(foldersComplete)
resolver, err := exchange.PopulateExchangeContainerResolver(ctx, qp)
if err != nil {
return nil, errors.Wrap(err, "getting folder cache")
}
err = exchange.FilterContainersAndFillCollections(
ctx,
qp,
collections,
gc.UpdateStatus,
resolver,
scope)
if err != nil {
return nil, errors.Wrap(err, "filling collections")
}
foldersComplete <- struct{}{}
for _, collection := range collections {
gc.incrementAwaitingMessages()
allCollections = append(allCollections, collection)
}
}
return allCollections, errs.ErrorOrNil()
}
// ExchangeDataCollections returns a DataCollection which the caller can
// use to read mailbox data out for the specified user
// Assumption: User exists
//
// Add iota to this call -> mail, contacts, calendar, etc.
func (gc *GraphConnector) ExchangeDataCollection(
ctx context.Context,
selector selectors.Selector,
) ([]data.Collection, error) {
eb, err := selector.ToExchangeBackup()
if err != nil {
return nil, errors.Wrap(err, "exchangeDataCollection: parsing selector")
}
var (
scopes = eb.DiscreteScopes(gc.GetUsers())
collections = []data.Collection{}
errs error
)
for _, scope := range scopes {
// Creates a map of collections based on scope
dcs, err := gc.createExchangeCollections(ctx, scope)
if err != nil {
user := scope.Get(selectors.ExchangeUser)
return nil, support.WrapAndAppend(user[0], err, errs)
}
for _, collection := range dcs {
collections = append(collections, collection)
}
}
return collections, errs
}
// ---------------------------------------------------------------------------
// OneDrive
// ---------------------------------------------------------------------------
type odFolderMatcher struct {
scope selectors.OneDriveScope
}
func (fm odFolderMatcher) IsAny() bool {
return fm.scope.IsAny(selectors.OneDriveFolder)
}
func (fm odFolderMatcher) Matches(path string) bool {
return fm.scope.Matches(selectors.OneDriveFolder, path)
}
// OneDriveDataCollections returns a set of DataCollection which represents the OneDrive data
// for the specified user
func (gc *GraphConnector) OneDriveDataCollections(
ctx context.Context,
selector selectors.Selector,
) ([]data.Collection, error) {
odb, err := selector.ToOneDriveBackup()
if err != nil {
return nil, errors.Wrap(err, "oneDriveDataCollection: parsing selector")
}
var (
scopes = odb.DiscreteScopes(gc.GetUsers())
collections = []data.Collection{}
errs error
)
// for each scope that includes oneDrive items, get all
for _, scope := range scopes {
for _, user := range scope.Get(selectors.OneDriveUser) {
logger.Ctx(ctx).With("user", user).Debug("Creating OneDrive collections")
odcs, err := onedrive.NewCollections(
gc.credentials.AzureTenantID,
user,
onedrive.OneDriveSource,
odFolderMatcher{scope},
&gc.graphService,
gc.UpdateStatus,
).Get(ctx)
if err != nil {
return nil, support.WrapAndAppend(user, err, errs)
}
collections = append(collections, odcs...)
}
}
for range collections {
gc.incrementAwaitingMessages()
}
return collections, errs
}
// ---------------------------------------------------------------------------
// SharePoint
// ---------------------------------------------------------------------------
// createSharePointCollections - utility function that retrieves M365
// IDs through Microsoft Graph API. The selectors.SharePointScope
// determines the type of collections that are retrieved.
func (gc *GraphConnector) createSharePointCollections(
ctx context.Context,
scope selectors.SharePointScope,
) ([]data.Collection, error) {
var (
errs *multierror.Error
sites = scope.Get(selectors.SharePointSite)
category = scope.Category().PathType()
collections = make([]data.Collection, 0)
)
// Create collection of ExchangeDataCollection
for _, site := range sites {
foldersComplete, closer := observe.MessageWithCompletion(fmt.Sprintf("∙ %s - %s:", category, site))
defer closer()
defer close(foldersComplete)
switch category {
case path.FilesCategory: // TODO: better category for drives
spcs, err := sharepoint.CollectLibraries(
ctx,
gc.Service(),
gc.credentials.AzureTenantID,
gc.GetSiteIds(),
scope,
gc.UpdateStatus,
gc.incrementAwaitingMessages,
)
if err != nil {
return nil, support.WrapAndAppend(site, err, errs)
}
collections = append(collections, spcs...)
// case path.UnknownCategory: // TODO: ListsCategory
// // get lists
}
foldersComplete <- struct{}{}
}
return collections, errs.ErrorOrNil()
}
// SharePointDataCollections returns a set of DataCollection which represents the SharePoint data
// for the specified user
func (gc *GraphConnector) SharePointDataCollections(
ctx context.Context,
selector selectors.Selector,
) ([]data.Collection, error) {
b, err := selector.ToSharePointBackup()
if err != nil {
return nil, errors.Wrap(err, "sharePointDataCollection: parsing selector")
}
var (
scopes = b.DiscreteScopes(gc.GetSites())
collections = []data.Collection{}
errs error
)
// for each scope that includes oneDrive items, get all
for _, scope := range scopes {
// Creates a slice of collections based on scope
dcs, err := gc.createSharePointCollections(ctx, scope)
if err != nil {
return nil, support.WrapAndAppend(scope.Get(selectors.SharePointSite)[0], err, errs)
}
for _, collection := range dcs {
collections = append(collections, collection)
}
}
for range collections {
gc.incrementAwaitingMessages()
}
return collections, errs
}

View File

@ -0,0 +1,510 @@
package connector
import (
"bytes"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/alcionai/corso/src/internal/connector/exchange"
"github.com/alcionai/corso/src/internal/connector/support"
"github.com/alcionai/corso/src/internal/tester"
"github.com/alcionai/corso/src/pkg/selectors"
)
// ---------------------------------------------------------------------------
// DataCollection tests
// ---------------------------------------------------------------------------
type ConnectorDataCollectionIntegrationSuite struct {
suite.Suite
connector *GraphConnector
user string
site string
}
func TestConnectorDataCollectionIntegrationSuite(t *testing.T) {
if err := tester.RunOnAny(
tester.CorsoCITests,
tester.CorsoConnectorDataCollectionTests,
); err != nil {
t.Skip(err)
}
suite.Run(t, new(ConnectorDataCollectionIntegrationSuite))
}
func (suite *ConnectorDataCollectionIntegrationSuite) SetupSuite() {
ctx, flush := tester.NewContext()
defer flush()
_, err := tester.GetRequiredEnvVars(tester.M365AcctCredEnvs...)
require.NoError(suite.T(), err)
suite.connector = loadConnector(ctx, suite.T())
suite.user = tester.M365UserID(suite.T())
suite.site = tester.M365SiteID(suite.T())
tester.LogTimeOfTest(suite.T())
}
// TestExchangeDataCollection verifies interface between operation and
// GraphConnector remains stable to receive a non-zero amount of Collections
// for the Exchange Package. Enabled exchange applications:
// - mail
// - contacts
// - events
func (suite *ConnectorDataCollectionIntegrationSuite) TestExchangeDataCollection() {
ctx, flush := tester.NewContext()
defer flush()
connector := loadConnector(ctx, suite.T())
tests := []struct {
name string
getSelector func(t *testing.T) selectors.Selector
}{
{
name: suite.user + " Email",
getSelector: func(t *testing.T) selectors.Selector {
sel := selectors.NewExchangeBackup()
sel.Include(sel.MailFolders([]string{suite.user}, []string{exchange.DefaultMailFolder}, selectors.PrefixMatch()))
return sel.Selector
},
},
{
name: suite.user + " Contacts",
getSelector: func(t *testing.T) selectors.Selector {
sel := selectors.NewExchangeBackup()
sel.Include(sel.ContactFolders(
[]string{suite.user},
[]string{exchange.DefaultContactFolder},
selectors.PrefixMatch()))
return sel.Selector
},
},
{
name: suite.user + " Events",
getSelector: func(t *testing.T) selectors.Selector {
sel := selectors.NewExchangeBackup()
sel.Include(sel.EventCalendars([]string{suite.user}, []string{exchange.DefaultCalendar}, selectors.PrefixMatch()))
return sel.Selector
},
},
}
for _, test := range tests {
suite.T().Run(test.name, func(t *testing.T) {
collection, err := connector.ExchangeDataCollection(ctx, test.getSelector(t))
require.NoError(t, err)
assert.Equal(t, len(collection), 1)
channel := collection[0].Items()
for object := range channel {
buf := &bytes.Buffer{}
_, err := buf.ReadFrom(object.ToReader())
assert.NoError(t, err, "received a buf.Read error")
}
status := connector.AwaitStatus()
assert.NotZero(t, status.Successful)
t.Log(status.String())
})
}
}
// TestInvalidUserForDataCollections ensures verification process for users
func (suite *ConnectorDataCollectionIntegrationSuite) TestInvalidUserForDataCollections() {
ctx, flush := tester.NewContext()
defer flush()
invalidUser := "foo@example.com"
connector := loadConnector(ctx, suite.T())
tests := []struct {
name string
getSelector func(t *testing.T) selectors.Selector
}{
{
name: "invalid exchange backup user",
getSelector: func(t *testing.T) selectors.Selector {
sel := selectors.NewExchangeBackup()
sel.Include(sel.MailFolders([]string{invalidUser}, selectors.Any()))
return sel.Selector
},
},
{
name: "Invalid onedrive backup user",
getSelector: func(t *testing.T) selectors.Selector {
sel := selectors.NewOneDriveBackup()
sel.Include(sel.Folders([]string{invalidUser}, selectors.Any()))
return sel.Selector
},
},
}
for _, test := range tests {
suite.T().Run(test.name, func(t *testing.T) {
collections, err := connector.DataCollections(ctx, test.getSelector(t))
assert.Error(t, err)
assert.Empty(t, collections)
})
}
}
// TestSharePointDataCollection verifies interface between operation and
// GraphConnector remains stable to receive a non-zero amount of Collections
// for the SharePoint Package.
func (suite *ConnectorDataCollectionIntegrationSuite) TestSharePointDataCollection() {
ctx, flush := tester.NewContext()
defer flush()
connector := loadConnector(ctx, suite.T())
tests := []struct {
name string
getSelector func(t *testing.T) selectors.Selector
}{
{
name: "Items - TODO: actual sharepoint categories",
getSelector: func(t *testing.T) selectors.Selector {
sel := selectors.NewSharePointBackup()
sel.Include(sel.Folders([]string{suite.site}, selectors.Any()))
return sel.Selector
},
},
}
for _, test := range tests {
suite.T().Run(test.name, func(t *testing.T) {
_, err := connector.SharePointDataCollections(ctx, test.getSelector(t))
require.NoError(t, err)
// TODO: Implementation
// assert.Equal(t, len(collection), 1)
// channel := collection[0].Items()
// for object := range channel {
// buf := &bytes.Buffer{}
// _, err := buf.ReadFrom(object.ToReader())
// assert.NoError(t, err, "received a buf.Read error")
// }
// status := connector.AwaitStatus()
// assert.NotZero(t, status.Successful)
// t.Log(status.String())
})
}
}
// ---------------------------------------------------------------------------
// CreateExchangeCollection tests
// ---------------------------------------------------------------------------
type ConnectorCreateExchangeCollectionIntegrationSuite struct {
suite.Suite
connector *GraphConnector
user string
site string
}
func TestConnectorCreateExchangeCollectionIntegrationSuite(t *testing.T) {
if err := tester.RunOnAny(
tester.CorsoCITests,
tester.CorsoConnectorCreateExchangeCollectionTests,
); err != nil {
t.Skip(err)
}
suite.Run(t, new(ConnectorCreateExchangeCollectionIntegrationSuite))
}
func (suite *ConnectorCreateExchangeCollectionIntegrationSuite) SetupSuite() {
ctx, flush := tester.NewContext()
defer flush()
_, err := tester.GetRequiredEnvVars(tester.M365AcctCredEnvs...)
require.NoError(suite.T(), err)
suite.connector = loadConnector(ctx, suite.T())
suite.user = tester.M365UserID(suite.T())
suite.site = tester.M365SiteID(suite.T())
tester.LogTimeOfTest(suite.T())
}
func (suite *ConnectorCreateExchangeCollectionIntegrationSuite) TestMailFetch() {
ctx, flush := tester.NewContext()
defer flush()
var (
t = suite.T()
userID = tester.M365UserID(t)
)
tests := []struct {
name string
scope selectors.ExchangeScope
folderNames map[string]struct{}
}{
{
name: "Folder Iterative Check Mail",
scope: selectors.NewExchangeBackup().MailFolders(
[]string{userID},
[]string{exchange.DefaultMailFolder},
selectors.PrefixMatch(),
)[0],
folderNames: map[string]struct{}{
exchange.DefaultMailFolder: {},
},
},
}
gc := loadConnector(ctx, t)
for _, test := range tests {
suite.T().Run(test.name, func(t *testing.T) {
collections, err := gc.createExchangeCollections(ctx, test.scope)
require.NoError(t, err)
for _, c := range collections {
require.NotEmpty(t, c.FullPath().Folder())
folder := c.FullPath().Folder()
if _, ok := test.folderNames[folder]; ok {
delete(test.folderNames, folder)
}
}
assert.Empty(t, test.folderNames)
})
}
}
// TestMailSerializationRegression verifies that all mail data stored in the
// test account can be successfully downloaded into bytes and restored into
// M365 mail objects
func (suite *ConnectorCreateExchangeCollectionIntegrationSuite) TestMailSerializationRegression() {
ctx, flush := tester.NewContext()
defer flush()
t := suite.T()
connector := loadConnector(ctx, t)
sel := selectors.NewExchangeBackup()
sel.Include(sel.MailFolders([]string{suite.user}, []string{exchange.DefaultMailFolder}, selectors.PrefixMatch()))
collection, err := connector.createExchangeCollections(ctx, sel.Scopes()[0])
require.NoError(t, err)
for _, edc := range collection {
suite.T().Run(edc.FullPath().String(), func(t *testing.T) {
streamChannel := edc.Items()
// Verify that each message can be restored
for stream := range streamChannel {
buf := &bytes.Buffer{}
read, err := buf.ReadFrom(stream.ToReader())
assert.NoError(t, err)
assert.NotZero(t, read)
message, err := support.CreateMessageFromBytes(buf.Bytes())
assert.NotNil(t, message)
assert.NoError(t, err)
}
})
}
status := connector.AwaitStatus()
suite.NotNil(status)
suite.Equal(status.ObjectCount, status.Successful)
}
// TestContactSerializationRegression verifies ability to query contact items
// and to store contact within Collection. Downloaded contacts are run through
// a regression test to ensure that downloaded items can be uploaded.
func (suite *ConnectorCreateExchangeCollectionIntegrationSuite) TestContactSerializationRegression() {
ctx, flush := tester.NewContext()
defer flush()
connector := loadConnector(ctx, suite.T())
tests := []struct {
name string
getCollection func(t *testing.T) []*exchange.Collection
}{
{
name: "Default Contact Folder",
getCollection: func(t *testing.T) []*exchange.Collection {
scope := selectors.
NewExchangeBackup().
ContactFolders([]string{suite.user}, []string{exchange.DefaultContactFolder}, selectors.PrefixMatch())[0]
collections, err := connector.createExchangeCollections(ctx, scope)
require.NoError(t, err)
return collections
},
},
}
for _, test := range tests {
suite.T().Run(test.name, func(t *testing.T) {
edcs := test.getCollection(t)
require.Equal(t, len(edcs), 1)
edc := edcs[0]
assert.Equal(t, edc.FullPath().Folder(), exchange.DefaultContactFolder)
streamChannel := edc.Items()
count := 0
for stream := range streamChannel {
buf := &bytes.Buffer{}
read, err := buf.ReadFrom(stream.ToReader())
assert.NoError(t, err)
assert.NotZero(t, read)
contact, err := support.CreateContactFromBytes(buf.Bytes())
assert.NotNil(t, contact)
assert.NoError(t, err, "error on converting contact bytes: "+string(buf.Bytes()))
count++
}
assert.NotZero(t, count)
status := connector.AwaitStatus()
suite.NotNil(status)
suite.Equal(status.ObjectCount, status.Successful)
})
}
}
// TestEventsSerializationRegression ensures functionality of createCollections
// to be able to successfully query, download and restore event objects
func (suite *ConnectorCreateExchangeCollectionIntegrationSuite) TestEventsSerializationRegression() {
ctx, flush := tester.NewContext()
defer flush()
connector := loadConnector(ctx, suite.T())
tests := []struct {
name, expected string
getCollection func(t *testing.T) []*exchange.Collection
}{
{
name: "Default Event Calendar",
expected: exchange.DefaultCalendar,
getCollection: func(t *testing.T) []*exchange.Collection {
sel := selectors.NewExchangeBackup()
sel.Include(sel.EventCalendars([]string{suite.user}, []string{exchange.DefaultCalendar}, selectors.PrefixMatch()))
collections, err := connector.createExchangeCollections(ctx, sel.Scopes()[0])
require.NoError(t, err)
return collections
},
},
{
name: "Birthday Calendar",
expected: "Birthdays",
getCollection: func(t *testing.T) []*exchange.Collection {
sel := selectors.NewExchangeBackup()
sel.Include(sel.EventCalendars([]string{suite.user}, []string{"Birthdays"}, selectors.PrefixMatch()))
collections, err := connector.createExchangeCollections(ctx, sel.Scopes()[0])
require.NoError(t, err)
return collections
},
},
}
for _, test := range tests {
suite.T().Run(test.name, func(t *testing.T) {
collections := test.getCollection(t)
require.Equal(t, len(collections), 1)
edc := collections[0]
assert.Equal(t, edc.FullPath().Folder(), test.expected)
streamChannel := edc.Items()
for stream := range streamChannel {
buf := &bytes.Buffer{}
read, err := buf.ReadFrom(stream.ToReader())
assert.NoError(t, err)
assert.NotZero(t, read)
event, err := support.CreateEventFromBytes(buf.Bytes())
assert.NotNil(t, event)
assert.NoError(t, err, "experienced error parsing event bytes: "+string(buf.Bytes()))
}
status := connector.AwaitStatus()
suite.NotNil(status)
suite.Equal(status.ObjectCount, status.Successful)
})
}
}
// TestAccessOfInboxAllUsers verifies that GraphConnector can
// support `--users *` for backup operations. Selector.DiscreteScopes
// returns all of the users within one scope. Only users who have
// messages in their inbox will have a collection returned.
// The final test insures that more than a 75% of the user collections are
// returned. If an error was experienced, the test will fail overall
func (suite *ConnectorCreateExchangeCollectionIntegrationSuite) TestAccessOfInboxAllUsers() {
ctx, flush := tester.NewContext()
defer flush()
t := suite.T()
connector := loadConnector(ctx, t)
sel := selectors.NewExchangeBackup()
sel.Include(sel.MailFolders(selectors.Any(), []string{exchange.DefaultMailFolder}, selectors.PrefixMatch()))
scopes := sel.DiscreteScopes(connector.GetUsers())
for _, scope := range scopes {
users := scope.Get(selectors.ExchangeUser)
standard := (len(users) / 4) * 3
collections, err := connector.createExchangeCollections(ctx, scope)
require.NoError(t, err)
suite.Greater(len(collections), standard)
}
}
// ---------------------------------------------------------------------------
// CreateSharePointCollection tests
// ---------------------------------------------------------------------------
type ConnectorCreateSharePointCollectionIntegrationSuite struct {
suite.Suite
connector *GraphConnector
user string
}
func TestConnectorCreateSharePointCollectionIntegrationSuite(t *testing.T) {
if err := tester.RunOnAny(
tester.CorsoCITests,
tester.CorsoConnectorCreateSharePointCollectionTests,
); err != nil {
t.Skip(err)
}
suite.Run(t, new(ConnectorCreateSharePointCollectionIntegrationSuite))
}
func (suite *ConnectorCreateSharePointCollectionIntegrationSuite) SetupSuite() {
ctx, flush := tester.NewContext()
defer flush()
_, err := tester.GetRequiredEnvVars(tester.M365AcctCredEnvs...)
require.NoError(suite.T(), err)
suite.connector = loadConnector(ctx, suite.T())
suite.user = tester.M365UserID(suite.T())
tester.LogTimeOfTest(suite.T())
}
func (suite *ConnectorCreateSharePointCollectionIntegrationSuite) TestCreateSharePointCollection() {
ctx, flush := tester.NewContext()
defer flush()
var (
t = suite.T()
userID = tester.M365UserID(t)
)
gc := loadConnector(ctx, t)
scope := selectors.NewSharePointBackup().Folders(
[]string{userID},
[]string{"foo"},
selectors.PrefixMatch(),
)[0]
_, err := gc.createSharePointCollections(ctx, scope)
require.NoError(t, err)
}

View File

@ -2,66 +2,8 @@ package exchange
import ( import (
"github.com/microsoftgraph/msgraph-sdk-go/models" "github.com/microsoftgraph/msgraph-sdk-go/models"
"github.com/pkg/errors"
"github.com/alcionai/corso/src/internal/connector/graph"
"github.com/alcionai/corso/src/pkg/path"
) )
// checkIDAndName is a helper function to ensure that
// the ID and name pointers are set prior to being called.
func checkIDAndName(c graph.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)
}
return nil
}
// checkRequiredValues is a helper function to ensure that
// all the pointers are set prior to being called.
func checkRequiredValues(c graph.Container) error {
if err := checkIDAndName(c); err != nil {
return err
}
ptr := c.GetParentFolderId()
if ptr == nil || len(*ptr) == 0 {
return errors.Errorf("folder %s without parent ID", *c.GetId())
}
return nil
}
//======================================
// cachedContainer Implementations
//======================
var _ graph.CachedContainer = &cacheFolder{}
type cacheFolder struct {
graph.Container
p *path.Builder
}
//=========================================
// 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 // CalendarDisplayable is a transformative struct that aligns
// models.Calendarable interface with the container interface. // models.Calendarable interface with the container interface.
// Calendars do not have a parentFolderID. Therefore, // Calendars do not have a parentFolderID. Therefore,

View File

@ -12,10 +12,10 @@ import (
"github.com/alcionai/corso/src/pkg/path" "github.com/alcionai/corso/src/pkg/path"
) )
var _ graph.ContainerResolver = &contactFolderCache{} var _ graph.ContainerPopulater = &contactFolderCache{}
type contactFolderCache struct { type contactFolderCache struct {
*containerResolver *graph.ContainerCache
gs graph.Service gs graph.Service
userID string userID string
} }
@ -44,12 +44,11 @@ func (cfc *contactFolderCache) populateContactRoot(
"fetching root contact folder: "+support.ConnectorStackErrorTrace(err)) "fetching root contact folder: "+support.ConnectorStackErrorTrace(err))
} }
temp := cacheFolder{ temp := graph.NewCacheFolder(
Container: f, f,
p: path.Builder{}.Append(baseContainerPath...), path.Builder{}.Append(baseContainerPath...))
}
if err := cfc.addFolder(temp); err != nil { if err := cfc.ContainerCache.AddFolder(temp); err != nil {
return errors.Wrap(err, "adding cache root") return errors.Wrap(err, "adding cache root")
} }
@ -105,11 +104,11 @@ func (cfc *contactFolderCache) Populate(
} }
for _, entry := range containers { for _, entry := range containers {
temp := cacheFolder{ temp := graph.CacheFolder{
Container: entry, Container: entry,
} }
err = cfc.addFolder(temp) err = cfc.ContainerCache.AddFolder(temp)
if err != nil { if err != nil {
errs = support.WrapAndAppend( errs = support.WrapAndAppend(
"cache build in cfc.Populate", "cache build in cfc.Populate",
@ -118,7 +117,7 @@ func (cfc *contactFolderCache) Populate(
} }
} }
if err := cfc.populatePaths(ctx); err != nil { if err := cfc.ContainerCache.PopulatePaths(ctx); err != nil {
errs = support.WrapAndAppend( errs = support.WrapAndAppend(
"contacts resolver", "contacts resolver",
err, err,
@ -138,8 +137,8 @@ func (cfc *contactFolderCache) init(
return errors.New("m365 folderID required for base folder") return errors.New("m365 folderID required for base folder")
} }
if cfc.containerResolver == nil { if cfc.ContainerCache == nil {
cfc.containerResolver = newContainerResolver() cfc.ContainerCache = graph.NewContainerCache()
} }
return cfc.populateContactRoot(ctx, baseNode, baseContainerPath) return cfc.populateContactRoot(ctx, baseNode, baseContainerPath)

View File

@ -12,10 +12,10 @@ import (
"github.com/alcionai/corso/src/pkg/path" "github.com/alcionai/corso/src/pkg/path"
) )
var _ graph.ContainerResolver = &eventCalendarCache{} var _ graph.ContainerPopulater = &eventCalendarCache{}
type eventCalendarCache struct { type eventCalendarCache struct {
*containerResolver *graph.ContainerCache
gs graph.Service gs graph.Service
userID string userID string
} }
@ -28,8 +28,8 @@ func (ecc *eventCalendarCache) Populate(
baseID string, baseID string,
baseContainerPath ...string, baseContainerPath ...string,
) error { ) error {
if ecc.containerResolver == nil { if ecc.ContainerCache == nil {
ecc.containerResolver = newContainerResolver() ecc.ContainerCache = graph.NewContainerCache()
} }
options, err := optionsForCalendars([]string{"name"}) options, err := optionsForCalendars([]string{"name"})
@ -76,7 +76,7 @@ func (ecc *eventCalendarCache) Populate(
} }
for _, container := range directories { for _, container := range directories {
if err := checkIDAndName(container); err != nil { if err := graph.CheckIDAndName(container); err != nil {
iterateErr = support.WrapAndAppend( iterateErr = support.WrapAndAppend(
"adding folder to cache", "adding folder to cache",
err, err,
@ -86,12 +86,11 @@ func (ecc *eventCalendarCache) Populate(
continue continue
} }
temp := cacheFolder{ temp := graph.NewCacheFolder(
Container: container, container,
p: path.Builder{}.Append(*container.GetDisplayName()), path.Builder{}.Append(*container.GetDisplayName()))
}
if err := ecc.addFolder(temp); err != nil { if err := ecc.ContainerCache.AddFolder(temp); err != nil {
iterateErr = support.WrapAndAppend( iterateErr = support.WrapAndAppend(
"failure adding "+*container.GetDisplayName(), "failure adding "+*container.GetDisplayName(),
err, err,
@ -104,23 +103,22 @@ func (ecc *eventCalendarCache) Populate(
// AddToCache adds container to map in field 'cache' // AddToCache adds container to map in field 'cache'
// @returns error iff the required values are not accessible. // @returns error iff the required values are not accessible.
func (ecc *eventCalendarCache) AddToCache(ctx context.Context, f graph.Container) error { func (ecc *eventCalendarCache) AddToCache(ctx context.Context, c graph.Container) error {
if err := checkIDAndName(f); err != nil { if err := graph.CheckIDAndName(c); err != nil {
return errors.Wrap(err, "adding cache folder") return errors.Wrap(err, "adding cache folder")
} }
temp := cacheFolder{ temp := graph.NewCacheFolder(
Container: f, c,
p: path.Builder{}.Append(*f.GetDisplayName()), path.Builder{}.Append(*c.GetDisplayName()))
}
if err := ecc.addFolder(temp); err != nil { if err := ecc.ContainerCache.AddFolder(temp); err != nil {
return errors.Wrap(err, "adding cache folder") return errors.Wrap(err, "adding cache folder")
} }
// Populate the path for this entry so calls to PathInCache succeed no matter // Populate the path for this entry so calls to PathInCache succeed no matter
// when they're made. // when they're made.
_, err := ecc.IDToPath(ctx, *f.GetId()) _, err := ecc.ContainerCache.IDToPath(ctx, *c.GetId())
if err != nil { if err != nil {
return errors.Wrap(err, "adding cache entry") return errors.Wrap(err, "adding cache entry")
} }

View File

@ -494,7 +494,7 @@ func (suite *ExchangeServiceSuite) TestGetContainerIDFromCache() {
t = suite.T() t = suite.T()
user = tester.M365UserID(t) user = tester.M365UserID(t)
connector = loadService(t) connector = loadService(t)
directoryCaches = make(map[path.CategoryType]graph.ContainerResolver) directoryCaches = make(map[path.CategoryType]graph.ContainerPopulater)
folderName = tester.DefaultTestRestoreDestination().ContainerName folderName = tester.DefaultTestRestoreDestination().ContainerName
tests = []struct { tests = []struct {
name string name string

View File

@ -50,14 +50,14 @@ func (suite *CacheResolverSuite) TestPopulate() {
ctx, flush := tester.NewContext() ctx, flush := tester.NewContext()
defer flush() defer flush()
eventFunc := func(t *testing.T) graph.ContainerResolver { eventFunc := func(t *testing.T) graph.ContainerPopulater {
return &eventCalendarCache{ return &eventCalendarCache{
userID: tester.M365UserID(t), userID: tester.M365UserID(t),
gs: suite.gs, gs: suite.gs,
} }
} }
contactFunc := func(t *testing.T) graph.ContainerResolver { contactFunc := func(t *testing.T) graph.ContainerPopulater {
return &contactFolderCache{ return &contactFolderCache{
userID: tester.M365UserID(t), userID: tester.M365UserID(t),
gs: suite.gs, gs: suite.gs,
@ -66,7 +66,7 @@ func (suite *CacheResolverSuite) TestPopulate() {
tests := []struct { tests := []struct {
name, folderName, root, basePath string name, folderName, root, basePath string
resolverFunc func(t *testing.T) graph.ContainerResolver resolverFunc func(t *testing.T) graph.ContainerPopulater
canFind assert.BoolAssertionFunc canFind assert.BoolAssertionFunc
}{ }{
{ {

View File

@ -12,13 +12,13 @@ import (
"github.com/alcionai/corso/src/pkg/path" "github.com/alcionai/corso/src/pkg/path"
) )
var _ graph.ContainerResolver = &mailFolderCache{} var _ graph.ContainerPopulater = &mailFolderCache{}
// mailFolderCache struct used to improve lookup of directories within exchange.Mail // mailFolderCache struct used to improve lookup of directories within exchange.Mail
// cache map of cachedContainers where the key = M365ID // cache map of cachedContainers where the key = M365ID
// nameLookup map: Key: DisplayName Value: ID // nameLookup map: Key: DisplayName Value: ID
type mailFolderCache struct { type mailFolderCache struct {
*containerResolver *graph.ContainerCache
gs graph.Service gs graph.Service
userID string userID string
} }
@ -51,12 +51,11 @@ func (mc *mailFolderCache) populateMailRoot(
return errors.Wrap(err, "fetching root folder"+support.ConnectorStackErrorTrace(err)) return errors.Wrap(err, "fetching root folder"+support.ConnectorStackErrorTrace(err))
} }
temp := cacheFolder{ temp := graph.NewCacheFolder(
Container: f, f,
p: path.Builder{}.Append(baseContainerPath...), path.Builder{}.Append(baseContainerPath...))
}
if err := mc.addFolder(temp); err != nil { if err := mc.ContainerCache.AddFolder(temp); err != nil {
return errors.Wrap(err, "initializing mail resolver") return errors.Wrap(err, "initializing mail resolver")
} }
@ -95,14 +94,14 @@ func (mc *mailFolderCache) Populate(
} }
for _, f := range resp.GetValue() { for _, f := range resp.GetValue() {
temp := cacheFolder{ temp := graph.CacheFolder{
Container: f, Container: f,
} }
// Use addFolder instead of AddToCache to be conservative about path // Use addFolder instead of AddToCache to be conservative about path
// population. The fetch order of the folders could cause failures while // population. The fetch order of the folders could cause failures while
// trying to resolve paths, so put it off until we've gotten all folders. // trying to resolve paths, so put it off until we've gotten all folders.
if err := mc.addFolder(temp); err != nil { if err := mc.ContainerCache.AddFolder(temp); err != nil {
errs = multierror.Append(errs, errors.Wrap(err, "delta fetch")) errs = multierror.Append(errs, errors.Wrap(err, "delta fetch"))
continue continue
} }
@ -119,7 +118,7 @@ func (mc *mailFolderCache) Populate(
query = msfolderdelta.NewDeltaRequestBuilder(link, mc.gs.Adapter()) query = msfolderdelta.NewDeltaRequestBuilder(link, mc.gs.Adapter())
} }
if err := mc.populatePaths(ctx); err != nil { if err := mc.ContainerCache.PopulatePaths(ctx); err != nil {
errs = multierror.Append(errs, errors.Wrap(err, "mail resolver")) errs = multierror.Append(errs, errors.Wrap(err, "mail resolver"))
} }
@ -138,8 +137,8 @@ func (mc *mailFolderCache) init(
return errors.New("m365 folder ID required for base folder") return errors.New("m365 folder ID required for base folder")
} }
if mc.containerResolver == nil { if mc.ContainerCache == nil {
mc.containerResolver = newContainerResolver() mc.ContainerCache = graph.NewContainerCache()
} }
return mc.populateMailRoot(ctx, baseNode, baseContainerPath) return mc.populateMailRoot(ctx, baseNode, baseContainerPath)

View File

@ -26,6 +26,7 @@ type exchangeService struct {
///------------------------------------------------------------ ///------------------------------------------------------------
// Functions to comply with graph.Service Interface // Functions to comply with graph.Service Interface
//------------------------------------------------------- //-------------------------------------------------------
func (es *exchangeService) Client() *msgraphsdk.GraphServiceClient { func (es *exchangeService) Client() *msgraphsdk.GraphServiceClient {
return &es.client return &es.client
} }
@ -137,10 +138,11 @@ func GetAllMailFolders(
ctx context.Context, ctx context.Context,
qp graph.QueryParams, qp graph.QueryParams,
gs graph.Service, gs graph.Service,
scope selectors.ExchangeScope,
) ([]graph.CachedContainer, error) { ) ([]graph.CachedContainer, error) {
containers := make([]graph.CachedContainer, 0) containers := make([]graph.CachedContainer, 0)
resolver, err := PopulateExchangeContainerResolver(ctx, qp, path.EmailCategory) resolver, err := PopulateExchangeContainerResolver(ctx, qp)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "building directory resolver in GetAllMailFolders") return nil, errors.Wrap(err, "building directory resolver in GetAllMailFolders")
} }
@ -151,7 +153,7 @@ func GetAllMailFolders(
continue continue
} }
if qp.Scope.Matches(selectors.ExchangeMailFolder, directory) { if scope.Matches(selectors.ExchangeMailFolder, directory) {
containers = append(containers, c) containers = append(containers, c)
} }
} }
@ -166,10 +168,11 @@ func GetAllCalendars(
ctx context.Context, ctx context.Context,
qp graph.QueryParams, qp graph.QueryParams,
gs graph.Service, gs graph.Service,
scope selectors.ExchangeScope,
) ([]graph.CachedContainer, error) { ) ([]graph.CachedContainer, error) {
containers := make([]graph.CachedContainer, 0) containers := make([]graph.CachedContainer, 0)
resolver, err := PopulateExchangeContainerResolver(ctx, qp, path.EventsCategory) resolver, err := PopulateExchangeContainerResolver(ctx, qp)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "building calendar resolver in GetAllCalendars") return nil, errors.Wrap(err, "building calendar resolver in GetAllCalendars")
} }
@ -177,7 +180,7 @@ func GetAllCalendars(
for _, c := range resolver.Items() { for _, c := range resolver.Items() {
directory := c.Path().String() directory := c.Path().String()
if qp.Scope.Matches(selectors.ExchangeEventCalendar, directory) { if scope.Matches(selectors.ExchangeEventCalendar, directory) {
containers = append(containers, c) containers = append(containers, c)
} }
} }
@ -193,12 +196,13 @@ func GetAllContactFolders(
ctx context.Context, ctx context.Context,
qp graph.QueryParams, qp graph.QueryParams,
gs graph.Service, gs graph.Service,
scope selectors.ExchangeScope,
) ([]graph.CachedContainer, error) { ) ([]graph.CachedContainer, error) {
var query string var query string
containers := make([]graph.CachedContainer, 0) containers := make([]graph.CachedContainer, 0)
resolver, err := PopulateExchangeContainerResolver(ctx, qp, path.ContactsCategory) resolver, err := PopulateExchangeContainerResolver(ctx, qp)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "building directory resolver in GetAllContactFolders") return nil, errors.Wrap(err, "building directory resolver in GetAllContactFolders")
} }
@ -212,7 +216,7 @@ func GetAllContactFolders(
query = directory query = directory
} }
if qp.Scope.Matches(selectors.ExchangeContactFolder, query) { if scope.Matches(selectors.ExchangeContactFolder, query) {
containers = append(containers, c) containers = append(containers, c)
} }
} }
@ -220,25 +224,6 @@ func GetAllContactFolders(
return containers, err return containers, err
} }
func GetContainers(
ctx context.Context,
qp graph.QueryParams,
gs graph.Service,
) ([]graph.CachedContainer, error) {
category := qp.Scope.Category().PathType()
switch category {
case path.ContactsCategory:
return GetAllContactFolders(ctx, qp, gs)
case path.EmailCategory:
return GetAllMailFolders(ctx, qp, gs)
case path.EventsCategory:
return GetAllCalendars(ctx, qp, gs)
default:
return nil, fmt.Errorf("path.Category %s not supported", category)
}
}
// PopulateExchangeContainerResolver 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
@ -246,10 +231,9 @@ func GetContainers(
func PopulateExchangeContainerResolver( func PopulateExchangeContainerResolver(
ctx context.Context, ctx context.Context,
qp graph.QueryParams, qp graph.QueryParams,
category path.CategoryType,
) (graph.ContainerResolver, error) { ) (graph.ContainerResolver, error) {
var ( var (
res graph.ContainerResolver res graph.ContainerPopulater
cacheRoot string cacheRoot string
service, err = createService(qp.Credentials, qp.FailFast) service, err = createService(qp.Credentials, qp.FailFast)
) )
@ -258,30 +242,30 @@ func PopulateExchangeContainerResolver(
return nil, err return nil, err
} }
switch category { switch qp.Category {
case path.EmailCategory: case path.EmailCategory:
res = &mailFolderCache{ res = &mailFolderCache{
userID: qp.User, userID: qp.ResourceOwner,
gs: service, gs: service,
} }
cacheRoot = rootFolderAlias cacheRoot = rootFolderAlias
case path.ContactsCategory: case path.ContactsCategory:
res = &contactFolderCache{ res = &contactFolderCache{
userID: qp.User, userID: qp.ResourceOwner,
gs: service, gs: service,
} }
cacheRoot = DefaultContactFolder cacheRoot = DefaultContactFolder
case path.EventsCategory: case path.EventsCategory:
res = &eventCalendarCache{ res = &eventCalendarCache{
userID: qp.User, userID: qp.ResourceOwner,
gs: service, gs: service,
} }
cacheRoot = DefaultCalendar cacheRoot = DefaultCalendar
default: default:
return nil, fmt.Errorf("ContainerResolver not present for %s type", category) return nil, fmt.Errorf("ContainerResolver not present for %s type", qp.Category)
} }
if err := res.Populate(ctx, cacheRoot); err != nil { if err := res.Populate(ctx, cacheRoot); err != nil {
@ -291,8 +275,13 @@ func PopulateExchangeContainerResolver(
return res, nil return res, nil
} }
func pathAndMatch(qp graph.QueryParams, category path.CategoryType, c graph.CachedContainer) (path.Path, bool) { func pathAndMatch(
qp graph.QueryParams,
c graph.CachedContainer,
scope selectors.ExchangeScope,
) (path.Path, bool) {
var ( var (
category = scope.Category().PathType()
directory string directory string
pb = c.Path() pb = c.Path()
) )
@ -304,7 +293,7 @@ func pathAndMatch(qp graph.QueryParams, category path.CategoryType, c graph.Cach
dirPath, err := pb.ToDataLayerExchangePathForCategory( dirPath, err := pb.ToDataLayerExchangePathForCategory(
qp.Credentials.AzureTenantID, qp.Credentials.AzureTenantID,
qp.User, qp.ResourceOwner,
category, category,
false, false,
) )
@ -320,11 +309,11 @@ func pathAndMatch(qp graph.QueryParams, category path.CategoryType, c graph.Cach
switch category { switch category {
case path.EmailCategory: case path.EmailCategory:
return dirPath, qp.Scope.Matches(selectors.ExchangeMailFolder, directory) return dirPath, scope.Matches(selectors.ExchangeMailFolder, directory)
case path.ContactsCategory: case path.ContactsCategory:
return dirPath, qp.Scope.Matches(selectors.ExchangeContactFolder, directory) return dirPath, scope.Matches(selectors.ExchangeContactFolder, directory)
case path.EventsCategory: case path.EventsCategory:
return dirPath, qp.Scope.Matches(selectors.ExchangeEventCalendar, directory) return dirPath, scope.Matches(selectors.ExchangeEventCalendar, directory)
default: default:
return nil, false return nil, false
} }

View File

@ -115,13 +115,14 @@ func (suite *ServiceFunctionsIntegrationSuite) TestGetAllCalendars() {
} }
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) {
scope := test.getScope(t)
params := graph.QueryParams{ params := graph.QueryParams{
User: test.user, Category: scope.Category().PathType(),
Scope: test.getScope(t), ResourceOwner: test.user,
FailFast: false, FailFast: false,
Credentials: suite.creds, Credentials: suite.creds,
} }
cals, err := GetAllCalendars(ctx, params, gs) cals, err := GetAllCalendars(ctx, params, gs, scope)
test.expectErr(t, err) test.expectErr(t, err)
test.expectCount(t, len(cals), 0) test.expectCount(t, len(cals), 0)
}) })
@ -199,13 +200,14 @@ func (suite *ServiceFunctionsIntegrationSuite) TestGetAllContactFolders() {
} }
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) {
scope := test.getScope(t)
params := graph.QueryParams{ params := graph.QueryParams{
User: test.user, Category: scope.Category().PathType(),
Scope: test.getScope(t), ResourceOwner: test.user,
FailFast: false, FailFast: false,
Credentials: suite.creds, Credentials: suite.creds,
} }
cals, err := GetAllContactFolders(ctx, params, gs) cals, err := GetAllContactFolders(ctx, params, gs, scope)
test.expectErr(t, err) test.expectErr(t, err)
test.expectCount(t, len(cals), 0) test.expectCount(t, len(cals), 0)
}) })
@ -283,84 +285,16 @@ func (suite *ServiceFunctionsIntegrationSuite) TestGetAllMailFolders() {
} }
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) {
scope := test.getScope(t)
params := graph.QueryParams{ params := graph.QueryParams{
User: test.user, Category: scope.Category().PathType(),
Scope: test.getScope(t), ResourceOwner: test.user,
FailFast: false, FailFast: false,
Credentials: suite.creds, Credentials: suite.creds,
} }
cals, err := GetAllMailFolders(ctx, params, gs) cals, err := GetAllMailFolders(ctx, params, gs, scope)
test.expectErr(t, err) test.expectErr(t, err)
test.expectCount(t, len(cals), 0) test.expectCount(t, len(cals), 0)
}) })
} }
} }
func (suite *ServiceFunctionsIntegrationSuite) TestCollectContainers() {
ctx, flush := tester.NewContext()
defer flush()
failFast := false
containerCount := 1
t := suite.T()
user := tester.M365UserID(t)
a := tester.NewM365Account(t)
service := loadService(t)
credentials, err := a.M365Config()
require.NoError(t, err)
tests := []struct {
name, contains string
getScope func() selectors.ExchangeScope
expectedCount assert.ComparisonAssertionFunc
}{
{
name: "All Events",
contains: "Birthdays",
expectedCount: assert.Greater,
getScope: func() selectors.ExchangeScope {
return selectors.
NewExchangeBackup().
EventCalendars([]string{user}, selectors.Any())[0]
},
}, {
name: "Default Calendar",
contains: DefaultCalendar,
expectedCount: assert.Equal,
getScope: func() selectors.ExchangeScope {
return selectors.
NewExchangeBackup().
EventCalendars([]string{user}, []string{DefaultCalendar}, selectors.PrefixMatch())[0]
},
}, {
name: "Default Mail",
contains: DefaultMailFolder,
expectedCount: assert.Equal,
getScope: func() selectors.ExchangeScope {
return selectors.
NewExchangeBackup().
MailFolders([]string{user}, []string{DefaultMailFolder}, selectors.PrefixMatch())[0]
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
qp := graph.QueryParams{
User: user,
Scope: test.getScope(),
FailFast: failFast,
Credentials: credentials,
}
collections, err := GetContainers(ctx, qp, service)
assert.NoError(t, err)
test.expectedCount(t, len(collections), containerCount)
keys := make([]string, 0, len(collections))
for _, k := range collections {
keys = append(keys, *k.GetDisplayName())
}
assert.Contains(t, keys, test.contains)
})
}
}

View File

@ -13,6 +13,7 @@ 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"
) )
// FilterContainersAndFillCollections is a utility function // FilterContainersAndFillCollections is a utility function
@ -26,21 +27,21 @@ func FilterContainersAndFillCollections(
collections map[string]*Collection, collections map[string]*Collection,
statusUpdater support.StatusUpdater, statusUpdater support.StatusUpdater,
resolver graph.ContainerResolver, resolver graph.ContainerResolver,
scope selectors.ExchangeScope,
) error { ) error {
var ( var (
category = qp.Scope.Category().PathType() collectionType = CategoryToOptionIdentifier(scope.Category().PathType())
collectionType = CategoryToOptionIdentifier(category)
errs error errs error
) )
for _, c := range resolver.Items() { for _, c := range resolver.Items() {
dirPath, ok := pathAndMatch(qp, category, c) dirPath, ok := pathAndMatch(qp, c, scope)
if ok { if ok {
// Create only those that match // 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 {
errs = support.WrapAndAppend( errs = support.WrapAndAppend(
qp.User+" FilterContainerAndFillCollection", qp.ResourceOwner+" FilterContainerAndFillCollection",
err, err,
errs) errs)
@ -50,7 +51,7 @@ func FilterContainersAndFillCollections(
} }
edc := NewCollection( edc := NewCollection(
qp.User, qp.ResourceOwner,
dirPath, dirPath,
collectionType, collectionType,
service, service,
@ -61,10 +62,10 @@ func FilterContainersAndFillCollections(
} }
for directoryID, col := range collections { for directoryID, col := range collections {
fetchFunc, err := getFetchIDFunc(category) fetchFunc, err := getFetchIDFunc(scope.Category().PathType())
if err != nil { if err != nil {
errs = support.WrapAndAppend( errs = support.WrapAndAppend(
qp.User, qp.ResourceOwner,
err, err,
errs) errs)
@ -75,10 +76,10 @@ func FilterContainersAndFillCollections(
continue continue
} }
jobs, err := fetchFunc(ctx, col.service, qp.User, directoryID) jobs, err := fetchFunc(ctx, col.service, qp.ResourceOwner, directoryID)
if err != nil { if err != nil {
errs = support.WrapAndAppend( errs = support.WrapAndAppend(
qp.User, qp.ResourceOwner,
err, err,
errs, errs,
) )

View File

@ -292,7 +292,7 @@ func RestoreExchangeDataCollections(
) (*support.ConnectorOperationStatus, error) { ) (*support.ConnectorOperationStatus, error) {
var ( var (
// map of caches... but not yet... // map of caches... but not yet...
directoryCaches = make(map[string]map[path.CategoryType]graph.ContainerResolver) directoryCaches = make(map[string]map[path.CategoryType]graph.ContainerPopulater)
metrics support.CollectionMetrics metrics support.CollectionMetrics
errs error errs error
// TODO policy to be updated from external source after completion of refactoring // TODO policy to be updated from external source after completion of refactoring
@ -308,7 +308,7 @@ func RestoreExchangeDataCollections(
userCaches := directoryCaches[userID] userCaches := directoryCaches[userID]
if userCaches == nil { if userCaches == nil {
directoryCaches[userID] = make(map[path.CategoryType]graph.ContainerResolver) directoryCaches[userID] = make(map[path.CategoryType]graph.ContainerPopulater)
userCaches = directoryCaches[userID] userCaches = directoryCaches[userID]
} }
@ -432,7 +432,7 @@ func GetContainerIDFromCache(
gs graph.Service, gs graph.Service,
directory path.Path, directory path.Path,
destination string, destination string,
caches map[path.CategoryType]graph.ContainerResolver, caches map[path.CategoryType]graph.ContainerPopulater,
) (string, error) { ) (string, error) {
var ( var (
newCache = false newCache = false
@ -512,7 +512,7 @@ func GetContainerIDFromCache(
func establishMailRestoreLocation( func establishMailRestoreLocation(
ctx context.Context, ctx context.Context,
folders []string, folders []string,
mfc graph.ContainerResolver, mfc graph.ContainerPopulater,
user string, user string,
service graph.Service, service graph.Service,
isNewCache bool, isNewCache bool,
@ -569,7 +569,7 @@ func establishMailRestoreLocation(
func establishContactsRestoreLocation( func establishContactsRestoreLocation(
ctx context.Context, ctx context.Context,
folders []string, folders []string,
cfc graph.ContainerResolver, cfc graph.ContainerPopulater,
user string, user string,
gs graph.Service, gs graph.Service,
isNewCache bool, isNewCache bool,
@ -602,7 +602,7 @@ func establishContactsRestoreLocation(
func establishEventsRestoreLocation( func establishEventsRestoreLocation(
ctx context.Context, ctx context.Context,
folders []string, folders []string,
ecc graph.ContainerResolver, // eventCalendarCache ecc graph.ContainerPopulater, // eventCalendarCache
user string, user string,
gs graph.Service, gs graph.Service,
isNewCache bool, isNewCache bool,

View File

@ -1,7 +1,6 @@
package graph package graph
import ( import (
"github.com/microsoftgraph/msgraph-sdk-go/models"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/alcionai/corso/src/pkg/path" "github.com/alcionai/corso/src/pkg/path"
@ -16,9 +15,9 @@ type CachedContainer interface {
SetPath(*path.Builder) SetPath(*path.Builder)
} }
// checkRequiredValues is a helper function to ensure that // CheckIDAndName is a helper function to ensure that
// all the pointers are set prior to being called. // the ID and name pointers are set prior to being called.
func CheckRequiredValues(c Container) error { func CheckIDAndName(c Container) error {
idPtr := c.GetId() idPtr := c.GetId()
if idPtr == nil || len(*idPtr) == 0 { if idPtr == nil || len(*idPtr) == 0 {
return errors.New("folder without ID") return errors.New("folder without ID")
@ -29,9 +28,19 @@ func CheckRequiredValues(c Container) error {
return errors.Errorf("folder %s without display name", *idPtr) return errors.Errorf("folder %s without display name", *idPtr)
} }
ptr = c.GetParentFolderId() return nil
}
// CheckRequiredValues is a helper function to ensure that
// all the pointers are set prior to being called.
func CheckRequiredValues(c Container) error {
if err := CheckIDAndName(c); err != nil {
return err
}
ptr := c.GetParentFolderId()
if ptr == nil || len(*ptr) == 0 { if ptr == nil || len(*ptr) == 0 {
return errors.Errorf("folder %s without parent ID", *idPtr) return errors.Errorf("folder %s without parent ID", *c.GetId())
} }
return nil return nil
@ -58,10 +67,6 @@ func NewCacheFolder(c Container, pb *path.Builder) CacheFolder {
return cf return cf
} }
//=========================================
// Required Functions to satisfy interfaces
//=========================================
func (cf CacheFolder) Path() *path.Builder { func (cf CacheFolder) Path() *path.Builder {
return cf.p return cf.p
} }
@ -69,41 +74,3 @@ func (cf CacheFolder) Path() *path.Builder {
func (cf *CacheFolder) SetPath(newPath *path.Builder) { func (cf *CacheFolder) SetPath(newPath *path.Builder) {
cf.p = newPath 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

@ -1,4 +1,4 @@
package exchange package graph
import ( import (
"context" "context"
@ -6,21 +6,28 @@ import (
"github.com/hashicorp/go-multierror" "github.com/hashicorp/go-multierror"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/alcionai/corso/src/internal/connector/graph"
"github.com/alcionai/corso/src/pkg/path" "github.com/alcionai/corso/src/pkg/path"
) )
func newContainerResolver() *containerResolver { var _ ContainerResolver = &ContainerCache{}
return &containerResolver{
cache: map[string]graph.CachedContainer{}, type populatorFunc func(
ctx context.Context,
baseID string,
baseContainerPath ...string,
)
type ContainerCache struct {
cache map[string]CachedContainer
}
func NewContainerCache() *ContainerCache {
return &ContainerCache{
cache: map[string]CachedContainer{},
} }
} }
type containerResolver struct { func (cr *ContainerCache) IDToPath(
cache map[string]graph.CachedContainer
}
func (cr *containerResolver) IDToPath(
ctx context.Context, ctx context.Context,
folderID string, folderID string,
) (*path.Builder, error) { ) (*path.Builder, error) {
@ -48,7 +55,7 @@ func (cr *containerResolver) IDToPath(
// PathInCache utility function to return m365ID of folder if the pathString // PathInCache utility function to return m365ID of folder if the pathString
// matches the path of a container within the cache. A boolean function // matches the path of a container within the cache. A boolean function
// accompanies the call to indicate whether the lookup was successful. // accompanies the call to indicate whether the lookup was successful.
func (cr *containerResolver) PathInCache(pathString string) (string, bool) { func (cr *ContainerCache) PathInCache(pathString string) (string, bool) {
if len(pathString) == 0 || cr == nil { if len(pathString) == 0 || cr == nil {
return "", false return "", false
} }
@ -66,17 +73,17 @@ func (cr *containerResolver) PathInCache(pathString string) (string, bool) {
return "", false return "", false
} }
// addFolder adds a folder to the cache with the given ID. If the item is // AddFolder adds a folder to the cache with the given ID. If the item is
// already in the cache does nothing. The path for the item is not modified. // already in the cache does nothing. The path for the item is not modified.
func (cr *containerResolver) addFolder(cf cacheFolder) error { func (cr *ContainerCache) AddFolder(cf CacheFolder) error {
// Only require a non-nil non-empty parent if the path isn't already // Only require a non-nil non-empty parent if the path isn't already
// populated. // populated.
if cf.p != nil { if cf.p != nil {
if err := checkIDAndName(cf.Container); err != nil { if err := CheckIDAndName(cf.Container); err != nil {
return errors.Wrap(err, "adding item to cache") return errors.Wrap(err, "adding item to cache")
} }
} else { } else {
if err := checkRequiredValues(cf.Container); err != nil { if err := CheckRequiredValues(cf.Container); err != nil {
return errors.Wrap(err, "adding item to cache") return errors.Wrap(err, "adding item to cache")
} }
} }
@ -90,8 +97,9 @@ func (cr *containerResolver) addFolder(cf cacheFolder) error {
return nil return nil
} }
func (cr *containerResolver) Items() []graph.CachedContainer { // Items returns the list of Containers in the cache.
res := make([]graph.CachedContainer, 0, len(cr.cache)) func (cr *ContainerCache) Items() []CachedContainer {
res := make([]CachedContainer, 0, len(cr.cache))
for _, c := range cr.cache { for _, c := range cr.cache {
res = append(res, c) res = append(res, c)
@ -102,12 +110,12 @@ func (cr *containerResolver) Items() []graph.CachedContainer {
// AddToCache adds container to map in field 'cache' // AddToCache adds container to map in field 'cache'
// @returns error iff the required values are not accessible. // @returns error iff the required values are not accessible.
func (cr *containerResolver) AddToCache(ctx context.Context, f graph.Container) error { func (cr *ContainerCache) AddToCache(ctx context.Context, f Container) error {
temp := cacheFolder{ temp := CacheFolder{
Container: f, Container: f,
} }
if err := cr.addFolder(temp); err != nil { if err := cr.AddFolder(temp); err != nil {
return errors.Wrap(err, "adding cache folder") return errors.Wrap(err, "adding cache folder")
} }
@ -121,10 +129,10 @@ func (cr *containerResolver) AddToCache(ctx context.Context, f graph.Container)
return nil return nil
} }
func (cr *containerResolver) populatePaths(ctx context.Context) error { // PopulatePaths ensures that all items in the cache can construct valid paths.
func (cr *ContainerCache) PopulatePaths(ctx context.Context) error {
var errs *multierror.Error var errs *multierror.Error
// Populate all folder paths.
for _, f := range cr.Items() { for _, f := range cr.Items() {
_, err := cr.IDToPath(ctx, *f.GetId()) _, err := cr.IDToPath(ctx, *f.GetId())
if err != nil { if err != nil {

View File

@ -7,12 +7,11 @@ import (
"github.com/alcionai/corso/src/pkg/account" "github.com/alcionai/corso/src/pkg/account"
"github.com/alcionai/corso/src/pkg/path" "github.com/alcionai/corso/src/pkg/path"
"github.com/alcionai/corso/src/pkg/selectors"
) )
type QueryParams struct { type QueryParams struct {
User string Category path.CategoryType
Scope selectors.ExchangeScope ResourceOwner string
Credentials account.M365Config Credentials account.M365Config
FailFast bool FailFast bool
} }
@ -61,11 +60,6 @@ type ContainerResolver interface {
// to that container. The path has a similar format to paths on the local // to that container. The path has a similar format to paths on the local
// file system. // file system.
IDToPath(ctx context.Context, m365ID string) (*path.Builder, error) IDToPath(ctx context.Context, m365ID string) (*path.Builder, error)
// Populate performs initialization steps for the resolver
// @param ctx is necessary param for Graph API tracing
// @param baseFolderID represents the M365ID base that the resolver will
// conclude its search. Default input is "".
Populate(ctx context.Context, baseFolderID string, baseContainerPather ...string) error
// PathInCache performs a look up of a path reprensentation // PathInCache performs a look up of a path reprensentation
// and returns the m365ID of directory iff the pathString // and returns the m365ID of directory iff the pathString
@ -78,3 +72,16 @@ type ContainerResolver interface {
// Items returns the containers in the cache. // Items returns the containers in the cache.
Items() []CachedContainer Items() []CachedContainer
} }
// ContainerPopulater houses functions for populating and retrieving info
// about containers from remote APIs (i.e. resolve folder paths with Graph
// API). Populaters may cache information about containers.
type ContainerPopulater interface {
ContainerResolver
// Populate performs initialization steps for the populater
// @param ctx is necessary param for Graph API tracing
// @param baseFolderID represents the M365ID base that the
// populater will conclude its search. Default input is "".
Populate(ctx context.Context, baseFolderID string, baseContainerPather ...string) error
}

View File

@ -6,10 +6,8 @@ import (
"context" "context"
"fmt" "fmt"
"runtime/trace" "runtime/trace"
"strings"
"sync" "sync"
"github.com/hashicorp/go-multierror"
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"
@ -21,14 +19,16 @@ import (
"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"
D "github.com/alcionai/corso/src/internal/diagnostics" D "github.com/alcionai/corso/src/internal/diagnostics"
"github.com/alcionai/corso/src/internal/observe"
"github.com/alcionai/corso/src/pkg/account" "github.com/alcionai/corso/src/pkg/account"
"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/selectors" "github.com/alcionai/corso/src/pkg/selectors"
) )
// ---------------------------------------------------------------------------
// Graph Connector
// ---------------------------------------------------------------------------
// GraphConnector is a struct used to wrap the GraphServiceClient and // GraphConnector is a struct used to wrap the GraphServiceClient and
// GraphRequestAdapter from the msgraph-sdk-go. Additional fields are for // GraphRequestAdapter from the msgraph-sdk-go. Additional fields are for
// bookkeeping and interfacing with other component. // bookkeeping and interfacing with other component.
@ -36,6 +36,7 @@ type GraphConnector struct {
graphService graphService
tenant string tenant string
Users map[string]string // key<email> value<id> Users map[string]string // key<email> value<id>
Sites map[string]string // key<???> value<???>
credentials account.M365Config credentials account.M365Config
// wg is used to track completion of GC tasks // wg is used to track completion of GC tasks
@ -97,6 +98,12 @@ func NewGraphConnector(ctx context.Context, acct account.Account) (*GraphConnect
return nil, errors.Wrap(err, "retrieving tenant user list") return nil, errors.Wrap(err, "retrieving tenant user list")
} }
// TODO: users or sites, one or the other, not both.
err = gc.setTenantSites(ctx)
if err != nil {
return nil, errors.Wrap(err, "retrieveing tenant site list")
}
return &gc, nil return &gc, nil
} }
@ -126,7 +133,7 @@ func (gs *graphService) EnableFailFast() {
// setTenantUsers queries the M365 to identify the users in the // setTenantUsers queries the M365 to identify the users in the
// workspace. The users field is updated during this method // workspace. The users field is updated during this method
// iff the return value is true // iff the return value is nil
func (gc *GraphConnector) setTenantUsers(ctx context.Context) error { func (gc *GraphConnector) setTenantUsers(ctx context.Context) error {
ctx, end := D.Span(ctx, "gc:setTenantUsers") ctx, end := D.Span(ctx, "gc:setTenantUsers")
defer end() defer end()
@ -191,6 +198,79 @@ func (gc *GraphConnector) GetUsersIds() []string {
return buildFromMap(false, gc.Users) return buildFromMap(false, gc.Users)
} }
// setTenantSites queries the M365 to identify the sites in the
// workspace. The sitets field is updated during this method
// iff the return value is nil
func (gc *GraphConnector) setTenantSites(ctx context.Context) error {
// TODO
gc.Sites = map[string]string{}
// ctx, end := D.Span(ctx, "gc:setTenantSites")
// defer end()
// response, err := exchange.GetAllUsersForTenant(ctx, gc.graphService, "")
// if err != nil {
// return errors.Wrapf(
// err,
// "tenant %s M365 query: %s",
// gc.tenant,
// support.ConnectorStackErrorTrace(err),
// )
// }
// userIterator, err := msgraphgocore.NewPageIterator(
// response,
// &gc.graphService.adapter,
// models.CreateUserCollectionResponseFromDiscriminatorValue,
// )
// if err != nil {
// return errors.Wrap(err, support.ConnectorStackErrorTrace(err))
// }
// callbackFunc := func(userItem interface{}) bool {
// user, ok := userItem.(models.Userable)
// if !ok {
// err = support.WrapAndAppend(gc.graphService.adapter.GetBaseUrl(),
// errors.New("received non-User on iteration"), err)
// return true
// }
// if user.GetUserPrincipalName() == nil {
// err = support.WrapAndAppend(
// gc.graphService.adapter.GetBaseUrl(),
// fmt.Errorf("no email address for User: %s", *user.GetId()),
// err,
// )
// return true
// }
// // *user.GetId() is populated for every M365 entityable object by M365 backstore
// gc.Users[*user.GetUserPrincipalName()] = *user.GetId()
// return true
// }
// iterateError := userIterator.Iterate(ctx, callbackFunc)
// if iterateError != nil {
// err = support.WrapAndAppend(gc.graphService.adapter.GetBaseUrl(), iterateError, err)
// }
// return err
return nil
}
// GetSites returns the siteIDs of sharepoint sites within tenant.
func (gc *GraphConnector) GetSites() []string {
return buildFromMap(true, gc.Sites)
}
// GetSiteIds returns the M365 id for the user
func (gc *GraphConnector) GetSiteIds() []string {
return buildFromMap(false, gc.Sites)
}
// buildFromMap helper function for returning []string from map. // buildFromMap helper function for returning []string from map.
// Returns list of keys iff true; otherwise returns a list of values // Returns list of keys iff true; otherwise returns a list of values
func buildFromMap(isKey bool, mapping map[string]string) []string { func buildFromMap(isKey bool, mapping map[string]string) []string {
@ -209,42 +289,6 @@ func buildFromMap(isKey bool, mapping map[string]string) []string {
return returnString return returnString
} }
// ExchangeDataStream returns a DataCollection which the caller can
// use to read mailbox data out for the specified user
// Assumption: User exists
//
// Add iota to this call -> mail, contacts, calendar, etc.
func (gc *GraphConnector) ExchangeDataCollection(
ctx context.Context,
selector selectors.Selector,
) ([]data.Collection, error) {
eb, err := selector.ToExchangeBackup()
if err != nil {
return nil, errors.Wrap(err, "exchangeDataCollection: parsing selector")
}
var (
scopes = eb.DiscreteScopes(gc.GetUsers())
collections = []data.Collection{}
errs error
)
for _, scope := range scopes {
// Creates a map of collections based on scope
dcs, err := gc.createCollections(ctx, scope)
if err != nil {
user := scope.Get(selectors.ExchangeUser)
return nil, support.WrapAndAppend(user[0], err, errs)
}
for _, collection := range dcs {
collections = append(collections, collection)
}
}
return collections, errs
}
// RestoreDataCollections restores data from the specified collections // RestoreDataCollections restores data from the specified collections
// into M365 using the GraphAPI. // into M365 using the GraphAPI.
// SideEffect: gc.status is updated at the completion of operation // SideEffect: gc.status is updated at the completion of operation
@ -278,67 +322,6 @@ func (gc *GraphConnector) RestoreDataCollections(
return deets, err return deets, err
} }
// createCollections - utility function that retrieves M365
// IDs through Microsoft Graph API. The selectors.ExchangeScope
// determines the type of collections that are stored.
// to the GraphConnector struct.
func (gc *GraphConnector) createCollections(
ctx context.Context,
scope selectors.ExchangeScope,
) ([]*exchange.Collection, error) {
var errs *multierror.Error
users := scope.Get(selectors.ExchangeUser)
allCollections := make([]*exchange.Collection, 0)
// Create collection of ExchangeDataCollection
for _, user := range users {
collections := make(map[string]*exchange.Collection)
qp := graph.QueryParams{
User: user,
Scope: scope,
FailFast: gc.failFast,
Credentials: gc.credentials,
}
itemCategory := qp.Scope.Category().PathType()
foldersComplete, closer := observe.MessageWithCompletion(fmt.Sprintf("∙ %s - %s:", itemCategory.String(), user))
defer closer()
defer close(foldersComplete)
resolver, err := exchange.PopulateExchangeContainerResolver(
ctx,
qp,
qp.Scope.Category().PathType(),
)
if err != nil {
return nil, errors.Wrap(err, "getting folder cache")
}
err = exchange.FilterContainersAndFillCollections(
ctx,
qp,
collections,
gc.UpdateStatus,
resolver)
if err != nil {
return nil, errors.Wrap(err, "filling collections")
}
foldersComplete <- struct{}{}
for _, collection := range collections {
gc.incrementAwaitingMessages()
allCollections = append(allCollections, collection)
}
}
return allCollections, errs.ErrorOrNil()
}
// AwaitStatus waits for all gc tasks to complete and then returns status // AwaitStatus waits for all gc tasks to complete and then returns status
func (gc *GraphConnector) AwaitStatus() *support.ConnectorOperationStatus { func (gc *GraphConnector) AwaitStatus() *support.ConnectorOperationStatus {
defer func() { defer func() {
@ -378,6 +361,10 @@ func (gc *GraphConnector) incrementAwaitingMessages() {
gc.wg.Add(1) gc.wg.Add(1)
} }
// ---------------------------------------------------------------------------
// Helper Funcs
// ---------------------------------------------------------------------------
// IsRecoverableError returns true iff error is a RecoverableGCEerror // IsRecoverableError returns true iff error is a RecoverableGCEerror
func IsRecoverableError(e error) bool { func IsRecoverableError(e error) bool {
var recoverable support.RecoverableGCError var recoverable support.RecoverableGCError
@ -389,113 +376,3 @@ func IsNonRecoverableError(e error) bool {
var nonRecoverable support.NonRecoverableGCError var nonRecoverable support.NonRecoverableGCError
return errors.As(e, &nonRecoverable) return errors.As(e, &nonRecoverable)
} }
// DataCollections utility function to launch backup operations for exchange and onedrive
func (gc *GraphConnector) DataCollections(ctx context.Context, sels selectors.Selector) ([]data.Collection, error) {
ctx, end := D.Span(ctx, "gc:dataCollections", D.Index("service", sels.Service.String()))
defer end()
err := verifyBackupInputs(sels, gc.Users)
if err != nil {
return nil, err
}
switch sels.Service {
case selectors.ServiceExchange:
return gc.ExchangeDataCollection(ctx, sels)
case selectors.ServiceOneDrive:
return gc.OneDriveDataCollections(ctx, sels)
default:
return nil, errors.Errorf("service %s not supported", sels)
}
}
// OneDriveDataCollections returns a set of DataCollection which represents the OneDrive data
// for the specified user
func (gc *GraphConnector) OneDriveDataCollections(
ctx context.Context,
selector selectors.Selector,
) ([]data.Collection, error) {
odb, err := selector.ToOneDriveBackup()
if err != nil {
return nil, errors.Wrap(err, "oneDriveDataCollection: parsing selector")
}
collections := []data.Collection{}
scopes := odb.DiscreteScopes(gc.GetUsers())
var errs error
// for each scope that includes oneDrive items, get all
for _, scope := range scopes {
for _, user := range scope.Get(selectors.OneDriveUser) {
logger.Ctx(ctx).With("user", user).Debug("Creating OneDrive collections")
odcs, err := onedrive.NewCollections(
gc.credentials.AzureTenantID,
user,
scope,
&gc.graphService,
gc.UpdateStatus,
).Get(ctx)
if err != nil {
return nil, support.WrapAndAppend(user, err, errs)
}
collections = append(collections, odcs...)
}
}
for range collections {
gc.incrementAwaitingMessages()
}
return collections, errs
}
func verifyBackupInputs(sel selectors.Selector, mapOfUsers map[string]string) error {
var personnel []string
// retrieve users from selectors
switch sel.Service {
case selectors.ServiceExchange:
backup, err := sel.ToExchangeBackup()
if err != nil {
return err
}
for _, scope := range backup.Scopes() {
temp := scope.Get(selectors.ExchangeUser)
personnel = append(personnel, temp...)
}
case selectors.ServiceOneDrive:
backup, err := sel.ToOneDriveBackup()
if err != nil {
return err
}
for _, user := range backup.Scopes() {
temp := user.Get(selectors.OneDriveUser)
personnel = append(personnel, temp...)
}
default:
return errors.New("service %s not supported")
}
// verify personnel
normUsers := map[string]struct{}{}
for k := range mapOfUsers {
normUsers[strings.ToLower(k)] = struct{}{}
}
for _, user := range personnel {
if _, ok := normUsers[strings.ToLower(user)]; !ok {
return fmt.Errorf("%s user not found within tenant", user)
}
}
return nil
}

View File

@ -1,6 +1,7 @@
package connector package connector
import ( import (
"context"
"io" "io"
"reflect" "reflect"
"testing" "testing"
@ -13,6 +14,7 @@ import (
"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"
"github.com/alcionai/corso/src/internal/tester"
"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" "github.com/alcionai/corso/src/pkg/selectors"
@ -878,3 +880,11 @@ func getSelectorWith(service path.ServiceType) selectors.Selector {
Service: s, Service: s,
} }
} }
func loadConnector(ctx context.Context, t *testing.T) *GraphConnector {
a := tester.NewM365Account(t)
connector, err := NewGraphConnector(ctx, a)
require.NoError(t, err)
return connector
}

View File

@ -1,8 +1,6 @@
package connector package connector
import ( import (
"bytes"
"context"
"testing" "testing"
"time" "time"
@ -10,9 +8,7 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"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/mockconnector" "github.com/alcionai/corso/src/internal/connector/mockconnector"
"github.com/alcionai/corso/src/internal/connector/support"
"github.com/alcionai/corso/src/internal/data" "github.com/alcionai/corso/src/internal/data"
"github.com/alcionai/corso/src/internal/tester" "github.com/alcionai/corso/src/internal/tester"
"github.com/alcionai/corso/src/pkg/control" "github.com/alcionai/corso/src/pkg/control"
@ -26,14 +22,6 @@ type GraphConnectorIntegrationSuite struct {
user string user string
} }
func loadConnector(ctx context.Context, t *testing.T) *GraphConnector {
a := tester.NewM365Account(t)
connector, err := NewGraphConnector(ctx, a)
require.NoError(t, err)
return connector
}
func TestGraphConnectorIntegrationSuite(t *testing.T) { func TestGraphConnectorIntegrationSuite(t *testing.T) {
if err := tester.RunOnAny( if err := tester.RunOnAny(
tester.CorsoCITests, tester.CorsoCITests,
@ -80,338 +68,31 @@ func (suite *GraphConnectorIntegrationSuite) TestSetTenantUsers() {
suite.Greater(len(newConnector.Users), 0) suite.Greater(len(newConnector.Users), 0)
} }
// TestInvalidUserForDataCollections ensures verification process for users // TestSetTenantUsers verifies GraphConnector's ability to query
func (suite *GraphConnectorIntegrationSuite) TestInvalidUserForDataCollections() { // the sites associated with the credentials
func (suite *GraphConnectorIntegrationSuite) TestSetTenantSites() {
newConnector := GraphConnector{
tenant: "test_tenant",
Sites: make(map[string]string, 0),
credentials: suite.connector.credentials,
}
ctx, flush := tester.NewContext() ctx, flush := tester.NewContext()
defer flush() defer flush()
invalidUser := "foo@example.com" service, err := newConnector.createService(false)
connector := loadConnector(ctx, suite.T()) require.NoError(suite.T(), err)
tests := []struct {
name string newConnector.graphService = *service
getSelector func(t *testing.T) selectors.Selector
}{ suite.Equal(0, len(newConnector.Sites))
{ err = newConnector.setTenantSites(ctx)
name: "invalid exchange backup user", assert.NoError(suite.T(), err)
getSelector: func(t *testing.T) selectors.Selector { // TODO: should be non-zero once implemented.
sel := selectors.NewExchangeBackup() // suite.Greater(len(newConnector.Users), 0)
sel.Include(sel.MailFolders([]string{invalidUser}, selectors.Any())) suite.Equal(0, len(newConnector.Sites))
return sel.Selector
},
},
{
name: "Invalid onedrive backup user",
getSelector: func(t *testing.T) selectors.Selector {
sel := selectors.NewOneDriveBackup()
sel.Include(sel.Folders([]string{invalidUser}, selectors.Any()))
return sel.Selector
},
},
} }
for _, test := range tests {
suite.T().Run(test.name, func(t *testing.T) {
collections, err := connector.DataCollections(ctx, test.getSelector(t))
assert.Error(t, err)
assert.Empty(t, collections)
})
}
}
// TestExchangeDataCollection verifies interface between operation and
// GraphConnector remains stable to receive a non-zero amount of Collections
// for the Exchange Package. Enabled exchange applications:
// - mail
// - contacts
// - events
func (suite *GraphConnectorIntegrationSuite) TestExchangeDataCollection() {
ctx, flush := tester.NewContext()
defer flush()
connector := loadConnector(ctx, suite.T())
tests := []struct {
name string
getSelector func(t *testing.T) selectors.Selector
}{
{
name: suite.user + " Email",
getSelector: func(t *testing.T) selectors.Selector {
sel := selectors.NewExchangeBackup()
sel.Include(sel.MailFolders([]string{suite.user}, []string{exchange.DefaultMailFolder}, selectors.PrefixMatch()))
return sel.Selector
},
},
{
name: suite.user + " Contacts",
getSelector: func(t *testing.T) selectors.Selector {
sel := selectors.NewExchangeBackup()
sel.Include(sel.ContactFolders(
[]string{suite.user},
[]string{exchange.DefaultContactFolder},
selectors.PrefixMatch()))
return sel.Selector
},
},
{
name: suite.user + " Events",
getSelector: func(t *testing.T) selectors.Selector {
sel := selectors.NewExchangeBackup()
sel.Include(sel.EventCalendars([]string{suite.user}, []string{exchange.DefaultCalendar}, selectors.PrefixMatch()))
return sel.Selector
},
},
}
for _, test := range tests {
suite.T().Run(test.name, func(t *testing.T) {
collection, err := connector.ExchangeDataCollection(ctx, test.getSelector(t))
require.NoError(t, err)
assert.Equal(t, len(collection), 1)
channel := collection[0].Items()
for object := range channel {
buf := &bytes.Buffer{}
_, err := buf.ReadFrom(object.ToReader())
assert.NoError(t, err, "received a buf.Read error")
}
status := connector.AwaitStatus()
assert.NotZero(t, status.Successful)
t.Log(status.String())
})
}
}
// TestMailSerializationRegression verifies that all mail data stored in the
// test account can be successfully downloaded into bytes and restored into
// M365 mail objects
func (suite *GraphConnectorIntegrationSuite) TestMailSerializationRegression() {
ctx, flush := tester.NewContext()
defer flush()
t := suite.T()
connector := loadConnector(ctx, t)
sel := selectors.NewExchangeBackup()
sel.Include(sel.MailFolders([]string{suite.user}, []string{exchange.DefaultMailFolder}, selectors.PrefixMatch()))
collection, err := connector.createCollections(ctx, sel.Scopes()[0])
require.NoError(t, err)
for _, edc := range collection {
suite.T().Run(edc.FullPath().String(), func(t *testing.T) {
streamChannel := edc.Items()
// Verify that each message can be restored
for stream := range streamChannel {
buf := &bytes.Buffer{}
read, err := buf.ReadFrom(stream.ToReader())
assert.NoError(t, err)
assert.NotZero(t, read)
message, err := support.CreateMessageFromBytes(buf.Bytes())
assert.NotNil(t, message)
assert.NoError(t, err)
}
})
}
status := connector.AwaitStatus()
suite.NotNil(status)
suite.Equal(status.ObjectCount, status.Successful)
}
// TestContactSerializationRegression verifies ability to query contact items
// and to store contact within Collection. Downloaded contacts are run through
// a regression test to ensure that downloaded items can be uploaded.
func (suite *GraphConnectorIntegrationSuite) TestContactSerializationRegression() {
ctx, flush := tester.NewContext()
defer flush()
connector := loadConnector(ctx, suite.T())
tests := []struct {
name string
getCollection func(t *testing.T) []*exchange.Collection
}{
{
name: "Default Contact Folder",
getCollection: func(t *testing.T) []*exchange.Collection {
scope := selectors.
NewExchangeBackup().
ContactFolders([]string{suite.user}, []string{exchange.DefaultContactFolder}, selectors.PrefixMatch())[0]
collections, err := connector.createCollections(ctx, scope)
require.NoError(t, err)
return collections
},
},
}
for _, test := range tests {
suite.T().Run(test.name, func(t *testing.T) {
edcs := test.getCollection(t)
require.Equal(t, len(edcs), 1)
edc := edcs[0]
assert.Equal(t, edc.FullPath().Folder(), exchange.DefaultContactFolder)
streamChannel := edc.Items()
count := 0
for stream := range streamChannel {
buf := &bytes.Buffer{}
read, err := buf.ReadFrom(stream.ToReader())
assert.NoError(t, err)
assert.NotZero(t, read)
contact, err := support.CreateContactFromBytes(buf.Bytes())
assert.NotNil(t, contact)
assert.NoError(t, err, "error on converting contact bytes: "+string(buf.Bytes()))
count++
}
assert.NotZero(t, count)
status := connector.AwaitStatus()
suite.NotNil(status)
suite.Equal(status.ObjectCount, status.Successful)
})
}
}
// TestEventsSerializationRegression ensures functionality of createCollections
// to be able to successfully query, download and restore event objects
func (suite *GraphConnectorIntegrationSuite) TestEventsSerializationRegression() {
ctx, flush := tester.NewContext()
defer flush()
connector := loadConnector(ctx, suite.T())
tests := []struct {
name, expected string
getCollection func(t *testing.T) []*exchange.Collection
}{
{
name: "Default Event Calendar",
expected: exchange.DefaultCalendar,
getCollection: func(t *testing.T) []*exchange.Collection {
sel := selectors.NewExchangeBackup()
sel.Include(sel.EventCalendars([]string{suite.user}, []string{exchange.DefaultCalendar}, selectors.PrefixMatch()))
collections, err := connector.createCollections(ctx, sel.Scopes()[0])
require.NoError(t, err)
return collections
},
},
{
name: "Birthday Calendar",
expected: "Birthdays",
getCollection: func(t *testing.T) []*exchange.Collection {
sel := selectors.NewExchangeBackup()
sel.Include(sel.EventCalendars([]string{suite.user}, []string{"Birthdays"}, selectors.PrefixMatch()))
collections, err := connector.createCollections(ctx, sel.Scopes()[0])
require.NoError(t, err)
return collections
},
},
}
for _, test := range tests {
suite.T().Run(test.name, func(t *testing.T) {
collections := test.getCollection(t)
require.Equal(t, len(collections), 1)
edc := collections[0]
assert.Equal(t, edc.FullPath().Folder(), test.expected)
streamChannel := edc.Items()
for stream := range streamChannel {
buf := &bytes.Buffer{}
read, err := buf.ReadFrom(stream.ToReader())
assert.NoError(t, err)
assert.NotZero(t, read)
event, err := support.CreateEventFromBytes(buf.Bytes())
assert.NotNil(t, event)
assert.NoError(t, err, "experienced error parsing event bytes: "+string(buf.Bytes()))
}
status := connector.AwaitStatus()
suite.NotNil(status)
suite.Equal(status.ObjectCount, status.Successful)
})
}
}
// TestAccessOfInboxAllUsers verifies that GraphConnector can
// support `--users *` for backup operations. Selector.DiscreteScopes
// returns all of the users within one scope. Only users who have
// messages in their inbox will have a collection returned.
// The final test insures that more than a 75% of the user collections are
// returned. If an error was experienced, the test will fail overall
func (suite *GraphConnectorIntegrationSuite) TestAccessOfInboxAllUsers() {
ctx, flush := tester.NewContext()
defer flush()
t := suite.T()
connector := loadConnector(ctx, t)
sel := selectors.NewExchangeBackup()
sel.Include(sel.MailFolders(selectors.Any(), []string{exchange.DefaultMailFolder}, selectors.PrefixMatch()))
scopes := sel.DiscreteScopes(connector.GetUsers())
for _, scope := range scopes {
users := scope.Get(selectors.ExchangeUser)
standard := (len(users) / 4) * 3
collections, err := connector.createCollections(ctx, scope)
require.NoError(t, err)
suite.Greater(len(collections), standard)
}
}
func (suite *GraphConnectorIntegrationSuite) TestMailFetch() {
ctx, flush := tester.NewContext()
defer flush()
var (
t = suite.T()
userID = tester.M365UserID(t)
)
tests := []struct {
name string
scope selectors.ExchangeScope
folderNames map[string]struct{}
}{
{
name: "Folder Iterative Check Mail",
scope: selectors.NewExchangeBackup().MailFolders(
[]string{userID},
[]string{exchange.DefaultMailFolder},
selectors.PrefixMatch(),
)[0],
folderNames: map[string]struct{}{
exchange.DefaultMailFolder: {},
},
},
}
gc := loadConnector(ctx, t)
for _, test := range tests {
suite.T().Run(test.name, func(t *testing.T) {
collections, err := gc.createCollections(ctx, test.scope)
require.NoError(t, err)
for _, c := range collections {
require.NotEmpty(t, c.FullPath().Folder())
folder := c.FullPath().Folder()
if _, ok := test.folderNames[folder]; ok {
delete(test.folderNames, folder)
}
}
assert.Empty(t, test.folderNames)
})
}
}
///------------------------------------------------------------
// Exchange Functions
//-------------------------------------------------------
func (suite *GraphConnectorIntegrationSuite) TestEmptyCollections() { func (suite *GraphConnectorIntegrationSuite) TestEmptyCollections() {
dest := tester.DefaultTestRestoreDestination() dest := tester.DefaultTestRestoreDestination()
table := []struct { table := []struct {
@ -447,6 +128,7 @@ func (suite *GraphConnectorIntegrationSuite) TestEmptyCollections() {
Service: selectors.ServiceOneDrive, Service: selectors.ServiceOneDrive,
}, },
}, },
// TODO: SharePoint
} }
for _, test := range table { for _, test := range table {
@ -466,6 +148,10 @@ func (suite *GraphConnectorIntegrationSuite) TestEmptyCollections() {
} }
} }
//-------------------------------------------------------------
// Exchange Functions
//-------------------------------------------------------------
func runRestoreBackupTest( func runRestoreBackupTest(
t *testing.T, t *testing.T,
test restoreBackupInfo, test restoreBackupInfo,

View File

@ -60,7 +60,7 @@ func (suite *OneDriveCollectionSuite) TestOneDriveCollection() {
wg := sync.WaitGroup{} wg := sync.WaitGroup{}
collStatus := support.ConnectorOperationStatus{} collStatus := support.ConnectorOperationStatus{}
folderPath, err := getCanonicalPath("drive/driveID1/root:/dir1/dir2/dir3", "a-tenant", "a-user") folderPath, err := getCanonicalPath("drive/driveID1/root:/dir1/dir2/dir3", "a-tenant", "a-user", OneDriveSource)
require.NoError(t, err) require.NoError(t, err)
driveFolderPath, err := getDriveFolderPath(folderPath) driveFolderPath, err := getDriveFolderPath(folderPath)
require.NoError(t, err) require.NoError(t, err)
@ -117,7 +117,7 @@ func (suite *OneDriveCollectionSuite) TestOneDriveCollectionReadError() {
wg := sync.WaitGroup{} wg := sync.WaitGroup{}
wg.Add(1) wg.Add(1)
folderPath, err := getCanonicalPath("drive/driveID1/root:/folderPath", "a-tenant", "a-user") folderPath, err := getCanonicalPath("drive/driveID1/root:/folderPath", "a-tenant", "a-user", OneDriveSource)
require.NoError(t, err) require.NoError(t, err)
coll := NewCollection(folderPath, "fakeDriveID", suite, suite.testStatusUpdater(&wg, &collStatus)) coll := NewCollection(folderPath, "fakeDriveID", suite, suite.testStatusUpdater(&wg, &collStatus))

View File

@ -14,20 +14,34 @@ import (
"github.com/alcionai/corso/src/internal/observe" "github.com/alcionai/corso/src/internal/observe"
"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/path"
"github.com/alcionai/corso/src/pkg/selectors"
) )
// Collections is used to retrieve OneDrive data for a type driveSource int
// specified user
const (
unknownDriveSource = iota
OneDriveSource
SharePointSource
)
type isAnyMatcher interface {
IsAny() bool
Matches(path string) bool
}
// Collections is used to retrieve drive data for a
// resource owner, which can be either a user or a sharepoint site.
type Collections struct { type Collections struct {
tenant string tenant string
user string resourceOwner string
scope selectors.OneDriveScope source driveSource
matcher isAnyMatcher
service graph.Service
statusUpdater support.StatusUpdater
// collectionMap allows lookup of the data.Collection // collectionMap allows lookup of the data.Collection
// for a OneDrive folder // for a OneDrive folder
collectionMap map[string]data.Collection collectionMap map[string]data.Collection
service graph.Service
statusUpdater support.StatusUpdater
// Track stats from drive enumeration. Represents the items backed up. // Track stats from drive enumeration. Represents the items backed up.
numItems int numItems int
@ -37,25 +51,27 @@ type Collections struct {
func NewCollections( func NewCollections(
tenant string, tenant string,
user string, resourceOwner string,
scope selectors.OneDriveScope, source driveSource,
matcher isAnyMatcher,
service graph.Service, service graph.Service,
statusUpdater support.StatusUpdater, statusUpdater support.StatusUpdater,
) *Collections { ) *Collections {
return &Collections{ return &Collections{
tenant: tenant, tenant: tenant,
user: user, resourceOwner: resourceOwner,
scope: scope, source: source,
matcher: matcher,
collectionMap: map[string]data.Collection{}, collectionMap: map[string]data.Collection{},
service: service, service: service,
statusUpdater: statusUpdater, statusUpdater: statusUpdater,
} }
} }
// Retrieves OneDrive data as set of `data.Collections` // Retrieves drive data as set of `data.Collections`
func (c *Collections) Get(ctx context.Context) ([]data.Collection, error) { func (c *Collections) Get(ctx context.Context) ([]data.Collection, error) {
// Enumerate drives for the specified user // Enumerate drives for the specified resourceOwner
drives, err := drives(ctx, c.service, c.user) drives, err := drives(ctx, c.service, c.resourceOwner, c.source)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -78,29 +94,8 @@ func (c *Collections) Get(ctx context.Context) ([]data.Collection, error) {
return collections, nil return collections, nil
} }
func getCanonicalPath(p, tenant, user string) (path.Path, error) { // updateCollections initializes and adds the provided drive items to Collections
pathBuilder := path.Builder{}.Append(strings.Split(p, "/")...) // A new collection is created for every drive folder (or package)
res, err := pathBuilder.ToDataLayerOneDrivePath(tenant, user, false)
if err != nil {
return nil, errors.Wrap(err, "converting to canonical path")
}
return res, nil
}
// Returns the path to the folder within the drive (i.e. under `root:`)
func getDriveFolderPath(p path.Path) (string, error) {
drivePath, err := toOneDrivePath(p)
if err != nil {
return "", err
}
return path.Builder{}.Append(drivePath.folders...).String(), nil
}
// updateCollections initializes and adds the provided OneDrive items to Collections
// A new collection is created for every OneDrive folder (or package)
func (c *Collections) updateCollections(ctx context.Context, driveID string, items []models.DriveItemable) error { func (c *Collections) updateCollections(ctx context.Context, driveID string, items []models.DriveItemable) error {
for _, item := range items { for _, item := range items {
if item.GetRoot() != nil { if item.GetRoot() != nil {
@ -116,14 +111,15 @@ func (c *Collections) updateCollections(ctx context.Context, driveID string, ite
collectionPath, err := getCanonicalPath( collectionPath, err := getCanonicalPath(
*item.GetParentReference().GetPath(), *item.GetParentReference().GetPath(),
c.tenant, c.tenant,
c.user, c.resourceOwner,
c.source,
) )
if err != nil { if err != nil {
return err return err
} }
// Skip items that don't match the folder selectors we were given. // Skip items that don't match the folder selectors we were given.
if !includePath(ctx, c.scope, collectionPath) { if !includePath(ctx, c.matcher, collectionPath) {
logger.Ctx(ctx).Infof("Skipping path %s", collectionPath.String()) logger.Ctx(ctx).Infof("Skipping path %s", collectionPath.String())
continue continue
} }
@ -162,7 +158,40 @@ func (c *Collections) updateCollections(ctx context.Context, driveID string, ite
return nil return nil
} }
func includePath(ctx context.Context, scope selectors.OneDriveScope, folderPath path.Path) bool { func getCanonicalPath(p, tenant, resourceOwner string, source driveSource) (path.Path, error) {
var (
pathBuilder = path.Builder{}.Append(strings.Split(p, "/")...)
result path.Path
err error
)
switch source {
case OneDriveSource:
result, err = pathBuilder.ToDataLayerOneDrivePath(tenant, resourceOwner, false)
case SharePointSource:
result, err = pathBuilder.ToDataLayerSharePointPath(tenant, resourceOwner, false)
default:
return nil, errors.Errorf("unrecognized drive data source")
}
if err != nil {
return nil, errors.Wrap(err, "converting to canonical path")
}
return result, nil
}
// Returns the path to the folder within the drive (i.e. under `root:`)
func getDriveFolderPath(p path.Path) (string, error) {
drivePath, err := toOneDrivePath(p)
if err != nil {
return "", err
}
return path.Builder{}.Append(drivePath.folders...).String(), nil
}
func includePath(ctx context.Context, m isAnyMatcher, folderPath path.Path) bool {
// Check if the folder is allowed by the scope. // Check if the folder is allowed by the scope.
folderPathString, err := getDriveFolderPath(folderPath) folderPathString, err := getDriveFolderPath(folderPath)
if err != nil { if err != nil {
@ -172,9 +201,9 @@ func includePath(ctx context.Context, scope selectors.OneDriveScope, folderPath
// Hack for the edge case where we're looking at the root folder and can // Hack for the edge case where we're looking at the root folder and can
// select any folder. Right now the root folder has an empty folder path. // select any folder. Right now the root folder has an empty folder path.
if len(folderPathString) == 0 && scope.IsAny(selectors.OneDriveFolder) { if len(folderPathString) == 0 && m.IsAny() {
return true return true
} }
return scope.Matches(selectors.OneDriveFolder, folderPathString) return m.Matches(folderPathString)
} }

View File

@ -20,7 +20,7 @@ func expectedPathAsSlice(t *testing.T, tenant, user string, rest ...string) []st
res := make([]string, 0, len(rest)) res := make([]string, 0, len(rest))
for _, r := range rest { for _, r := range rest {
p, err := getCanonicalPath(r, tenant, user) p, err := getCanonicalPath(r, tenant, user, OneDriveSource)
require.NoError(t, err) require.NoError(t, err)
res = append(res, p.String()) res = append(res, p.String())
@ -211,7 +211,7 @@ func (suite *OneDriveCollectionsSuite) TestUpdateCollections() {
ctx, flush := tester.NewContext() ctx, flush := tester.NewContext()
defer flush() defer flush()
c := NewCollections(tenant, user, tt.scope, &MockGraphService{}, nil) c := NewCollections(tenant, user, OneDriveSource, testFolderMatcher{tt.scope}, &MockGraphService{}, nil)
err := c.updateCollections(ctx, "driveID", tt.items) err := c.updateCollections(ctx, "driveID", tt.items)
tt.expect(t, err) tt.expect(t, err)
assert.Equal(t, len(tt.expectedCollectionPaths), len(c.collectionMap), "collection paths") assert.Equal(t, len(tt.expectedCollectionPaths), len(c.collectionMap), "collection paths")

View File

@ -67,7 +67,33 @@ const (
) )
// Enumerates the drives for the specified user // Enumerates the drives for the specified user
func drives(ctx context.Context, service graph.Service, user string) ([]models.Driveable, error) { func drives(
ctx context.Context,
service graph.Service,
resourceOwner string,
source driveSource,
) ([]models.Driveable, error) {
switch source {
case OneDriveSource:
return userDrives(ctx, service, resourceOwner)
case SharePointSource:
return siteDrives(ctx, service, resourceOwner)
default:
return nil, errors.Errorf("unrecognized drive data source")
}
}
func siteDrives(ctx context.Context, service graph.Service, site string) ([]models.Driveable, error) {
r, err := service.Client().SitesById(site).Drives().Get(ctx, nil)
if err != nil {
return nil, errors.Wrapf(err, "failed to retrieve site drives. site: %s, details: %s",
site, support.ConnectorStackErrorTrace(err))
}
return r.GetValue(), nil
}
func userDrives(ctx context.Context, service graph.Service, user string) ([]models.Driveable, error) {
var hasDrive bool var hasDrive bool
hasDrive, err := hasDriveLicense(ctx, service, user) hasDrive, err := hasDriveLicense(ctx, service, user)
@ -237,7 +263,7 @@ func GetAllFolders(
userID string, userID string,
prefix string, prefix string,
) ([]*Displayable, error) { ) ([]*Displayable, error) {
drives, err := drives(ctx, gs, userID) drives, err := drives(ctx, gs, userID, OneDriveSource)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "getting OneDrive folders") return nil, errors.Wrap(err, "getting OneDrive folders")
} }
@ -321,7 +347,7 @@ func hasDriveLicense(
cb := func(pageItem any) bool { cb := func(pageItem any) bool {
entry, ok := pageItem.(models.LicenseDetailsable) entry, ok := pageItem.(models.LicenseDetailsable)
if !ok { if !ok {
err = errors.New("casting item to models.MailFolderable") err = errors.New("casting item to models.LicenseDetailsable")
return false return false
} }

View File

@ -43,7 +43,7 @@ func (suite *OneDriveSuite) TestCreateGetDeleteFolder() {
folderElements := []string{folderName1} folderElements := []string{folderName1}
gs := loadTestService(t) gs := loadTestService(t)
drives, err := drives(ctx, gs, suite.userID) drives, err := drives(ctx, gs, suite.userID, OneDriveSource)
require.NoError(t, err) require.NoError(t, err)
require.NotEmpty(t, drives) require.NotEmpty(t, drives)
@ -100,6 +100,18 @@ func (suite *OneDriveSuite) TestCreateGetDeleteFolder() {
} }
} }
type testFolderMatcher struct {
scope selectors.OneDriveScope
}
func (fm testFolderMatcher) IsAny() bool {
return fm.scope.IsAny(selectors.OneDriveFolder)
}
func (fm testFolderMatcher) Matches(path string) bool {
return fm.scope.Matches(selectors.OneDriveFolder, path)
}
func (suite *OneDriveSuite) TestOneDriveNewCollections() { func (suite *OneDriveSuite) TestOneDriveNewCollections() {
ctx, flush := tester.NewContext() ctx, flush := tester.NewContext()
defer flush() defer flush()
@ -129,7 +141,8 @@ func (suite *OneDriveSuite) TestOneDriveNewCollections() {
odcs, err := NewCollections( odcs, err := NewCollections(
creds.AzureTenantID, creds.AzureTenantID,
test.user, test.user,
scope, OneDriveSource,
testFolderMatcher{scope},
service, service,
service.updateStatus, service.updateStatus,
).Get(ctx) ).Get(ctx)

View File

@ -67,7 +67,7 @@ func (suite *ItemIntegrationSuite) SetupSuite() {
suite.user = tester.SecondaryM365UserID(suite.T()) suite.user = tester.SecondaryM365UserID(suite.T())
drives, err := drives(ctx, suite, suite.user) drives, err := drives(ctx, suite, suite.user, OneDriveSource)
require.NoError(suite.T(), err) require.NoError(suite.T(), err)
// Test Requirement 1: Need a drive // Test Requirement 1: Need a drive
require.Greaterf(suite.T(), len(drives), 0, "user %s does not have a drive", suite.user) require.Greaterf(suite.T(), len(drives), 0, "user %s does not have a drive", suite.user)

View File

@ -0,0 +1,65 @@
package sharepoint
import (
"context"
"github.com/alcionai/corso/src/internal/connector/graph"
"github.com/alcionai/corso/src/internal/connector/onedrive"
"github.com/alcionai/corso/src/internal/connector/support"
"github.com/alcionai/corso/src/internal/data"
"github.com/alcionai/corso/src/pkg/logger"
"github.com/alcionai/corso/src/pkg/selectors"
)
func CollectLibraries(
ctx context.Context,
serv graph.Service,
tenantID string,
siteIDs []string,
scope selectors.SharePointScope,
updater support.StatusUpdater,
incrementWaitCount func(),
) ([]data.Collection, error) {
var (
collections = []data.Collection{}
errs error
)
for _, site := range scope.Get(selectors.SharePointSite) {
logger.Ctx(ctx).With("site", site).Debug("Creating SharePoint Libary collections")
colls := onedrive.NewCollections(
tenantID,
site,
onedrive.SharePointSource,
folderMatcher{scope},
serv,
updater,
)
odcs, err := colls.Get(ctx)
if err != nil {
return nil, support.WrapAndAppend(site, err, errs)
}
collections = append(collections, odcs...)
}
for range collections {
incrementWaitCount()
}
return collections, errs
}
type folderMatcher struct {
scope selectors.SharePointScope
}
func (fm folderMatcher) IsAny() bool {
return fm.scope.IsAny(selectors.SharePointFolder)
}
func (fm folderMatcher) Matches(path string) bool {
return fm.scope.Matches(selectors.SharePointFolder, path)
}

View File

@ -0,0 +1,95 @@
package sharepoint
import (
"context"
msgraphsdk "github.com/microsoftgraph/msgraph-sdk-go"
"github.com/pkg/errors"
"github.com/alcionai/corso/src/internal/connector/graph"
"github.com/alcionai/corso/src/pkg/account"
)
type sharePointService struct {
client msgraphsdk.GraphServiceClient
adapter msgraphsdk.GraphRequestAdapter
failFast bool // if true service will exit sequence upon encountering an error
credentials account.M365Config
}
///------------------------------------------------------------
// Functions to comply with graph.Service Interface
//-------------------------------------------------------
func (es *sharePointService) Client() *msgraphsdk.GraphServiceClient {
return &es.client
}
func (es *sharePointService) Adapter() *msgraphsdk.GraphRequestAdapter {
return &es.adapter
}
func (es *sharePointService) ErrPolicy() bool {
return es.failFast
}
// createService internal constructor for sharePointService struct returns an error
// iff the params for the entry are incorrect (e.g. len(TenantID) == 0, etc.)
// NOTE: Incorrect account information will result in errors on subsequent queries.
func createService(credentials account.M365Config, shouldFailFast bool) (*sharePointService, error) {
adapter, err := graph.CreateAdapter(
credentials.AzureTenantID,
credentials.AzureClientID,
credentials.AzureClientSecret,
)
if err != nil {
return nil, errors.Wrap(err, "creating microsoft graph service")
}
service := sharePointService{
adapter: *adapter,
client: *msgraphsdk.NewGraphServiceClient(adapter),
failFast: shouldFailFast,
credentials: credentials,
}
return &service, nil
}
// PopulateContainerResolver gets a container resolver if one is available for
// 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
// nil. If an error occurs populating the resolver, returns an error.
func PopulateContainerResolver(
ctx context.Context,
qp graph.QueryParams,
) (graph.ContainerResolver, error) {
return nil, nil
// var (
// c graph.ContainerPopulater
// service, err = createService(qp.Credentials, qp.FailFast)
// cacheRoot string
// )
// if err != nil {
// return nil, err
// }
// switch qp.Category {
// case path.FilesCategory:
// c = &driveCache{
// siteID: qp.ResourceOwner,
// gs: service,
// }
// cacheRoot = "root"
// default:
// return nil, fmt.Errorf("ContainerResolver not present for %s type", qp.Category)
// }
// if err := c.Populate(ctx, cacheRoot); err != nil {
// return nil, errors.Wrap(err, "populating container resolver")
// }
// return c, nil
}

View File

@ -0,0 +1,88 @@
package sharepoint
import (
"context"
"github.com/alcionai/corso/src/internal/connector/graph"
"github.com/alcionai/corso/src/internal/connector/support"
"github.com/alcionai/corso/src/pkg/selectors"
)
// FilterContainersAndFillCollections is a utility function
// that places the M365 object ids belonging to specific directories
// into a Collection. Items outside of those directories are omitted.
// @param collection is filled with during this function.
func FilterContainersAndFillCollections(
ctx context.Context,
qp graph.QueryParams,
collections map[string]*Collection,
statusUpdater support.StatusUpdater,
resolver graph.ContainerResolver,
scope selectors.SharePointScope,
) error {
return nil
}
// code previously within the function, moved here to make the linter happy
// var (
// category = qp.Scope.Category().PathType()
// collectionType = CategoryToOptionIdentifier(category)
// errs error
// )
// 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)
// if err != nil {
// errs = support.WrapAndAppend(
// qp.User+" FilterContainerAndFillCollection",
// err,
// errs)
// if qp.FailFast {
// return errs
// }
// }
// edc := NewCollection(
// qp.User,
// dirPath,
// collectionType,
// service,
// statusUpdater,
// )
// collections[*c.GetId()] = &edc
// }
// }
// for directoryID, col := range collections {
// fetchFunc, err := getFetchIDFunc(category)
// if err != nil {
// errs = support.WrapAndAppend(
// qp.User,
// err,
// errs)
// if qp.FailFast {
// return errs
// }
// continue
// }
// jobs, err := fetchFunc(ctx, col.service, qp.User, directoryID)
// if err != nil {
// errs = support.WrapAndAppend(
// qp.User,
// err,
// errs,
// )
// }
// col.jobs = append(col.jobs, jobs...)
// }
// return errs

View File

@ -21,6 +21,7 @@ const (
// M365 config // M365 config
TestCfgAzureTenantID = "azure_tenantid" TestCfgAzureTenantID = "azure_tenantid"
TestCfgSiteID = "m365siteid"
TestCfgUserID = "m365userid" TestCfgUserID = "m365userid"
TestCfgSecondaryUserID = "secondarym365userid" TestCfgSecondaryUserID = "secondarym365userid"
TestCfgLoadTestUserID = "loadtestm365userid" TestCfgLoadTestUserID = "loadtestm365userid"
@ -30,6 +31,7 @@ const (
// test specific env vars // test specific env vars
const ( const (
EnvCorsoM365TestSiteID = "CORSO_M365_TEST_SITE_ID"
EnvCorsoM365TestUserID = "CORSO_M365_TEST_USER_ID" EnvCorsoM365TestUserID = "CORSO_M365_TEST_USER_ID"
EnvCorsoSecondaryM365TestUserID = "CORSO_SECONDARY_M365_TEST_USER_ID" EnvCorsoSecondaryM365TestUserID = "CORSO_SECONDARY_M365_TEST_USER_ID"
EnvCorsoM365LoadTestUserID = "CORSO_M365_LOAD_TEST_USER_ID" EnvCorsoM365LoadTestUserID = "CORSO_M365_LOAD_TEST_USER_ID"
@ -137,6 +139,13 @@ func readTestConfig() (map[string]string, error) {
vpr.GetString(TestCfgLoadTestOrgUsers), vpr.GetString(TestCfgLoadTestOrgUsers),
"lidiah@8qzvrj.onmicrosoft.com,lynner@8qzvrj.onmicrosoft.com", "lidiah@8qzvrj.onmicrosoft.com,lynner@8qzvrj.onmicrosoft.com",
) )
fallbackTo(
testEnv,
TestCfgSiteID,
os.Getenv(EnvCorsoM365TestSiteID),
vpr.GetString(TestCfgSiteID),
"8qzvrj.sharepoint.com,1c9ef309-f47c-4e69-832b-a83edd69fa7f,c57f6e0e-3e4b-472c-b528-b56a2ccd0507",
)
testEnv[EnvCorsoTestConfigFilePath] = os.Getenv(EnvCorsoTestConfigFilePath) testEnv[EnvCorsoTestConfigFilePath] = os.Getenv(EnvCorsoTestConfigFilePath)
testConfig = testEnv testConfig = testEnv

View File

@ -16,6 +16,9 @@ const (
CorsoCLIRepoTests = "CORSO_COMMAND_LINE_REPO_TESTS" CorsoCLIRepoTests = "CORSO_COMMAND_LINE_REPO_TESTS"
CorsoCLIRestoreTests = "CORSO_COMMAND_LINE_RESTORE_TESTS" CorsoCLIRestoreTests = "CORSO_COMMAND_LINE_RESTORE_TESTS"
CorsoCLITests = "CORSO_COMMAND_LINE_TESTS" CorsoCLITests = "CORSO_COMMAND_LINE_TESTS"
CorsoConnectorCreateExchangeCollectionTests = "CORSO_CONNECTOR_CREATE_EXCHANGE_COLLECTION_TESTS"
CorsoConnectorCreateSharePointCollectionTests = "CORSO_CONNECTOR_CREATE_SHAREPOINT_COLLECTION_TESTS"
CorsoConnectorDataCollectionTests = "CORSO_CONNECTOR_DATA_COLLECTION_TESTS"
CorsoGraphConnectorTests = "CORSO_GRAPH_CONNECTOR_TESTS" CorsoGraphConnectorTests = "CORSO_GRAPH_CONNECTOR_TESTS"
CorsoGraphConnectorExchangeTests = "CORSO_GRAPH_CONNECTOR_EXCHANGE_TESTS" CorsoGraphConnectorExchangeTests = "CORSO_GRAPH_CONNECTOR_EXCHANGE_TESTS"
CorsoGraphConnectorOneDriveTests = "CORSO_GRAPH_CONNECTOR_ONE_DRIVE_TESTS" CorsoGraphConnectorOneDriveTests = "CORSO_GRAPH_CONNECTOR_ONE_DRIVE_TESTS"

View File

@ -57,3 +57,14 @@ func LoadTestM365OrgUsers(t *testing.T) []string {
return strings.Split(users, ",") return strings.Split(users, ",")
} }
// M365SiteID returns a siteID string representing the m365SiteID described
// by either the env var CORSO_M365_TEST_SITE_ID, the corso_test.toml config
// file or the default value (in that order of priority). The default is a
// last-attempt fallback that will only work on alcion's testing org.
func M365SiteID(t *testing.T) string {
cfg, err := readTestConfig()
require.NoError(t, err, "retrieving m365 site id from test configuration")
return cfg[TestCfgSiteID]
}

View File

@ -241,7 +241,7 @@ func (pb Builder) verifyPrefix(tenant, resourceOwner string) error {
} }
if len(resourceOwner) == 0 { if len(resourceOwner) == 0 {
return errors.Wrap(errMissingSegment, "user") return errors.Wrap(errMissingSegment, "resourceOwner")
} }
if len(pb.elements) == 0 { if len(pb.elements) == 0 {