ProduceBackupCollections for SharePoint

This commit is contained in:
Abin 2023-08-18 17:39:47 +05:30
parent 2ba349797f
commit 203f7bd18d
18 changed files with 441 additions and 59 deletions

View File

@ -10,6 +10,7 @@ import (
"github.com/alcionai/corso/src/internal/diagnostics" "github.com/alcionai/corso/src/internal/diagnostics"
"github.com/alcionai/corso/src/internal/m365/graph" "github.com/alcionai/corso/src/internal/m365/graph"
"github.com/alcionai/corso/src/internal/m365/service/exchange" "github.com/alcionai/corso/src/internal/m365/service/exchange"
"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/onedrive"
"github.com/alcionai/corso/src/internal/m365/service/sharepoint" "github.com/alcionai/corso/src/internal/m365/service/sharepoint"
"github.com/alcionai/corso/src/internal/operations/inject" "github.com/alcionai/corso/src/internal/operations/inject"
@ -116,6 +117,18 @@ func (ctrl *Controller) ProduceBackupCollections(
return nil, nil, false, err return nil, nil, false, err
} }
case path.GroupsService:
colls, ssmb, canUsePreviousBackup, err = groups.ProduceBackupCollections(
ctx,
bpc,
ctrl.AC,
ctrl.credentials,
ctrl.UpdateStatus,
errs)
if err != nil {
return nil, nil, false, err
}
default: default:
return nil, nil, false, clues.Wrap(clues.New(service.String()), "service not supported").WithClues(ctx) return nil, nil, false, clues.Wrap(clues.New(service.String()), "service not supported").WithClues(ctx)
} }
@ -176,6 +189,10 @@ func verifyBackupInputs(sels selectors.Selector, siteIDs []string) error {
// Exchange and OneDrive user existence now checked in checkServiceEnabled. // Exchange and OneDrive user existence now checked in checkServiceEnabled.
return nil return nil
case selectors.ServiceGroups:
// TODO(meain): can use a proper check in checkServiceEnabled.
return nil
case selectors.ServiceSharePoint: case selectors.ServiceSharePoint:
ids = siteIDs ids = siteIDs
} }
@ -197,7 +214,8 @@ func checkServiceEnabled(
service path.ServiceType, service path.ServiceType,
resource string, resource string,
) (bool, bool, error) { ) (bool, bool, error) {
if service == path.SharePointService { if service == path.SharePointService || service == path.GroupsService {
// TODO(meain): use proper check for groups
// No "enabled" check required for sharepoint // No "enabled" check required for sharepoint
return true, true, nil return true, true, nil
} }

View File

@ -465,3 +465,86 @@ func (suite *SPCollectionIntgSuite) TestCreateSharePointCollection_Lists() {
} }
} }
} }
// ---------------------------------------------------------------------------
// CreateGroupsCollection tests
// ---------------------------------------------------------------------------
type GroupsCollectionIntgSuite struct {
tester.Suite
connector *Controller
user string
}
func TestGroupsCollectionIntgSuite(t *testing.T) {
suite.Run(t, &GroupsCollectionIntgSuite{
Suite: tester.NewIntegrationSuite(
t,
[][]string{tconfig.M365AcctCredEnvs},
),
})
}
func (suite *GroupsCollectionIntgSuite) SetupSuite() {
ctx, flush := tester.NewContext(suite.T())
defer flush()
suite.connector = newController(ctx, suite.T(), resource.Sites, path.GroupsService)
suite.user = tconfig.M365UserID(suite.T())
tester.LogTimeOfTest(suite.T())
}
func (suite *GroupsCollectionIntgSuite) TestCreateGroupsCollection_SharePoint() {
t := suite.T()
ctx, flush := tester.NewContext(t)
defer flush()
var (
groupID = tconfig.M365GroupID(t)
ctrl = newController(ctx, t, resource.Groups, path.GroupsService)
groupIDs = []string{groupID}
)
id, name, err := ctrl.PopulateProtectedResourceIDAndName(ctx, groupID, nil)
require.NoError(t, err, clues.ToCore(err))
sel := selectors.NewGroupsBackup(groupIDs)
// TODO(meain): make use of selectors
sel.Include(sel.LibraryFolders([]string{"test"}, selectors.PrefixMatch()))
sel.SetDiscreteOwnerIDName(id, name)
bpc := inject.BackupProducerConfig{
LastBackupVersion: version.NoBackup,
Options: control.DefaultOptions(),
ProtectedResource: inMock.NewProvider(id, name),
Selector: sel.Selector,
}
collections, excludes, canUsePreviousBackup, err := ctrl.ProduceBackupCollections(
ctx,
bpc,
fault.New(true))
require.NoError(t, err, clues.ToCore(err))
assert.True(t, canUsePreviousBackup, "can use previous backup")
// No excludes yet as this isn't an incremental backup.
assert.True(t, excludes.Empty())
// we don't know an exact count of drives this will produce,
// but it should be more than one.
assert.Greater(t, len(collections), 1)
for _, coll := range collections {
for object := range coll.Items(ctx, fault.New(true)) {
buf := &bytes.Buffer{}
_, err := buf.ReadFrom(object.ToReader())
assert.NoError(t, err, "reading item", clues.ToCore(err))
}
}
status := ctrl.Wait()
assert.NotZero(t, status.Successes)
t.Log(status.String())
}

View File

@ -439,7 +439,7 @@ func (c *Collections) Get(
service, category := c.handler.ServiceCat() service, category := c.handler.ServiceCat()
md, err := graph.MakeMetadataCollection( md, err := graph.MakeMetadataCollection(
c.tenantID, c.tenantID,
c.resourceOwner, c.resourceOwner, // TODO(meain): path fixes: group id
service, service,
category, category,
[]graph.MetadataCollectionEntry{ []graph.MetadataCollectionEntry{

View File

@ -0,0 +1,192 @@
package drive
import (
"context"
"net/http"
"strings"
"github.com/microsoftgraph/msgraph-sdk-go/models"
"github.com/alcionai/corso/src/internal/common/ptr"
odConsts "github.com/alcionai/corso/src/internal/m365/service/onedrive/consts"
"github.com/alcionai/corso/src/pkg/backup/details"
"github.com/alcionai/corso/src/pkg/path"
"github.com/alcionai/corso/src/pkg/selectors"
"github.com/alcionai/corso/src/pkg/services/m365/api"
)
var _ BackupHandler = &groupBackupHandler{}
type groupBackupHandler struct {
groupID string
ac api.Drives
scope selectors.GroupsScope
}
func NewGroupBackupHandler(groupID string, ac api.Drives, scope selectors.GroupsScope) groupBackupHandler {
return groupBackupHandler{groupID, ac, scope}
}
func (h groupBackupHandler) Get(
ctx context.Context,
url string,
headers map[string]string,
) (*http.Response, error) {
return h.ac.Get(ctx, url, headers)
}
func (h groupBackupHandler) PathPrefix(
tenantID, resourceOwner, driveID string,
) (path.Path, error) {
return path.Build(
tenantID,
resourceOwner,
path.GroupsService,
path.LibrariesCategory, // TODO(meain)
false,
odConsts.DrivesPathDir,
driveID,
odConsts.RootPathDir)
}
func (h groupBackupHandler) CanonicalPath(
folders *path.Builder,
tenantID, resourceOwner string,
) (path.Path, error) {
// TODO(meain): resourceOwner
return folders.ToDataLayerGroupPath(tenantID, h.groupID, path.LibrariesCategory, false)
}
func (h groupBackupHandler) ServiceCat() (path.ServiceType, path.CategoryType) {
return path.GroupsService, path.LibrariesCategory
}
func (h groupBackupHandler) NewDrivePager(
resourceOwner string,
fields []string,
) api.DrivePager {
return h.ac.NewSiteDrivePager(resourceOwner, fields)
}
func (h groupBackupHandler) NewItemPager(
driveID, link string,
fields []string,
) api.DriveItemDeltaEnumerator {
return h.ac.NewDriveItemDeltaPager(driveID, link, fields)
}
func (h groupBackupHandler) AugmentItemInfo(
dii details.ItemInfo,
item models.DriveItemable,
size int64,
parentPath *path.Builder,
) details.ItemInfo {
return augmentGroupItemInfo(dii, item, size, parentPath)
}
func (h groupBackupHandler) FormatDisplayPath(
driveName string,
pb *path.Builder,
) string {
return "/" + driveName + "/" + pb.String()
}
func (h groupBackupHandler) NewLocationIDer(
driveID string,
elems ...string,
) details.LocationIDer {
return details.NewSharePointLocationIDer(driveID, elems...)
}
func (h groupBackupHandler) GetItemPermission(
ctx context.Context,
driveID, itemID string,
) (models.PermissionCollectionResponseable, error) {
return h.ac.GetItemPermission(ctx, driveID, itemID)
}
func (h groupBackupHandler) GetItem(
ctx context.Context,
driveID, itemID string,
) (models.DriveItemable, error) {
return h.ac.GetItem(ctx, driveID, itemID)
}
func (h groupBackupHandler) IsAllPass() bool {
// TODO(meain)
return true
}
func (h groupBackupHandler) IncludesDir(dir string) bool {
// TODO(meain)
// return h.scope.Matches(selectors.SharePointGroupFolder, dir)
return true
}
// ---------------------------------------------------------------------------
// Common
// ---------------------------------------------------------------------------
func augmentGroupItemInfo(
dii details.ItemInfo,
item models.DriveItemable,
size int64,
parentPath *path.Builder,
) details.ItemInfo {
var driveName, driveID, creatorEmail string
// TODO: we rely on this info for details/restore lookups,
// so if it's nil we have an issue, and will need an alternative
// way to source the data.
if item.GetCreatedBy() != nil && item.GetCreatedBy().GetUser() != nil {
// User is sometimes not available when created via some
// external applications (like backup/restore solutions)
additionalData := item.GetCreatedBy().GetUser().GetAdditionalData()
ed, ok := additionalData["email"]
if !ok {
ed = additionalData["displayName"]
}
if ed != nil {
creatorEmail = *ed.(*string)
}
}
// gsi := item.GetSharepointIds()
// if gsi != nil {
// siteID = ptr.Val(gsi.GetSiteId())
// weburl = ptr.Val(gsi.GetSiteUrl())
// if len(weburl) == 0 {
// weburl = constructWebURL(item.GetAdditionalData())
// }
// }
if item.GetParentReference() != nil {
driveID = ptr.Val(item.GetParentReference().GetDriveId())
driveName = strings.TrimSpace(ptr.Val(item.GetParentReference().GetName()))
}
var pps string
if parentPath != nil {
pps = parentPath.String()
}
dii.Groups = &details.GroupsInfo{
Created: ptr.Val(item.GetCreatedDateTime()),
DriveID: driveID,
DriveName: driveName,
ItemName: ptr.Val(item.GetName()),
ItemType: details.SharePointLibrary,
Modified: ptr.Val(item.GetLastModifiedDateTime()),
Owner: creatorEmail,
ParentPath: pps,
Size: size,
}
dii.Extension = &details.ExtensionData{}
return dii
}

View File

@ -25,10 +25,9 @@ import (
func CollectLibraries( func CollectLibraries(
ctx context.Context, ctx context.Context,
bpc inject.BackupProducerConfig, bpc inject.BackupProducerConfig,
ad api.Drives, bh drive.BackupHandler,
tenantID string, tenantID string,
ssmb *prefixmatcher.StringSetMatchBuilder, ssmb *prefixmatcher.StringSetMatchBuilder,
scope selectors.SharePointScope,
su support.StatusUpdater, su support.StatusUpdater,
errs *fault.Bus, errs *fault.Bus,
) ([]data.BackupCollection, bool, error) { ) ([]data.BackupCollection, bool, error) {
@ -37,7 +36,7 @@ func CollectLibraries(
var ( var (
collections = []data.BackupCollection{} collections = []data.BackupCollection{}
colls = drive.NewCollections( colls = drive.NewCollections(
drive.NewLibraryBackupHandler(ad, scope), bh,
tenantID, tenantID,
bpc.ProtectedResource.ID(), bpc.ProtectedResource.ID(),
su, su,

View File

@ -170,6 +170,8 @@ func getResourceClient(rc resource.Category, ac api.Client) (*resourceClient, er
return &resourceClient{enum: rc, getter: ac.Users()}, nil return &resourceClient{enum: rc, getter: ac.Users()}, nil
case resource.Sites: case resource.Sites:
return &resourceClient{enum: rc, getter: ac.Sites()}, nil return &resourceClient{enum: rc, getter: ac.Sites()}, nil
case resource.Groups:
return &resourceClient{enum: rc, getter: ac.Groups()}, nil
default: default:
return nil, clues.New("unrecognized owner resource enum").With("resource_enum", rc) return nil, clues.New("unrecognized owner resource enum").With("resource_enum", rc)
} }

View File

@ -6,4 +6,5 @@ const (
UnknownResource Category = "" UnknownResource Category = ""
Users Category = "users" Users Category = "users"
Sites Category = "sites" Sites Category = "sites"
Groups Category = "groups"
) )

View File

@ -5,8 +5,12 @@ import (
"github.com/alcionai/clues" "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/common/prefixmatcher"
"github.com/alcionai/corso/src/internal/common/ptr"
"github.com/alcionai/corso/src/internal/data" "github.com/alcionai/corso/src/internal/data"
"github.com/alcionai/corso/src/internal/m365/collection/drive"
"github.com/alcionai/corso/src/internal/m365/collection/site"
"github.com/alcionai/corso/src/internal/m365/graph" "github.com/alcionai/corso/src/internal/m365/graph"
"github.com/alcionai/corso/src/internal/m365/support" "github.com/alcionai/corso/src/internal/m365/support"
"github.com/alcionai/corso/src/internal/observe" "github.com/alcionai/corso/src/internal/observe"
@ -56,7 +60,41 @@ func ProduceBackupCollections(
var dbcs []data.BackupCollection var dbcs []data.BackupCollection
switch scope.Category().PathType() { switch scope.Category().PathType() {
case path.LibrariesCategory: // TODO case path.LibrariesCategory:
// TODO(meain): Private channels get a separate SharePoint
// site. We should also back those up and not just the
// default one.
resp, err := ac.Stable.
Client().
Groups().
ByGroupId(bpc.ProtectedResource.ID()).
Sites().
BySiteId("root").
Get(ctx, nil)
if err != nil {
return nil, nil, false, clues.Wrap(err, "getting root site for group")
}
pr := idname.NewProvider(ptr.Val(resp.GetId()), ptr.Val(resp.GetName()))
sbpc := inject.BackupProducerConfig{
LastBackupVersion: bpc.LastBackupVersion,
Options: bpc.Options,
ProtectedResource: pr,
Selector: bpc.Selector,
}
dbcs, canUsePreviousBackup, err = site.CollectLibraries(
ctx,
sbpc,
drive.NewGroupBackupHandler(bpc.ProtectedResource.ID(), ac.Drives(), scope),
creds.AzureTenantID,
ssmb,
su,
errs)
if err != nil {
el.AddRecoverable(ctx, err)
continue
}
} }
collections = append(collections, dbcs...) collections = append(collections, dbcs...)
@ -70,7 +108,7 @@ func ProduceBackupCollections(
collections, collections,
creds.AzureTenantID, creds.AzureTenantID,
bpc.ProtectedResource.ID(), bpc.ProtectedResource.ID(),
path.UnknownService, // path.GroupsService path.GroupsService,
categories, categories,
su, su,
errs) errs)

View File

@ -7,6 +7,7 @@ import (
"github.com/alcionai/corso/src/internal/common/prefixmatcher" "github.com/alcionai/corso/src/internal/common/prefixmatcher"
"github.com/alcionai/corso/src/internal/data" "github.com/alcionai/corso/src/internal/data"
"github.com/alcionai/corso/src/internal/m365/collection/drive"
"github.com/alcionai/corso/src/internal/m365/collection/site" "github.com/alcionai/corso/src/internal/m365/collection/site"
"github.com/alcionai/corso/src/internal/m365/graph" "github.com/alcionai/corso/src/internal/m365/graph"
"github.com/alcionai/corso/src/internal/m365/support" "github.com/alcionai/corso/src/internal/m365/support"
@ -79,10 +80,9 @@ func ProduceBackupCollections(
spcs, canUsePreviousBackup, err = site.CollectLibraries( spcs, canUsePreviousBackup, err = site.CollectLibraries(
ctx, ctx,
bpc, bpc,
ac.Drives(), drive.NewLibraryBackupHandler(ac.Drives(), scope),
creds.AzureTenantID, creds.AzureTenantID,
ssmb, ssmb,
scope,
su, su,
errs) errs)
if err != nil { if err != nil {

View File

@ -30,6 +30,7 @@ const (
TestCfgGroupID = "m365groupid" TestCfgGroupID = "m365groupid"
TestCfgUserID = "m365userid" TestCfgUserID = "m365userid"
TestCfgSecondaryUserID = "secondarym365userid" TestCfgSecondaryUserID = "secondarym365userid"
TestCfgSecondaryGroupID = "secondarym365groupid"
TestCfgTertiaryUserID = "tertiarym365userid" TestCfgTertiaryUserID = "tertiarym365userid"
TestCfgLoadTestUserID = "loadtestm365userid" TestCfgLoadTestUserID = "loadtestm365userid"
TestCfgLoadTestOrgUsers = "loadtestm365orgusers" TestCfgLoadTestOrgUsers = "loadtestm365orgusers"

View File

@ -241,6 +241,8 @@ func (pb Builder) ToStreamStorePath(
metadataService = OneDriveMetadataService metadataService = OneDriveMetadataService
case SharePointService: case SharePointService:
metadataService = SharePointMetadataService metadataService = SharePointMetadataService
case GroupsService:
metadataService = GroupsMetadataService
} }
return &dataLayerResourcePath{ return &dataLayerResourcePath{
@ -282,6 +284,8 @@ func (pb Builder) ToServiceCategoryMetadataPath(
metadataService = OneDriveMetadataService metadataService = OneDriveMetadataService
case SharePointService: case SharePointService:
metadataService = SharePointMetadataService metadataService = SharePointMetadataService
case GroupsService:
metadataService = GroupsMetadataService
} }
return &dataLayerResourcePath{ return &dataLayerResourcePath{
@ -346,6 +350,14 @@ func (pb Builder) ToDataLayerSharePointPath(
return pb.ToDataLayerPath(tenant, site, SharePointService, category, isItem) return pb.ToDataLayerPath(tenant, site, SharePointService, category, isItem)
} }
func (pb Builder) ToDataLayerGroupPath(
tenant, group string,
category CategoryType,
isItem bool,
) (Path, error) {
return pb.ToDataLayerPath(tenant, group, GroupsService, category, isItem)
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Stringers and PII Concealer Compliance // Stringers and PII Concealer Compliance
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@ -73,6 +73,9 @@ var serviceCategories = map[ServiceType]map[CategoryType]struct{}{
ListsCategory: {}, ListsCategory: {},
PagesCategory: {}, PagesCategory: {},
}, },
GroupsService: {
LibrariesCategory: {}, // TODO(meain)
},
} }
func validateServiceAndCategoryStrings(s, c string) (ServiceType, CategoryType, error) { func validateServiceAndCategoryStrings(s, c string) (ServiceType, CategoryType, error) {

View File

@ -13,10 +13,12 @@ var piiSafePathElems = pii.MapWithPlurals(
UnknownService.String(), UnknownService.String(),
ExchangeService.String(), ExchangeService.String(),
OneDriveService.String(), OneDriveService.String(),
GroupsService.String(),
SharePointService.String(), SharePointService.String(),
ExchangeMetadataService.String(), ExchangeMetadataService.String(),
OneDriveMetadataService.String(), OneDriveMetadataService.String(),
SharePointMetadataService.String(), SharePointMetadataService.String(),
GroupsMetadataService.String(),
// categories // categories
UnknownCategory.String(), UnknownCategory.String(),

View File

@ -328,6 +328,13 @@ func (suite *DataLayerResourcePath) TestToServiceCategoryMetadataPath() {
expectedService: path.SharePointMetadataService, expectedService: path.SharePointMetadataService,
check: assert.NoError, check: assert.NoError,
}, },
{
name: "Passes",
service: path.GroupsService,
category: path.LibrariesCategory,
expectedService: path.GroupsMetadataService,
check: assert.NoError,
},
} }
for _, test := range table { for _, test := range table {

View File

@ -9,6 +9,7 @@ import (
"github.com/alcionai/corso/src/pkg/backup/details" "github.com/alcionai/corso/src/pkg/backup/details"
"github.com/alcionai/corso/src/pkg/backup/identity" "github.com/alcionai/corso/src/pkg/backup/identity"
"github.com/alcionai/corso/src/pkg/fault" "github.com/alcionai/corso/src/pkg/fault"
"github.com/alcionai/corso/src/pkg/filters"
"github.com/alcionai/corso/src/pkg/path" "github.com/alcionai/corso/src/pkg/path"
) )
@ -204,8 +205,8 @@ func (s *groups) Scopes() []GroupsScope {
// ------------------- // -------------------
// Scope Factories // Scope Factories
// Produces one or more Groups site scopes. // Produces one or more Groups scopes.
// One scope is created per site entry. // One scope is created per group entry.
// If any slice contains selectors.Any, that slice is reduced to [selectors.Any] // If any slice contains selectors.Any, that slice is reduced to [selectors.Any]
// If any slice contains selectors.None, that slice is reduced to [selectors.None] // If any slice contains selectors.None, that slice is reduced to [selectors.None]
// If any slice is empty, it defaults to [selectors.None] // If any slice is empty, it defaults to [selectors.None]
@ -214,38 +215,57 @@ func (s *groups) AllData() []GroupsScope {
scopes = append( scopes = append(
scopes, scopes,
makeScope[GroupsScope](GroupsTODOContainer, Any())) makeScope[GroupsScope](GroupsLibraryFolder, Any()))
return scopes return scopes
} }
// TODO produces one or more Groups TODO scopes. // Library produces one or more Group library scopes, where the library
// matches upon a given drive by ID or Name. In order to ensure library selection
// this should always be embedded within the Filter() set; include(Library()) will
// select all items in the library without further filtering.
// If any slice contains selectors.Any, that slice is reduced to [selectors.Any] // If any slice contains selectors.Any, that slice is reduced to [selectors.Any]
// If any slice contains selectors.None, that slice is reduced to [selectors.None] // If any slice contains selectors.None, that slice is reduced to [selectors.None]
// Any empty slice defaults to [selectors.None] // If any slice is empty, it defaults to [selectors.None]
func (s *groups) TODO(lists []string, opts ...option) []GroupsScope { func (s *groups) Library(library string) []GroupsScope {
return []GroupsScope{
makeInfoScope[GroupsScope](
GroupsLibraryItem,
SharePointInfoLibraryDrive, // TODO(meain)
[]string{library},
filters.Equal),
}
}
// LibraryFolders produces one or more SharePoint libraryFolder scopes.
// If any slice contains selectors.Any, that slice is reduced to [selectors.Any]
// If any slice contains selectors.None, that slice is reduced to [selectors.None]
// If any slice is empty, it defaults to [selectors.None]
func (s *groups) LibraryFolders(libraryFolders []string, opts ...option) []GroupsScope {
var ( var (
scopes = []GroupsScope{} scopes = []GroupsScope{}
os = append([]option{pathComparator()}, opts...) os = append([]option{pathComparator()}, opts...)
) )
scopes = append(scopes, makeScope[GroupsScope](GroupsTODOContainer, lists, os...)) scopes = append(
scopes,
makeScope[GroupsScope](GroupsLibraryFolder, libraryFolders, os...))
return scopes return scopes
} }
// ListTODOItemsItems produces one or more Groups TODO item scopes. // LibraryItems produces one or more Groups library item scopes.
// If any slice contains selectors.Any, that slice is reduced to [selectors.Any] // If any slice contains selectors.Any, that slice is reduced to [selectors.Any]
// If any slice contains selectors.None, that slice is reduced to [selectors.None] // If any slice contains selectors.None, that slice is reduced to [selectors.None]
// If any slice is empty, it defaults to [selectors.None] // If any slice is empty, it defaults to [selectors.None]
// options are only applied to the list scopes. // options are only applied to the library scopes.
func (s *groups) TODOItems(lists, items []string, opts ...option) []GroupsScope { func (s *groups) LibraryItems(libraries, items []string, opts ...option) []GroupsScope {
scopes := []GroupsScope{} scopes := []GroupsScope{}
scopes = append( scopes = append(
scopes, scopes,
makeScope[GroupsScope](GroupsTODOItem, items, defaultItemOptions(s.Cfg)...). makeScope[GroupsScope](GroupsLibraryItem, items, defaultItemOptions(s.Cfg)...).
set(GroupsTODOContainer, lists, opts...)) set(GroupsLibraryFolder, libraries, opts...))
return scopes return scopes
} }
@ -270,21 +290,27 @@ const (
GroupsCategoryUnknown groupsCategory = "" GroupsCategoryUnknown groupsCategory = ""
// types of data in Groups // types of data in Groups
GroupsGroup groupsCategory = "GroupsGroup" GroupsGroup groupsCategory = "GroupsGroup"
GroupsTODOContainer groupsCategory = "GroupsTODOContainer"
GroupsTODOItem groupsCategory = "GroupsTODOItem" // sharepoint
GroupsLibraryFolder groupsCategory = "GroupsLibraryFolder"
GroupsLibraryItem groupsCategory = "GroupsLibraryItem"
// messages
// GroupsTeamChannel groupsCategory = "GroupsTeamChannel"
// GroupsTeamChannelMessages groupsCategory = "GroupsTeamChannelMessages"
// details.itemInfo comparables // details.itemInfo comparables
// library drive selection // library drive selection
GroupsInfoSiteLibraryDrive groupsCategory = "GroupsInfoSiteLibraryDrive" GroupsInfoSiteLibraryDrive groupsCategory = "GroupsInfoSiteLibraryDrive" // TODO(meain)
) )
// groupsLeafProperties describes common metadata of the leaf categories // groupsLeafProperties describes common metadata of the leaf categories
var groupsLeafProperties = map[categorizer]leafProperty{ var groupsLeafProperties = map[categorizer]leafProperty{
GroupsTODOItem: { // the root category must be represented, even though it isn't a leaf GroupsLibraryItem: {
pathKeys: []categorizer{GroupsTODOContainer, GroupsTODOItem}, pathKeys: []categorizer{GroupsLibraryFolder, GroupsLibraryItem},
pathType: path.UnknownCategory, pathType: path.LibrariesCategory,
}, },
GroupsGroup: { // the root category must be represented, even though it isn't a leaf GroupsGroup: { // the root category must be represented, even though it isn't a leaf
pathKeys: []categorizer{GroupsGroup}, pathKeys: []categorizer{GroupsGroup},
@ -303,8 +329,8 @@ func (c groupsCategory) String() string {
// Ex: ServiceUser.leafCat() => ServiceUser // Ex: ServiceUser.leafCat() => ServiceUser
func (c groupsCategory) leafCat() categorizer { func (c groupsCategory) leafCat() categorizer {
switch c { switch c {
case GroupsTODOContainer, GroupsInfoSiteLibraryDrive: case GroupsLibraryFolder, GroupsLibraryItem, GroupsInfoSiteLibraryDrive:
return GroupsTODOItem return GroupsLibraryItem
} }
return c return c
@ -334,7 +360,7 @@ func (c groupsCategory) isLeaf() bool {
// pathValues transforms the two paths to maps of identified properties. // pathValues transforms the two paths to maps of identified properties.
// //
// Example: // Example:
// [tenantID, service, siteID, category, folder, itemID] // [tenantID, service, groupID, site, siteID, category, folder, itemID]
// => {spFolder: folder, spItemID: itemID} // => {spFolder: folder, spItemID: itemID}
func (c groupsCategory) pathValues( func (c groupsCategory) pathValues(
repo path.Path, repo path.Path,
@ -348,13 +374,13 @@ func (c groupsCategory) pathValues(
) )
switch c { switch c {
case GroupsTODOContainer, GroupsTODOItem: case GroupsLibraryFolder, GroupsLibraryItem:
if ent.Groups == nil { if ent.Groups == nil {
return nil, clues.New("no Groups ItemInfo in details") return nil, clues.New("no Groups ItemInfo in details")
} }
folderCat, itemCat = GroupsTODOContainer, GroupsTODOItem folderCat, itemCat = GroupsLibraryFolder, GroupsLibraryItem
rFld = ent.Groups.ParentPath rFld = ent.Groups.ParentPath // TODO(meain)
default: default:
return nil, clues.New("unrecognized groupsCategory").With("category", c) return nil, clues.New("unrecognized groupsCategory").With("category", c)
@ -451,7 +477,7 @@ func (s GroupsScope) set(cat groupsCategory, v []string, opts ...option) GroupsS
os := []option{} os := []option{}
switch cat { switch cat {
case GroupsTODOContainer: case GroupsLibraryFolder:
os = append(os, pathComparator()) os = append(os, pathComparator())
} }
@ -462,10 +488,10 @@ func (s GroupsScope) set(cat groupsCategory, v []string, opts ...option) GroupsS
func (s GroupsScope) setDefaults() { func (s GroupsScope) setDefaults() {
switch s.Category() { switch s.Category() {
case GroupsGroup: case GroupsGroup:
s[GroupsTODOContainer.String()] = passAny s[GroupsLibraryFolder.String()] = passAny
s[GroupsTODOItem.String()] = passAny s[GroupsLibraryItem.String()] = passAny
case GroupsTODOContainer: case GroupsLibraryFolder:
s[GroupsTODOItem.String()] = passAny s[GroupsLibraryItem.String()] = passAny
} }
} }
@ -485,7 +511,7 @@ func (s groups) Reduce(
deets, deets,
s.Selector, s.Selector,
map[path.CategoryType]groupsCategory{ map[path.CategoryType]groupsCategory{
path.UnknownCategory: GroupsTODOItem, path.LibrariesCategory: GroupsLibraryItem,
}, },
errs) errs)
} }

View File

@ -32,6 +32,7 @@ var serviceToPathType = map[service]path.ServiceType{
ServiceExchange: path.ExchangeService, ServiceExchange: path.ExchangeService,
ServiceOneDrive: path.OneDriveService, ServiceOneDrive: path.OneDriveService,
ServiceSharePoint: path.SharePointService, ServiceSharePoint: path.SharePointService,
ServiceGroups: path.GroupsService,
} }
var ( var (

View File

@ -7,6 +7,7 @@ import (
msgraphgocore "github.com/microsoftgraph/msgraph-sdk-go-core" msgraphgocore "github.com/microsoftgraph/msgraph-sdk-go-core"
"github.com/microsoftgraph/msgraph-sdk-go/models" "github.com/microsoftgraph/msgraph-sdk-go/models"
"github.com/alcionai/corso/src/internal/common/ptr"
"github.com/alcionai/corso/src/internal/common/str" "github.com/alcionai/corso/src/internal/common/str"
"github.com/alcionai/corso/src/internal/common/tform" "github.com/alcionai/corso/src/internal/common/tform"
"github.com/alcionai/corso/src/internal/m365/graph" "github.com/alcionai/corso/src/internal/m365/graph"
@ -27,7 +28,7 @@ func (c Client) Groups() Groups {
return Groups{c} return Groups{c}
} }
// On creation of each Teams team a corrsponding group gets created. // On creation of each Teams team a corresponding group gets created.
// The group acts as the protected resource, and all teams data like events, // The group acts as the protected resource, and all teams data like events,
// drive and mail messages are owned by that group. // drive and mail messages are owned by that group.
@ -167,3 +168,14 @@ func IsTeam(ctx context.Context, mg models.Groupable) bool {
return false return false
} }
// GetIDAndName looks up the group matching the given ID, and returns
// its canonical ID and the name.
func (c Groups) GetIDAndName(ctx context.Context, groupID string) (string, string, error) {
s, err := c.GetByID(ctx, groupID)
if err != nil {
return "", "", err
}
return ptr.Val(s.GetId()), ptr.Val(s.GetDisplayName()), nil
}

View File

@ -107,7 +107,7 @@ func (suite *GroupsIntgSuite) TestGetAll() {
Groups(). Groups().
GetAll(ctx, fault.New(true)) GetAll(ctx, fault.New(true))
require.NoError(t, err) require.NoError(t, err)
require.NotZero(t, len(groups), "must find at least one group") require.NotZero(t, len(groups), "must have at least one group")
} }
func (suite *GroupsIntgSuite) TestGroups_GetByID() { func (suite *GroupsIntgSuite) TestGroups_GetByID() {
@ -122,34 +122,19 @@ func (suite *GroupsIntgSuite) TestGroups_GetByID() {
expectErr func(*testing.T, error) expectErr func(*testing.T, error)
}{ }{
{ {
name: "3 part id", name: "valid id",
id: groupID, id: groupID,
expectErr: func(t *testing.T, err error) { expectErr: func(t *testing.T, err error) {
assert.NoError(t, err, clues.ToCore(err)) assert.NoError(t, err, clues.ToCore(err))
}, },
}, },
{ {
name: "malformed id", name: "invalid id",
id: uuid.NewString(), id: uuid.NewString(),
expectErr: func(t *testing.T, err error) { expectErr: func(t *testing.T, err error) {
assert.Error(t, err, clues.ToCore(err)) assert.Error(t, err, clues.ToCore(err))
}, },
}, },
{
name: "random id",
id: uuid.NewString() + "," + uuid.NewString(),
expectErr: func(t *testing.T, err error) {
assert.Error(t, err, clues.ToCore(err))
},
},
{
name: "malformed url",
id: "barunihlda",
expectErr: func(t *testing.T, err error) {
assert.Error(t, err, clues.ToCore(err))
},
},
} }
for _, test := range table { for _, test := range table {
suite.Run(test.name, func() { suite.Run(test.name, func() {