add export support to teamschats service layer
This commit is contained in:
parent
7ab1276d61
commit
b9f71280bf
@ -7,6 +7,7 @@ import (
|
||||
"github.com/alcionai/corso/src/internal/m365/service/groups"
|
||||
"github.com/alcionai/corso/src/internal/m365/service/onedrive"
|
||||
"github.com/alcionai/corso/src/internal/m365/service/sharepoint"
|
||||
"github.com/alcionai/corso/src/internal/m365/service/teamschats"
|
||||
"github.com/alcionai/corso/src/internal/operations/inject"
|
||||
"github.com/alcionai/corso/src/pkg/path"
|
||||
)
|
||||
@ -30,6 +31,9 @@ func (ctrl *Controller) NewServiceHandler(
|
||||
|
||||
case path.ExchangeService:
|
||||
return exchange.NewExchangeHandler(ctrl.AC, ctrl.resourceHandler), nil
|
||||
|
||||
case path.TeamsChatsService:
|
||||
return teamschats.NewTeamsChatsHandler(ctrl.AC, ctrl.resourceHandler), nil
|
||||
}
|
||||
|
||||
return nil, clues.New("unrecognized service").
|
||||
|
||||
@ -8,6 +8,7 @@ import (
|
||||
|
||||
"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"
|
||||
@ -79,7 +80,7 @@ func (suite *ExportUnitSuite) TestExportRestoreCollections_messages() {
|
||||
)
|
||||
|
||||
p, err := path.Build("t", "pr", path.GroupsService, path.ChannelMessagesCategory, false, containerName)
|
||||
assert.NoError(t, err, "build path")
|
||||
require.NoError(t, err, clues.ToCore(err))
|
||||
|
||||
dcs := []data.RestoreCollection{
|
||||
data.FetchRestoreCollection{
|
||||
@ -106,7 +107,7 @@ func (suite *ExportUnitSuite) TestExportRestoreCollections_messages() {
|
||||
dcs,
|
||||
stats,
|
||||
fault.New(true))
|
||||
assert.NoError(t, err, "export collections error")
|
||||
require.NoError(t, err, clues.ToCore(err))
|
||||
assert.Len(t, ecs, 1, "num of collections")
|
||||
|
||||
assert.Equal(t, expectedPath, ecs[0].BasePath(), "base dir")
|
||||
@ -117,7 +118,7 @@ func (suite *ExportUnitSuite) TestExportRestoreCollections_messages() {
|
||||
|
||||
for item := range ecs[0].Items(ctx) {
|
||||
b, err := io.ReadAll(item.Body)
|
||||
assert.NoError(t, err, clues.ToCore(err))
|
||||
require.NoError(t, err, clues.ToCore(err))
|
||||
|
||||
// count up size for tests
|
||||
size += len(b)
|
||||
@ -181,7 +182,7 @@ func (suite *ExportUnitSuite) TestExportRestoreCollections_libraries() {
|
||||
false,
|
||||
odConsts.SitesPathDir,
|
||||
siteID)
|
||||
assert.NoError(t, err, "build path")
|
||||
require.NoError(t, err, clues.ToCore(err))
|
||||
|
||||
dcs := []data.RestoreCollection{
|
||||
data.FetchRestoreCollection{
|
||||
@ -210,7 +211,7 @@ func (suite *ExportUnitSuite) TestExportRestoreCollections_libraries() {
|
||||
dcs,
|
||||
stats,
|
||||
fault.New(true))
|
||||
assert.NoError(t, err, "export collections error")
|
||||
require.NoError(t, err, clues.ToCore(err))
|
||||
assert.Len(t, ecs, 1, "num of collections")
|
||||
|
||||
assert.Equal(t, expectedPath, ecs[0].BasePath(), "base dir")
|
||||
@ -222,7 +223,7 @@ func (suite *ExportUnitSuite) TestExportRestoreCollections_libraries() {
|
||||
for item := range ecs[0].Items(ctx) {
|
||||
// unwrap the body from stats reader
|
||||
b, err := io.ReadAll(item.Body)
|
||||
assert.NoError(t, err, clues.ToCore(err))
|
||||
require.NoError(t, err, clues.ToCore(err))
|
||||
|
||||
size += len(b)
|
||||
bitem := io.NopCloser(bytes.NewBuffer(b))
|
||||
|
||||
119
src/internal/m365/service/teamschats/export.go
Normal file
119
src/internal/m365/service/teamschats/export.go
Normal file
@ -0,0 +1,119 @@
|
||||
package teamschats
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/alcionai/clues"
|
||||
|
||||
"github.com/alcionai/corso/src/internal/common/idname"
|
||||
"github.com/alcionai/corso/src/internal/data"
|
||||
"github.com/alcionai/corso/src/internal/m365/collection/teamschats"
|
||||
"github.com/alcionai/corso/src/internal/m365/resource"
|
||||
"github.com/alcionai/corso/src/internal/operations/inject"
|
||||
"github.com/alcionai/corso/src/pkg/backup/details"
|
||||
"github.com/alcionai/corso/src/pkg/control"
|
||||
"github.com/alcionai/corso/src/pkg/export"
|
||||
"github.com/alcionai/corso/src/pkg/fault"
|
||||
"github.com/alcionai/corso/src/pkg/metrics"
|
||||
"github.com/alcionai/corso/src/pkg/path"
|
||||
"github.com/alcionai/corso/src/pkg/services/m365/api"
|
||||
)
|
||||
|
||||
var _ inject.ServiceHandler = &teamsChatsHandler{}
|
||||
|
||||
func NewTeamsChatsHandler(
|
||||
apiClient api.Client,
|
||||
resourceGetter idname.GetResourceIDAndNamer,
|
||||
) *teamsChatsHandler {
|
||||
return &teamsChatsHandler{
|
||||
baseTeamsChatsHandler: baseTeamsChatsHandler{},
|
||||
apiClient: apiClient,
|
||||
resourceGetter: resourceGetter,
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================== //
|
||||
// baseTeamsChatsHandler
|
||||
// ========================================================================== //
|
||||
|
||||
// baseTeamsChatsHandler contains logic for tracking data and doing operations
|
||||
// (e.x. export) that don't require contact with external M356 services.
|
||||
type baseTeamsChatsHandler struct{}
|
||||
|
||||
func (h *baseTeamsChatsHandler) CacheItemInfo(v details.ItemInfo) {}
|
||||
|
||||
// ProduceExportCollections will create the export collections for the
|
||||
// given restore collections.
|
||||
func (h *baseTeamsChatsHandler) ProduceExportCollections(
|
||||
ctx context.Context,
|
||||
backupVersion int,
|
||||
exportCfg control.ExportConfig,
|
||||
dcs []data.RestoreCollection,
|
||||
stats *metrics.ExportStats,
|
||||
errs *fault.Bus,
|
||||
) ([]export.Collectioner, error) {
|
||||
var (
|
||||
el = errs.Local()
|
||||
ec = make([]export.Collectioner, 0, len(dcs))
|
||||
)
|
||||
|
||||
for _, dc := range dcs {
|
||||
category := dc.FullPath().Category()
|
||||
|
||||
switch category {
|
||||
case path.ChatsCategory:
|
||||
folders := dc.FullPath().Folders()
|
||||
pth := path.Builder{}.Append(category.HumanString()).Append(folders...)
|
||||
|
||||
ec = append(
|
||||
ec,
|
||||
teamschats.NewExportCollection(
|
||||
pth.String(),
|
||||
[]data.RestoreCollection{dc},
|
||||
backupVersion,
|
||||
exportCfg,
|
||||
stats))
|
||||
default:
|
||||
return nil, clues.NewWC(ctx, "data category not supported").
|
||||
With("category", category)
|
||||
}
|
||||
}
|
||||
|
||||
return ec, el.Failure()
|
||||
}
|
||||
|
||||
// ========================================================================== //
|
||||
// teamschatsHandler
|
||||
// ========================================================================== //
|
||||
|
||||
// teamsChatsHandler contains logic for handling data and performing operations
|
||||
// (e.x. restore) regardless of whether they require contact with external M365
|
||||
// services or not.
|
||||
type teamsChatsHandler struct {
|
||||
baseTeamsChatsHandler
|
||||
apiClient api.Client
|
||||
resourceGetter idname.GetResourceIDAndNamer
|
||||
}
|
||||
|
||||
func (h *teamsChatsHandler) IsServiceEnabled(
|
||||
ctx context.Context,
|
||||
resourceID string,
|
||||
) (bool, error) {
|
||||
// TODO(ashmrtn): Move free function implementation to this function.
|
||||
res, err := IsServiceEnabled(ctx, h.apiClient.Users(), resourceID)
|
||||
return res, clues.Stack(err).OrNil()
|
||||
}
|
||||
|
||||
func (h *teamsChatsHandler) PopulateProtectedResourceIDAndName(
|
||||
ctx context.Context,
|
||||
resourceID string, // Can be either ID or name.
|
||||
ins idname.Cacher,
|
||||
) (idname.Provider, error) {
|
||||
if h.resourceGetter == nil {
|
||||
return nil, clues.StackWC(ctx, resource.ErrNoResourceLookup)
|
||||
}
|
||||
|
||||
pr, err := h.resourceGetter.GetResourceIDAndNameFrom(ctx, resourceID, ins)
|
||||
|
||||
return pr, clues.Wrap(err, "identifying resource owner").OrNil()
|
||||
}
|
||||
140
src/internal/m365/service/teamschats/export_test.go
Normal file
140
src/internal/m365/service/teamschats/export_test.go
Normal file
@ -0,0 +1,140 @@
|
||||
package teamschats
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"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"
|
||||
dataMock "github.com/alcionai/corso/src/internal/data/mock"
|
||||
teamschatMock "github.com/alcionai/corso/src/internal/m365/service/teamschats/mock"
|
||||
"github.com/alcionai/corso/src/internal/tester"
|
||||
"github.com/alcionai/corso/src/internal/version"
|
||||
"github.com/alcionai/corso/src/pkg/control"
|
||||
"github.com/alcionai/corso/src/pkg/export"
|
||||
"github.com/alcionai/corso/src/pkg/fault"
|
||||
"github.com/alcionai/corso/src/pkg/metrics"
|
||||
"github.com/alcionai/corso/src/pkg/path"
|
||||
"github.com/alcionai/corso/src/pkg/services/m365/api"
|
||||
)
|
||||
|
||||
type ExportUnitSuite struct {
|
||||
tester.Suite
|
||||
}
|
||||
|
||||
func TestExportUnitSuite(t *testing.T) {
|
||||
suite.Run(t, &ExportUnitSuite{Suite: tester.NewUnitSuite(t)})
|
||||
}
|
||||
|
||||
type finD struct {
|
||||
id string
|
||||
key string
|
||||
name string
|
||||
err error
|
||||
}
|
||||
|
||||
func (fd finD) FetchItemByName(ctx context.Context, name string) (data.Item, error) {
|
||||
if fd.err != nil {
|
||||
return nil, fd.err
|
||||
}
|
||||
|
||||
if name == fd.id {
|
||||
return &dataMock.Item{
|
||||
ItemID: fd.id,
|
||||
Reader: io.NopCloser(bytes.NewBufferString(`{"` + fd.key + `": "` + fd.name + `"}`)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, assert.AnError
|
||||
}
|
||||
|
||||
func (suite *ExportUnitSuite) TestExportRestoreCollections_chats() {
|
||||
t := suite.T()
|
||||
|
||||
ctx, flush := tester.NewContext(t)
|
||||
defer flush()
|
||||
|
||||
var (
|
||||
category = path.ChatsCategory
|
||||
itemID = "itemID"
|
||||
dii = teamschatMock.ItemInfo()
|
||||
content = `{"topic": "` + dii.TeamsChats.Chat.Topic + `"}`
|
||||
body = io.NopCloser(bytes.NewBufferString(content))
|
||||
exportCfg = control.ExportConfig{}
|
||||
expectedPath = category.HumanString()
|
||||
expectedItems = []export.Item{
|
||||
{
|
||||
ID: itemID,
|
||||
Name: itemID + ".json",
|
||||
// Body: body, not checked
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
p, err := path.BuildPrefix("t", "pr", path.TeamsChatsService, category)
|
||||
require.NoError(t, err, clues.ToCore(err))
|
||||
|
||||
dcs := []data.RestoreCollection{
|
||||
data.FetchRestoreCollection{
|
||||
Collection: dataMock.Collection{
|
||||
Path: p,
|
||||
ItemData: []data.Item{
|
||||
&dataMock.Item{
|
||||
ItemID: itemID,
|
||||
Reader: body,
|
||||
},
|
||||
},
|
||||
},
|
||||
FetchItemByNamer: finD{
|
||||
id: itemID,
|
||||
key: "id",
|
||||
name: itemID,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
stats := metrics.NewExportStats()
|
||||
|
||||
ecs, err := NewTeamsChatsHandler(api.Client{}, nil).
|
||||
ProduceExportCollections(
|
||||
ctx,
|
||||
int(version.Backup),
|
||||
exportCfg,
|
||||
dcs,
|
||||
stats,
|
||||
fault.New(true))
|
||||
require.NoError(t, err, clues.ToCore(err))
|
||||
assert.Len(t, ecs, 1, "num of collections")
|
||||
|
||||
assert.Equal(t, expectedPath, ecs[0].BasePath(), "base dir")
|
||||
|
||||
fitems := []export.Item{}
|
||||
|
||||
size := 0
|
||||
|
||||
for item := range ecs[0].Items(ctx) {
|
||||
b, err := io.ReadAll(item.Body)
|
||||
require.NoError(t, err, clues.ToCore(err))
|
||||
|
||||
// count up size for tests
|
||||
size += len(b)
|
||||
|
||||
// have to nil out body, otherwise assert fails due to
|
||||
// pointer memory location differences
|
||||
item.Body = nil
|
||||
fitems = append(fitems, item)
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedItems, fitems, "items")
|
||||
|
||||
expectedStats := metrics.NewExportStats()
|
||||
expectedStats.UpdateBytes(category, int64(size))
|
||||
expectedStats.UpdateResourceCount(category)
|
||||
assert.Equal(t, expectedStats.GetStats(), stats.GetStats(), "stats")
|
||||
}
|
||||
84
src/pkg/backup/details/testdata/testdata.go
vendored
84
src/pkg/backup/details/testdata/testdata.go
vendored
@ -16,11 +16,13 @@ import (
|
||||
// mustParsePath takes a string representing a resource path and returns a path
|
||||
// instance. Panics if the path cannot be parsed. Useful for simple variable
|
||||
// assignments.
|
||||
func mustParsePath(ref string, isItem, isSharepointList bool) path.Path {
|
||||
var p path.Path
|
||||
var err error
|
||||
func mustParsePath(ref string, isItem, allowPrefix bool) path.Path {
|
||||
var (
|
||||
p path.Path
|
||||
err error
|
||||
)
|
||||
|
||||
if isSharepointList {
|
||||
if allowPrefix {
|
||||
p, err = path.PrefixOrPathFromDataLayerPath(ref, isItem)
|
||||
} else {
|
||||
p, err = path.FromDataLayerPath(ref, isItem)
|
||||
@ -126,9 +128,9 @@ func (p repoRefAndLocRef) locationAsRepoRef() path.Path {
|
||||
return res
|
||||
}
|
||||
|
||||
func mustPathRep(ref string, isItem, isSharepointList bool) repoRefAndLocRef {
|
||||
func mustPathRep(ref string, isItem, allowPrefix bool) repoRefAndLocRef {
|
||||
res := repoRefAndLocRef{}
|
||||
tmp := mustParsePath(ref, isItem, isSharepointList)
|
||||
tmp := mustParsePath(ref, isItem, allowPrefix)
|
||||
|
||||
// Now append stuff to the RepoRef elements so we have distinct LocationRef
|
||||
// and RepoRef elements to simulate using IDs in the path instead of display
|
||||
@ -969,6 +971,68 @@ var (
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
TeamsChatsRootPath = mustPathRep("tenant-id/"+path.TeamsChatsService.String()+"/user-id/chats", false, true)
|
||||
|
||||
TeamsChatsChatItemPath1 = TeamsChatsRootPath.MustAppend(ItemName1, true)
|
||||
TeamsChatsChatItemPath2 = TeamsChatsRootPath.MustAppend(ItemName2, true)
|
||||
TeamsChatsChatItemPath3 = TeamsChatsRootPath.MustAppend(ItemName3, true)
|
||||
|
||||
teamsChatsChatItemsByVersion = map[int][]details.Entry{
|
||||
version.Groups9Update: {
|
||||
{
|
||||
RepoRef: TeamsChatsChatItemPath1.locationAsRepoRef().String(),
|
||||
ShortRef: TeamsChatsChatItemPath1.locationAsRepoRef().ShortRef(),
|
||||
ParentRef: TeamsChatsChatItemPath1.locationAsRepoRef().ToBuilder().Dir().ShortRef(),
|
||||
ItemRef: TeamsChatsChatItemPath1.ItemLocation(),
|
||||
LocationRef: "",
|
||||
ItemInfo: details.ItemInfo{
|
||||
TeamsChats: &details.TeamsChatsInfo{
|
||||
ItemType: details.TeamsChat,
|
||||
Modified: Time4,
|
||||
ParentPath: "",
|
||||
Chat: details.ChatInfo{
|
||||
Topic: "item 1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
RepoRef: TeamsChatsChatItemPath2.locationAsRepoRef().String(),
|
||||
ShortRef: TeamsChatsChatItemPath2.locationAsRepoRef().ShortRef(),
|
||||
ParentRef: TeamsChatsChatItemPath2.locationAsRepoRef().ToBuilder().Dir().ShortRef(),
|
||||
ItemRef: TeamsChatsChatItemPath2.ItemLocation(),
|
||||
LocationRef: "",
|
||||
ItemInfo: details.ItemInfo{
|
||||
TeamsChats: &details.TeamsChatsInfo{
|
||||
ItemType: details.TeamsChat,
|
||||
Modified: Time3,
|
||||
ParentPath: "",
|
||||
Chat: details.ChatInfo{
|
||||
Topic: "item 2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
RepoRef: TeamsChatsChatItemPath3.locationAsRepoRef().String(),
|
||||
ShortRef: TeamsChatsChatItemPath3.locationAsRepoRef().ShortRef(),
|
||||
ParentRef: TeamsChatsChatItemPath3.locationAsRepoRef().ToBuilder().Dir().ShortRef(),
|
||||
ItemRef: TeamsChatsChatItemPath3.ItemLocation(),
|
||||
LocationRef: "",
|
||||
ItemInfo: details.ItemInfo{
|
||||
TeamsChats: &details.TeamsChatsInfo{
|
||||
ItemType: details.TeamsChat,
|
||||
ParentPath: "",
|
||||
Modified: Time4,
|
||||
Chat: details.ChatInfo{
|
||||
Topic: "item 3",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func GetDetailsSetForVersion(t *testing.T, wantedVersion int) *details.Details {
|
||||
@ -987,6 +1051,9 @@ func GetDetailsSetForVersion(t *testing.T, wantedVersion int) *details.Details {
|
||||
path.SharePointService: {
|
||||
path.LibrariesCategory,
|
||||
},
|
||||
path.TeamsChatsService: {
|
||||
path.ChatsCategory,
|
||||
},
|
||||
}
|
||||
|
||||
for s, cats := range dataTypes {
|
||||
@ -1060,6 +1127,11 @@ func GetDeetsForVersion(
|
||||
if cat == path.LibrariesCategory {
|
||||
input = sharePointLibraryItemsByVersion
|
||||
}
|
||||
|
||||
case path.TeamsChatsService:
|
||||
if cat == path.ChatsCategory {
|
||||
input = teamsChatsChatItemsByVersion
|
||||
}
|
||||
}
|
||||
|
||||
require.NotNil(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user