diff --git a/src/internal/m365/collection/teamsChats/backup.go b/src/internal/m365/collection/teamsChats/backup.go index 927f79044..5908dbc6e 100644 --- a/src/internal/m365/collection/teamsChats/backup.go +++ b/src/internal/m365/collection/teamsChats/backup.go @@ -139,7 +139,7 @@ func populateCollection[I chatsItemer]( cl.Add(count.ItemsAdded, int64(len(includedItems))) cl.Add(count.ItemsRemoved, 0) - p, err := bh.canonicalPath() + p, err := bh.CanonicalPath() if err != nil { err = clues.StackWC(ctx, err).Label(count.BadCollPath) errs.AddRecoverable(ctx, err) diff --git a/src/internal/m365/collection/teamsChats/backup_test.go b/src/internal/m365/collection/teamsChats/backup_test.go index 9812cee68..bbb8ee0d5 100644 --- a/src/internal/m365/collection/teamsChats/backup_test.go +++ b/src/internal/m365/collection/teamsChats/backup_test.go @@ -84,7 +84,7 @@ func (bh mockBackupHandler) includeItem( return !bh.doNotInclude } -func (bh mockBackupHandler) canonicalPath() (path.Path, error) { +func (bh mockBackupHandler) CanonicalPath() (path.Path, error) { return path.BuildPrefix( "tenant", "protectedResource", diff --git a/src/internal/m365/collection/teamsChats/chat_handler.go b/src/internal/m365/collection/teamsChats/chat_handler.go index d78fa5d63..a6ad0341a 100644 --- a/src/internal/m365/collection/teamsChats/chat_handler.go +++ b/src/internal/m365/collection/teamsChats/chat_handler.go @@ -61,7 +61,7 @@ func (bh usersChatsBackupHandler) includeItem( return scope.Matches(selectors.TeamsChatsChat, ptr.Val(ch.GetTopic())) } -func (bh usersChatsBackupHandler) canonicalPath() (path.Path, error) { +func (bh usersChatsBackupHandler) CanonicalPath() (path.Path, error) { return path.BuildPrefix( bh.tenantID, bh.protectedResourceID, diff --git a/src/internal/m365/collection/teamsChats/handlers.go b/src/internal/m365/collection/teamsChats/handlers.go index d925bd591..f1db7d32b 100644 --- a/src/internal/m365/collection/teamsChats/handlers.go +++ b/src/internal/m365/collection/teamsChats/handlers.go @@ -79,7 +79,7 @@ type includeItemer[I chatsItemer] interface { // the given builder. The tenantID and protectedResourceID are assumed // to be stored in the handler already. type canonicalPather interface { - canonicalPath() (path.Path, error) + CanonicalPath() (path.Path, error) } // --------------------------------------------------------------------------- diff --git a/src/internal/m365/service/teamschats/backup.go b/src/internal/m365/service/teamschats/backup.go new file mode 100644 index 000000000..2d99f6a46 --- /dev/null +++ b/src/internal/m365/service/teamschats/backup.go @@ -0,0 +1,162 @@ +package groups + +import ( + "context" + + "github.com/alcionai/clues" + + "github.com/alcionai/corso/src/internal/common/idname" + "github.com/alcionai/corso/src/internal/common/prefixmatcher" + "github.com/alcionai/corso/src/internal/data" + "github.com/alcionai/corso/src/internal/m365/collection/teamschats" + "github.com/alcionai/corso/src/internal/m365/support" + "github.com/alcionai/corso/src/internal/observe" + "github.com/alcionai/corso/src/internal/operations/inject" + "github.com/alcionai/corso/src/pkg/account" + "github.com/alcionai/corso/src/pkg/count" + "github.com/alcionai/corso/src/pkg/fault" + "github.com/alcionai/corso/src/pkg/logger" + "github.com/alcionai/corso/src/pkg/path" + "github.com/alcionai/corso/src/pkg/selectors" + "github.com/alcionai/corso/src/pkg/services/m365/api" + "github.com/alcionai/corso/src/pkg/services/m365/api/graph" +) + +func ProduceBackupCollections( + ctx context.Context, + bpc inject.BackupProducerConfig, + ac api.Client, + creds account.M365Config, + su support.StatusUpdater, + counter *count.Bus, + errs *fault.Bus, +) ([]data.BackupCollection, *prefixmatcher.StringSetMatcher, error) { + b, err := bpc.Selector.ToTeamsChatsBackup() + if err != nil { + return nil, nil, clues.WrapWC(ctx, err, "parsing selector") + } + + var ( + el = errs.Local() + collections = []data.BackupCollection{} + categories = map[path.CategoryType]struct{}{} + ) + + ctx = clues.Add( + ctx, + "user_id", clues.Hide(bpc.ProtectedResource.ID()), + "user_name", clues.Hide(bpc.ProtectedResource.Name())) + + bc := backupCommon{ + apiCli: ac, + producerConfig: bpc, + creds: creds, + user: bpc.ProtectedResource, + statusUpdater: su, + } + + for _, scope := range b.Scopes() { + if el.Failure() != nil { + break + } + + cl := counter.Local() + ictx := clues.AddLabelCounter(ctx, cl.PlainAdder()) + ictx = clues.Add(ictx, "category", scope.Category().PathType()) + + var colls []data.BackupCollection + + switch scope.Category().PathType() { + case path.ChatsCategory: + colls, err = backupChats( + ictx, + bc, + scope, + cl, + el) + } + + if err != nil { + el.AddRecoverable(ctx, clues.Stack(err)) + continue + } + + collections = append(collections, colls...) + + categories[scope.Category().PathType()] = struct{}{} + } + + if len(collections) > 0 { + baseCols, err := graph.BaseCollections( + ctx, + collections, + creds.AzureTenantID, + bpc.ProtectedResource.ID(), + path.TeamsChatsService, + categories, + su, + counter, + errs) + if err != nil { + return nil, nil, err + } + + collections = append(collections, baseCols...) + } + + counter.Add(count.Collections, int64(len(collections))) + + logger.Ctx(ctx).Infow("produced collections", "stats", counter.Values()) + + return collections, nil, clues.Stack(el.Failure()).OrNil() +} + +type backupCommon struct { + apiCli api.Client + producerConfig inject.BackupProducerConfig + creds account.M365Config + user idname.Provider + statusUpdater support.StatusUpdater +} + +func backupChats( + ctx context.Context, + bc backupCommon, + scope selectors.TeamsChatsScope, + counter *count.Bus, + errs *fault.Bus, +) ([]data.BackupCollection, error) { + var ( + colls []data.BackupCollection + ) + + progressMessage := observe.MessageWithCompletion( + ctx, + observe.ProgressCfg{ + Indent: 1, + CompletionMessage: func() string { return "(done)" }, + }, + scope.Category().PathType().HumanString()) + defer close(progressMessage) + + bh := teamschats.NewUsersChatsBackupHandler( + bc.creds.AzureTenantID, + bc.producerConfig.ProtectedResource.ID(), + bc.apiCli.Chats()) + + // Always disable lazy reader for channels until #4321 support is added + useLazyReader := false + + colls, _, err := teamschats.CreateCollections( + ctx, + bc.producerConfig, + bh, + bc.creds.AzureTenantID, + scope, + bc.statusUpdater, + useLazyReader, + counter, + errs) + + return colls, clues.Stack(err).OrNil() +} diff --git a/src/internal/m365/service/teamschats/enabled.go b/src/internal/m365/service/teamschats/enabled.go new file mode 100644 index 000000000..806449974 --- /dev/null +++ b/src/internal/m365/service/teamschats/enabled.go @@ -0,0 +1,18 @@ +package groups + +import ( + "context" + + "github.com/alcionai/clues" + "github.com/microsoftgraph/msgraph-sdk-go/models" + + "github.com/alcionai/corso/src/pkg/services/m365/api" +) + +func IsServiceEnabled( + ctx context.Context, + gbi api.GetByIDer[models.Userable], + resource string, +) (bool, error) { + return true, clues.New("needs implementation") +} diff --git a/src/internal/m365/service/teamschats/enabled_test.go b/src/internal/m365/service/teamschats/enabled_test.go new file mode 100644 index 000000000..d95b38d4f --- /dev/null +++ b/src/internal/m365/service/teamschats/enabled_test.go @@ -0,0 +1,69 @@ +package groups + +import ( + "context" + "testing" + + "github.com/alcionai/clues" + "github.com/microsoftgraph/msgraph-sdk-go/models" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" + + "github.com/alcionai/corso/src/internal/tester" + "github.com/alcionai/corso/src/pkg/services/m365/api" +) + +type EnabledUnitSuite struct { + tester.Suite +} + +func TestEnabledUnitSuite(t *testing.T) { + suite.Run(t, &EnabledUnitSuite{Suite: tester.NewUnitSuite(t)}) +} + +var _ api.GetByIDer[models.Userable] = mockGU{} + +type mockGU struct { + user models.Userable + err error +} + +func (m mockGU) GetByID( + ctx context.Context, + identifier string, + _ api.CallConfig, +) (models.Userable, error) { + return m.user, m.err +} + +func (suite *EnabledUnitSuite) TestIsServiceEnabled() { + table := []struct { + name string + mock func(context.Context) api.GetByIDer[models.Userable] + expect assert.BoolAssertionFunc + expectErr assert.ErrorAssertionFunc + }{ + { + name: "ok", + mock: func(ctx context.Context) api.GetByIDer[models.Userable] { + return mockGU{} + }, + expect: assert.True, + expectErr: assert.Error, + }, + } + for _, test := range table { + suite.Run(test.name, func() { + t := suite.T() + + ctx, flush := tester.NewContext(t) + defer flush() + + gu := test.mock(ctx) + + ok, err := IsServiceEnabled(ctx, gu, "resource_id") + test.expect(t, ok, "has mailbox flag") + test.expectErr(t, err, clues.ToCore(err)) + }) + } +} diff --git a/src/internal/m365/service/teamschats/mock/mock.go b/src/internal/m365/service/teamschats/mock/mock.go new file mode 100644 index 000000000..5ba482412 --- /dev/null +++ b/src/internal/m365/service/teamschats/mock/mock.go @@ -0,0 +1,14 @@ +package stub + +import ( + "github.com/alcionai/corso/src/pkg/backup/details" +) + +func ItemInfo() details.ItemInfo { + return details.ItemInfo{ + TeamsChats: &details.TeamsChatsInfo{ + ItemType: details.TeamsChat, + Chat: details.ChatInfo{}, + }, + } +}