From a7679554ab6a863ba2a679f12316d315c7492508 Mon Sep 17 00:00:00 2001 From: ryanfkeepers Date: Thu, 25 Jan 2024 11:28:07 -0700 Subject: [PATCH] add teamschats restore shim for export support --- .../m365/service/teamschats/restore.go | 100 ++++++++++++++++++ .../m365/service/teamschats/restore_test.go | 54 ++++++++++ .../restore_path_transformer.go | 1 + .../restore_path_transformer_test.go | 24 +++++ 4 files changed, 179 insertions(+) create mode 100644 src/internal/m365/service/teamschats/restore.go create mode 100644 src/internal/m365/service/teamschats/restore_test.go diff --git a/src/internal/m365/service/teamschats/restore.go b/src/internal/m365/service/teamschats/restore.go new file mode 100644 index 0000000000..e551e8e518 --- /dev/null +++ b/src/internal/m365/service/teamschats/restore.go @@ -0,0 +1,100 @@ +package teamschats + +import ( + "context" + "errors" + + "github.com/alcionai/clues" + + "github.com/alcionai/corso/src/internal/data" + "github.com/alcionai/corso/src/internal/m365/support" + "github.com/alcionai/corso/src/internal/operations/inject" + "github.com/alcionai/corso/src/pkg/backup/details" + "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/services/m365/api/graph" +) + +// ConsumeRestoreCollections will restore the specified data collections +func (h *teamsChatsHandler) ConsumeRestoreCollections( + ctx context.Context, + rcc inject.RestoreConsumerConfig, + dcs []data.RestoreCollection, + errs *fault.Bus, + ctr *count.Bus, +) (*details.Details, *data.CollectionStats, error) { + if len(dcs) == 0 { + return nil, nil, clues.WrapWC(ctx, data.ErrNoData, "performing restore") + } + + // TODO(ashmrtn): We should stop relying on the context for rate limiter stuff + // and instead configure this when we make the handler instance. We can't + // initialize it in the NewHandler call right now because those functions + // aren't (and shouldn't be) returning a context along with the handler. Since + // that call isn't directly calling into this function even if we did + // initialize the rate limiter there it would be lost because it wouldn't get + // stored in an ancestor of the context passed to this function. + ctx = graph.BindRateLimiterConfig( + ctx, + graph.LimiterCfg{Service: path.TeamsChatsService}) + + var ( + deets = &details.Builder{} + restoreMetrics support.CollectionMetrics + el = errs.Local() + ) + + // Reorder collections so that the parents directories are created + // before the child directories; a requirement for permissions. + data.SortRestoreCollections(dcs) + + // Iterate through the data collections and restore the contents of each + for _, dc := range dcs { + if el.Failure() != nil { + break + } + + var ( + err error + category = dc.FullPath().Category() + metrics support.CollectionMetrics + ictx = clues.Add(ctx, + "category", category, + "restore_location", clues.Hide(rcc.RestoreConfig.Location), + "protected_resource", clues.Hide(dc.FullPath().ProtectedResource()), + "full_path", dc.FullPath()) + ) + + switch dc.FullPath().Category() { + case path.ChatsCategory: + // chats cannot be restored using Graph API. + // a delegated token is required, and Corso has no + // good way of obtaining such a token. + logger.Ctx(ictx).Debug("Skipping restore for channel messages") + default: + return nil, nil, clues.NewWC(ictx, "data category not supported"). + With("category", category) + } + + restoreMetrics = support.CombineMetrics(restoreMetrics, metrics) + + if err != nil { + el.AddRecoverable(ictx, err) + } + + if errors.Is(err, context.Canceled) { + break + } + } + + status := support.CreateStatus( + ctx, + support.Restore, + len(dcs), + restoreMetrics, + rcc.RestoreConfig.Location) + + return deets.Details(), status.ToCollectionStats(), el.Failure() +} diff --git a/src/internal/m365/service/teamschats/restore_test.go b/src/internal/m365/service/teamschats/restore_test.go new file mode 100644 index 0000000000..a8feaab61d --- /dev/null +++ b/src/internal/m365/service/teamschats/restore_test.go @@ -0,0 +1,54 @@ +package teamschats + +import ( + "testing" + + "github.com/alcionai/clues" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + + "github.com/alcionai/corso/src/internal/data" + "github.com/alcionai/corso/src/internal/data/mock" + "github.com/alcionai/corso/src/internal/operations/inject" + "github.com/alcionai/corso/src/internal/tester" + "github.com/alcionai/corso/src/pkg/fault" + "github.com/alcionai/corso/src/pkg/path" + "github.com/alcionai/corso/src/pkg/services/m365/api" +) + +type RestoreUnitSuite struct { + tester.Suite +} + +func TestRestoreUnitSuite(t *testing.T) { + suite.Run(t, &RestoreUnitSuite{Suite: tester.NewUnitSuite(t)}) +} + +func (suite *RestoreUnitSuite) TestConsumeRestoreCollections_noErrorOnChats() { + t := suite.T() + + ctx, flush := tester.NewContext(t) + defer flush() + + rcc := inject.RestoreConsumerConfig{} + pth, err := path.BuildPrefix( + "t", + "pr", + path.TeamsChatsService, + path.ChatsCategory) + require.NoError(t, err, clues.ToCore(err)) + + dcs := []data.RestoreCollection{ + mock.Collection{Path: pth}, + } + + _, _, err = NewTeamsChatsHandler(api.Client{}, nil). + ConsumeRestoreCollections( + ctx, + rcc, + dcs, + fault.New(false), + nil) + assert.NoError(t, err, "Chats restore") +} diff --git a/src/internal/operations/pathtransformer/restore_path_transformer.go b/src/internal/operations/pathtransformer/restore_path_transformer.go index 5cc7944793..020de8de31 100644 --- a/src/internal/operations/pathtransformer/restore_path_transformer.go +++ b/src/internal/operations/pathtransformer/restore_path_transformer.go @@ -142,6 +142,7 @@ func makeRestorePathsForEntry( // * OneDrive/SharePoint (needs drive information) switch true { case ent.Exchange != nil || + ent.TeamsChats != nil || (ent.Groups != nil && ent.Groups.ItemType == details.GroupsChannelMessage) || (ent.SharePoint != nil && ent.SharePoint.ItemType == details.SharePointList): // TODO(ashmrtn): Eventually make Events have it's own function to handle diff --git a/src/internal/operations/pathtransformer/restore_path_transformer_test.go b/src/internal/operations/pathtransformer/restore_path_transformer_test.go index 390354ac02..cc9532a003 100644 --- a/src/internal/operations/pathtransformer/restore_path_transformer_test.go +++ b/src/internal/operations/pathtransformer/restore_path_transformer_test.go @@ -399,6 +399,30 @@ func (suite *RestorePathTransformerUnitSuite) TestGetPaths() { }, }, }, + { + name: "TeamsChats Chats", + backupVersion: version.Groups9Update, + input: []*details.Entry{ + { + RepoRef: testdata.ExchangeEmailItemPath3.RR.String(), + LocationRef: testdata.ExchangeEmailItemPath3.Loc.String(), + ItemInfo: details.ItemInfo{ + Exchange: &details.ExchangeInfo{ + ItemType: details.ExchangeMail, + }, + }, + }, + }, + expectErr: assert.NoError, + expected: []expectPaths{ + { + storage: testdata.ExchangeEmailItemPath3.RR.String(), + restore: toRestore( + testdata.ExchangeEmailItemPath3.RR, + testdata.ExchangeEmailItemPath3.Loc.Elements()...), + }, + }, + }, } for _, test := range table {