Part 1 of two changes I've been carting around since the hackathon. This PR moves the /cli/config package into /pkg/config. This allows sdk consumers to access corso configuration handling, intead of requiring the CLI package to access it. --- #### Does this PR need a docs update or release note? - [x] ⛔ No #### Type of change - [x] 🤖 Supportability/Tests #### Test Plan - [x] 💪 Manual - [x] ⚡ Unit test - [x] 💚 E2E
893 lines
30 KiB
Go
893 lines
30 KiB
Go
package selectors
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"github.com/alcionai/clues"
|
|
|
|
"github.com/alcionai/corso/src/pkg/backup/details"
|
|
"github.com/alcionai/corso/src/pkg/backup/identity"
|
|
"github.com/alcionai/corso/src/pkg/dttm"
|
|
"github.com/alcionai/corso/src/pkg/fault"
|
|
"github.com/alcionai/corso/src/pkg/filters"
|
|
"github.com/alcionai/corso/src/pkg/path"
|
|
)
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Selectors
|
|
// ---------------------------------------------------------------------------
|
|
|
|
type (
|
|
// groups provides an api for selecting
|
|
// data scopes applicable to the groups service.
|
|
groups struct {
|
|
Selector
|
|
}
|
|
|
|
// groups provides an api for selecting
|
|
// data scopes applicable to the groups service,
|
|
// plus backup-specific methods.
|
|
GroupsBackup struct {
|
|
groups
|
|
}
|
|
|
|
// GroupsRestorep provides an api for selecting
|
|
// data scopes applicable to the Groups service,
|
|
// plus restore-specific methods.
|
|
GroupsRestore struct {
|
|
groups
|
|
}
|
|
)
|
|
|
|
var (
|
|
_ Reducer = &GroupsRestore{}
|
|
_ pathCategorier = &GroupsRestore{}
|
|
_ reasoner = &GroupsRestore{}
|
|
)
|
|
|
|
// NewGroupsBackup produces a new Selector with the service set to ServiceGroups.
|
|
func NewGroupsBackup(resources []string) *GroupsBackup {
|
|
src := GroupsBackup{
|
|
groups{
|
|
newSelector(ServiceGroups, resources),
|
|
},
|
|
}
|
|
|
|
return &src
|
|
}
|
|
|
|
// ToGroupsBackup transforms the generic selector into an GroupsBackup.
|
|
// Errors if the service defined by the selector is not ServiceGroups.
|
|
func (s Selector) ToGroupsBackup() (*GroupsBackup, error) {
|
|
if s.Service != ServiceGroups {
|
|
return nil, badCastErr(ServiceGroups, s.Service)
|
|
}
|
|
|
|
src := GroupsBackup{groups{s}}
|
|
|
|
return &src, nil
|
|
}
|
|
|
|
func (s GroupsBackup) SplitByResourceOwner(resources []string) []GroupsBackup {
|
|
sels := splitByProtectedResource[GroupsScope](s.Selector, resources, GroupsGroup)
|
|
|
|
ss := make([]GroupsBackup, 0, len(sels))
|
|
for _, sel := range sels {
|
|
ss = append(ss, GroupsBackup{groups{sel}})
|
|
}
|
|
|
|
return ss
|
|
}
|
|
|
|
// NewGroupsRestore produces a new Selector with the service set to ServiceGroups.
|
|
func NewGroupsRestore(resources []string) *GroupsRestore {
|
|
src := GroupsRestore{
|
|
groups{
|
|
newSelector(ServiceGroups, resources),
|
|
},
|
|
}
|
|
|
|
return &src
|
|
}
|
|
|
|
// ToGroupsRestore transforms the generic selector into an GroupsRestore.
|
|
// Errors if the service defined by the selector is not ServiceGroups.
|
|
func (s Selector) ToGroupsRestore() (*GroupsRestore, error) {
|
|
if s.Service != ServiceGroups {
|
|
return nil, badCastErr(ServiceGroups, s.Service)
|
|
}
|
|
|
|
src := GroupsRestore{groups{s}}
|
|
|
|
return &src, nil
|
|
}
|
|
|
|
func (s GroupsRestore) SplitByResourceOwner(resources []string) []GroupsRestore {
|
|
sels := splitByProtectedResource[GroupsScope](s.Selector, resources, GroupsGroup)
|
|
|
|
ss := make([]GroupsRestore, 0, len(sels))
|
|
for _, sel := range sels {
|
|
ss = append(ss, GroupsRestore{groups{sel}})
|
|
}
|
|
|
|
return ss
|
|
}
|
|
|
|
// PathCategories produces the aggregation of discrete resources described by each type of scope.
|
|
func (s groups) PathCategories() selectorPathCategories {
|
|
return selectorPathCategories{
|
|
Excludes: pathCategoriesIn[GroupsScope, groupsCategory](s.Excludes),
|
|
Filters: pathCategoriesIn[GroupsScope, groupsCategory](s.Filters),
|
|
Includes: pathCategoriesIn[GroupsScope, groupsCategory](s.Includes),
|
|
}
|
|
}
|
|
|
|
// Reasons returns a deduplicated set of the backup reasons produced
|
|
// using the selector's discrete owner and each scopes' service and
|
|
// category types.
|
|
func (s groups) Reasons(tenantID string, useOwnerNameForID bool) []identity.Reasoner {
|
|
return reasonsFor(s, tenantID, useOwnerNameForID)
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Stringers and Concealers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func (s GroupsScope) Conceal() string { return conceal(s) }
|
|
func (s GroupsScope) Format(fs fmt.State, r rune) { format(s, fs, r) }
|
|
func (s GroupsScope) String() string { return conceal(s) }
|
|
func (s GroupsScope) PlainString() string { return plainString(s) }
|
|
|
|
// -------------------
|
|
// Scope Factories
|
|
|
|
// Include appends the provided scopes to the selector's inclusion set.
|
|
// Data is included if it matches ANY inclusion.
|
|
// The inclusion set is later filtered (all included data must pass ALL
|
|
// filters) and excluded (all included data must not match ANY exclusion).
|
|
// Data is included if it matches ANY inclusion (of the same data category).
|
|
//
|
|
// All parts of the scope must match for data to be exclucded.
|
|
// Ex: File(s1, f1, i1) => only excludes an item if it is owned by site s1,
|
|
// located in folder f1, and ID'd as i1. Use selectors.Any() to wildcard
|
|
// a scope value. No value will match if selectors.None() is provided.
|
|
//
|
|
// Group-level scopes will automatically apply the Any() wildcard to
|
|
// child properties.
|
|
// ex: Site(u1) automatically cascades to all folders and files owned
|
|
// by s1.
|
|
func (s *groups) Include(scopes ...[]GroupsScope) {
|
|
s.Includes = appendScopes(s.Includes, scopes...)
|
|
}
|
|
|
|
// Exclude appends the provided scopes to the selector's exclusion set.
|
|
// Every Exclusion scope applies globally, affecting all inclusion scopes.
|
|
// Data is excluded if it matches ANY exclusion.
|
|
//
|
|
// All parts of the scope must match for data to be exclucded.
|
|
// Ex: File(s1, f1, i1) => only excludes an item if it is owned by site s1,
|
|
// located in folder f1, and ID'd as i1. Use selectors.Any() to wildcard
|
|
// a scope value. No value will match if selectors.None() is provided.
|
|
//
|
|
// Group-level scopes will automatically apply the Any() wildcard to
|
|
// child properties.
|
|
// ex: Site(u1) automatically cascades to all folders and files owned
|
|
// by s1.
|
|
func (s *groups) Exclude(scopes ...[]GroupsScope) {
|
|
s.Excludes = appendScopes(s.Excludes, scopes...)
|
|
}
|
|
|
|
// Filter appends the provided scopes to the selector's filters set.
|
|
// A selector with >0 filters and 0 inclusions will include any data
|
|
// that passes all filters.
|
|
// A selector with >0 filters and >0 inclusions will reduce the
|
|
// inclusion set to only the data that passes all filters.
|
|
// Data is retained if it passes ALL filters.
|
|
//
|
|
// All parts of the scope must match for data to be exclucded.
|
|
// Ex: File(s1, f1, i1) => only excludes an item if it is owned by site s1,
|
|
// located in folder f1, and ID'd as i1. Use selectors.Any() to wildcard
|
|
// a scope value. No value will match if selectors.None() is provided.
|
|
//
|
|
// Group-level scopes will automatically apply the Any() wildcard to
|
|
// child properties.
|
|
// ex: Site(u1) automatically cascades to all folders and files owned
|
|
// by s1.
|
|
func (s *groups) Filter(scopes ...[]GroupsScope) {
|
|
s.Filters = appendScopes(s.Filters, scopes...)
|
|
}
|
|
|
|
// Scopes retrieves the list of groupsScopes in the selector.
|
|
func (s *groups) Scopes() []GroupsScope {
|
|
return scopes[GroupsScope](s.Selector)
|
|
}
|
|
|
|
// -------------------
|
|
// Scope Factories
|
|
|
|
// Produces one or more Groups scopes.
|
|
// 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.None, that slice is reduced to [selectors.None]
|
|
// If any slice is empty, it defaults to [selectors.None]
|
|
func (s *groups) AllData() []GroupsScope {
|
|
scopes := []GroupsScope{}
|
|
|
|
scopes = append(
|
|
scopes,
|
|
makeScope[GroupsScope](GroupsLibraryFolder, Any()),
|
|
makeScope[GroupsScope](GroupsChannel, Any()))
|
|
// TODO: enable conversations in all-data backups
|
|
// makeScope[GroupsScope](GroupsConversation, Any()))
|
|
|
|
return scopes
|
|
}
|
|
|
|
// Channels produces one or more SharePoint channel scopes, where the channel
|
|
// matches upon a given channel by ID or Name.
|
|
// 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) Channels(channels []string, opts ...option) []GroupsScope {
|
|
var (
|
|
scopes = []GroupsScope{}
|
|
os = append([]option{pathComparator()}, opts...)
|
|
)
|
|
|
|
scopes = append(
|
|
scopes,
|
|
makeScope[GroupsScope](GroupsChannel, channels, os...))
|
|
|
|
return scopes
|
|
}
|
|
|
|
// ChannelMessages produces one or more Groups channel message 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) ChannelMessages(channels, messages []string, opts ...option) []GroupsScope {
|
|
var (
|
|
scopes = []GroupsScope{}
|
|
os = append([]option{pathComparator()}, opts...)
|
|
)
|
|
|
|
scopes = append(
|
|
scopes,
|
|
makeScope[GroupsScope](GroupsChannelMessage, messages, os...).
|
|
set(GroupsChannel, channels, opts...))
|
|
|
|
return scopes
|
|
}
|
|
|
|
// Conversations produces one or more SharePoint conversation scopes, where the
|
|
// conversation matches with a given conversation by ID or Topic.
|
|
// 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) Conversation(conversations []string, opts ...option) []GroupsScope {
|
|
var (
|
|
scopes = []GroupsScope{}
|
|
os = append([]option{pathComparator()}, opts...)
|
|
)
|
|
|
|
scopes = append(
|
|
scopes,
|
|
makeScope[GroupsScope](GroupsConversation, conversations, os...))
|
|
|
|
return scopes
|
|
}
|
|
|
|
// ConversationPosts produces one or more Groups conversation post 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) ConversationPosts(conversations, posts []string, opts ...option) []GroupsScope {
|
|
var (
|
|
scopes = []GroupsScope{}
|
|
os = append([]option{pathComparator()}, opts...)
|
|
)
|
|
|
|
scopes = append(
|
|
scopes,
|
|
makeScope[GroupsScope](GroupsConversationPost, posts, os...).
|
|
set(GroupsConversation, conversations, opts...))
|
|
|
|
return scopes
|
|
}
|
|
|
|
// Sites produces one or more Groups site scopes, where the site
|
|
// matches upon a given site by ID or URL.
|
|
// 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) Site(site string) []GroupsScope {
|
|
return []GroupsScope{
|
|
makeInfoScope[GroupsScope](
|
|
GroupsLibraryItem,
|
|
GroupsInfoSite,
|
|
[]string{site},
|
|
filters.Equal),
|
|
}
|
|
}
|
|
|
|
// 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.None, that slice is reduced to [selectors.None]
|
|
// If any slice is empty, it defaults to [selectors.None]
|
|
func (s *groups) Library(library string) []GroupsScope {
|
|
return []GroupsScope{
|
|
makeInfoScope[GroupsScope](
|
|
GroupsLibraryItem,
|
|
GroupsInfoSiteLibraryDrive,
|
|
[]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 (
|
|
scopes = []GroupsScope{}
|
|
os = append([]option{pathComparator()}, opts...)
|
|
)
|
|
|
|
scopes = append(
|
|
scopes,
|
|
makeScope[GroupsScope](GroupsLibraryFolder, libraryFolders, os...))
|
|
|
|
return 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.None, that slice is reduced to [selectors.None]
|
|
// If any slice is empty, it defaults to [selectors.None]
|
|
// options are only applied to the library scopes.
|
|
func (s *groups) LibraryItems(libraries, items []string, opts ...option) []GroupsScope {
|
|
scopes := []GroupsScope{}
|
|
|
|
scopes = append(
|
|
scopes,
|
|
makeScope[GroupsScope](GroupsLibraryItem, items, defaultItemOptions(s.Cfg)...).
|
|
set(GroupsLibraryFolder, libraries, opts...))
|
|
|
|
return scopes
|
|
}
|
|
|
|
// Lists produces one or more Groups list 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]
|
|
// Any empty slice defaults to [selectors.None]
|
|
func (s *groups) Lists(lists []string, opts ...option) []GroupsScope {
|
|
var (
|
|
scopes = []GroupsScope{}
|
|
os = append([]option{pathComparator()}, opts...)
|
|
)
|
|
|
|
scopes = append(scopes, makeScope[GroupsScope](GroupsList, lists, os...))
|
|
|
|
return scopes
|
|
}
|
|
|
|
// ListItems produces one or more Groups list item 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]
|
|
// options are only applied to the list scopes.
|
|
func (s *groups) ListItems(lists, items []string, opts ...option) []GroupsScope {
|
|
scopes := []GroupsScope{}
|
|
|
|
scopes = append(
|
|
scopes,
|
|
makeScope[GroupsScope](GroupsListItem, items, defaultItemOptions(s.Cfg)...).
|
|
set(GroupsList, lists, opts...))
|
|
|
|
return scopes
|
|
}
|
|
|
|
// Pages produces one or more Groups page 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) Pages(pages []string, opts ...option) []GroupsScope {
|
|
var (
|
|
scopes = []GroupsScope{}
|
|
os = append([]option{pathComparator()}, opts...)
|
|
)
|
|
|
|
scopes = append(scopes, makeScope[GroupsScope](GroupsPageFolder, pages, os...))
|
|
|
|
return scopes
|
|
}
|
|
|
|
// PageItems produces one or more Groups page item 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]
|
|
// options are only applied to the page scopes.
|
|
func (s *groups) PageItems(pages, items []string, opts ...option) []GroupsScope {
|
|
scopes := []GroupsScope{}
|
|
|
|
scopes = append(
|
|
scopes,
|
|
makeScope[GroupsScope](GroupsPage, items).
|
|
set(GroupsPageFolder, pages, opts...))
|
|
|
|
return scopes
|
|
}
|
|
|
|
// -------------------
|
|
// ItemInfo Factories
|
|
|
|
func (s *groups) CreatedAfter(timeStrings string) []GroupsScope {
|
|
return []GroupsScope{
|
|
makeInfoScope[GroupsScope](
|
|
GroupsLibraryItem,
|
|
GroupsInfoLibraryItemCreatedAfter,
|
|
[]string{timeStrings},
|
|
filters.Less),
|
|
}
|
|
}
|
|
|
|
func (s *groups) CreatedBefore(timeStrings string) []GroupsScope {
|
|
return []GroupsScope{
|
|
makeInfoScope[GroupsScope](
|
|
GroupsLibraryItem,
|
|
GroupsInfoLibraryItemCreatedBefore,
|
|
[]string{timeStrings},
|
|
filters.Greater),
|
|
}
|
|
}
|
|
|
|
func (s *groups) ModifiedAfter(timeStrings string) []GroupsScope {
|
|
return []GroupsScope{
|
|
makeInfoScope[GroupsScope](
|
|
GroupsLibraryItem,
|
|
GroupsInfoLibraryItemModifiedAfter,
|
|
[]string{timeStrings},
|
|
filters.Less),
|
|
}
|
|
}
|
|
|
|
func (s *groups) ModifiedBefore(timeStrings string) []GroupsScope {
|
|
return []GroupsScope{
|
|
makeInfoScope[GroupsScope](
|
|
GroupsLibraryItem,
|
|
GroupsInfoLibraryItemModifiedBefore,
|
|
[]string{timeStrings},
|
|
filters.Greater),
|
|
}
|
|
}
|
|
|
|
// MessageCreator produces one or more groups channelMessage info scopes.
|
|
// Matches any channel message created by the specified user.
|
|
// 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 *GroupsRestore) MessageCreator(creator string) []GroupsScope {
|
|
return []GroupsScope{
|
|
makeInfoScope[GroupsScope](
|
|
GroupsChannelMessage,
|
|
GroupsInfoChannelMessageCreator,
|
|
[]string{creator},
|
|
filters.In),
|
|
}
|
|
}
|
|
|
|
// MessageCreatedAfter produces a channel message created-after info scope.
|
|
// Matches any message where the creation time is after the timestring.
|
|
// If the input equals selectors.Any, the scope will match all times.
|
|
// If the input is empty or selectors.None, the scope will always fail comparisons.
|
|
func (s *GroupsRestore) MessageCreatedAfter(timeStrings string) []GroupsScope {
|
|
return []GroupsScope{
|
|
makeInfoScope[GroupsScope](
|
|
GroupsChannelMessage,
|
|
GroupsInfoChannelMessageCreatedAfter,
|
|
[]string{timeStrings},
|
|
filters.Less),
|
|
}
|
|
}
|
|
|
|
// MessageCreatedBefore produces a channel message created-before info scope.
|
|
// Matches any message where the creation time is after the timestring.
|
|
// If the input equals selectors.Any, the scope will match all times.
|
|
// If the input is empty or selectors.None, the scope will always fail comparisons.
|
|
func (s *GroupsRestore) MessageCreatedBefore(timeStrings string) []GroupsScope {
|
|
return []GroupsScope{
|
|
makeInfoScope[GroupsScope](
|
|
GroupsChannelMessage,
|
|
GroupsInfoChannelMessageCreatedBefore,
|
|
[]string{timeStrings},
|
|
filters.Greater),
|
|
}
|
|
}
|
|
|
|
// MessageLastReplyAfter produces a channel message last-response-after info scope.
|
|
// Matches any message where last response time is after the timestring.
|
|
// If the input equals selectors.Any, the scope will match all times.
|
|
// If the input is empty or selectors.None, the scope will always fail comparisons.
|
|
func (s *GroupsRestore) MessageLastReplyAfter(timeStrings string) []GroupsScope {
|
|
return []GroupsScope{
|
|
makeInfoScope[GroupsScope](
|
|
GroupsChannelMessage,
|
|
GroupsInfoChannelMessageLastReplyAfter,
|
|
[]string{timeStrings},
|
|
filters.Less),
|
|
}
|
|
}
|
|
|
|
// MessageLastReplyBefore produces a channel message last-response-before info scope.
|
|
// Matches any message where last response time is after the timestring.
|
|
// If the input equals selectors.Any, the scope will match all times.
|
|
// If the input is empty or selectors.None, the scope will always fail comparisons.
|
|
func (s *GroupsRestore) MessageLastReplyBefore(timeStrings string) []GroupsScope {
|
|
return []GroupsScope{
|
|
makeInfoScope[GroupsScope](
|
|
GroupsChannelMessage,
|
|
GroupsInfoChannelMessageLastReplyBefore,
|
|
[]string{timeStrings},
|
|
filters.Greater),
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Categories
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// groupsCategory enumerates the type of the lowest level
|
|
// of data () in a scope.
|
|
type groupsCategory string
|
|
|
|
// interface compliance checks
|
|
var _ categorizer = GroupsCategoryUnknown
|
|
|
|
const (
|
|
GroupsCategoryUnknown groupsCategory = ""
|
|
|
|
// types of data in Groups
|
|
GroupsGroup groupsCategory = "GroupsGroup"
|
|
GroupsChannel groupsCategory = "GroupsChannel"
|
|
GroupsChannelMessage groupsCategory = "GroupsChannelMessage"
|
|
GroupsConversation groupsCategory = "GroupsConversation"
|
|
GroupsConversationPost groupsCategory = "GroupsConversationPost"
|
|
GroupsLibraryFolder groupsCategory = "GroupsLibraryFolder"
|
|
GroupsLibraryItem groupsCategory = "GroupsLibraryItem"
|
|
GroupsList groupsCategory = "GroupsList"
|
|
GroupsListItem groupsCategory = "GroupsListItem"
|
|
GroupsPageFolder groupsCategory = "GroupsPageFolder"
|
|
GroupsPage groupsCategory = "GroupsPage"
|
|
|
|
// details.itemInfo comparables
|
|
GroupsInfoLibraryItemCreatedAfter groupsCategory = "GroupsInfoLibraryItemCreatedAfter"
|
|
GroupsInfoLibraryItemCreatedBefore groupsCategory = "GroupsInfoLibraryItemCreatedBefore"
|
|
GroupsInfoLibraryItemModifiedAfter groupsCategory = "GroupsInfoLibraryItemModifiedAfter"
|
|
GroupsInfoLibraryItemModifiedBefore groupsCategory = "GroupsInfoLibraryItemModifiedBefore"
|
|
|
|
// channel and drive selection
|
|
GroupsInfoSite groupsCategory = "GroupsInfoSite"
|
|
GroupsInfoSiteLibraryDrive groupsCategory = "GroupsInfoSiteLibraryDrive"
|
|
|
|
// data contained within details.ItemInfo
|
|
GroupsInfoChannelMessageCreatedAfter groupsCategory = "GroupsInfoChannelMessageCreatedAfter"
|
|
GroupsInfoChannelMessageCreatedBefore groupsCategory = "GroupsInfoChannelMessageCreatedBefore"
|
|
GroupsInfoChannelMessageCreator groupsCategory = "GroupsInfoChannelMessageCreator"
|
|
GroupsInfoChannelMessageLastReplyAfter groupsCategory = "GroupsInfoChannelMessageLastReplyAfter"
|
|
GroupsInfoChannelMessageLastReplyBefore groupsCategory = "GroupsInfoChannelMessageLastReplyBefore"
|
|
)
|
|
|
|
// groupsLeafProperties describes common metadata of the leaf categories
|
|
var groupsLeafProperties = map[categorizer]leafProperty{
|
|
GroupsChannelMessage: {
|
|
pathKeys: []categorizer{GroupsChannel, GroupsChannelMessage},
|
|
pathType: path.ChannelMessagesCategory,
|
|
},
|
|
GroupsConversationPost: {
|
|
pathKeys: []categorizer{GroupsConversation, GroupsConversationPost},
|
|
pathType: path.ConversationPostsCategory,
|
|
},
|
|
GroupsLibraryItem: {
|
|
pathKeys: []categorizer{GroupsLibraryFolder, GroupsLibraryItem},
|
|
pathType: path.LibrariesCategory,
|
|
},
|
|
GroupsGroup: { // the root category must be represented, even though it isn't a leaf
|
|
pathKeys: []categorizer{GroupsGroup},
|
|
pathType: path.UnknownCategory,
|
|
},
|
|
}
|
|
|
|
func (c groupsCategory) String() string {
|
|
return string(c)
|
|
}
|
|
|
|
// leafCat returns the leaf category of the receiver.
|
|
// If the receiver category has multiple leaves (ex: User) or no leaves,
|
|
// (ex: Unknown), the receiver itself is returned.
|
|
// Ex: ServiceTypeFolder.leafCat() => ServiceTypeItem
|
|
// Ex: ServiceUser.leafCat() => ServiceUser
|
|
func (c groupsCategory) leafCat() categorizer {
|
|
switch c {
|
|
case GroupsChannel, GroupsChannelMessage,
|
|
GroupsInfoChannelMessageCreatedAfter, GroupsInfoChannelMessageCreatedBefore, GroupsInfoChannelMessageCreator,
|
|
GroupsInfoChannelMessageLastReplyAfter, GroupsInfoChannelMessageLastReplyBefore:
|
|
return GroupsChannelMessage
|
|
case GroupsConversation, GroupsConversationPost:
|
|
return GroupsConversationPost
|
|
case GroupsLibraryFolder, GroupsLibraryItem, GroupsInfoSite, GroupsInfoSiteLibraryDrive,
|
|
GroupsInfoLibraryItemCreatedAfter, GroupsInfoLibraryItemCreatedBefore,
|
|
GroupsInfoLibraryItemModifiedAfter, GroupsInfoLibraryItemModifiedBefore:
|
|
return GroupsLibraryItem
|
|
}
|
|
|
|
return c
|
|
}
|
|
|
|
// rootCat returns the root category type.
|
|
func (c groupsCategory) rootCat() categorizer {
|
|
return GroupsGroup
|
|
}
|
|
|
|
// unknownCat returns the unknown category type.
|
|
func (c groupsCategory) unknownCat() categorizer {
|
|
return GroupsCategoryUnknown
|
|
}
|
|
|
|
// isUnion returns true if the category is a site or a webURL, which
|
|
// can act as an alternative identifier to siteID across all site types.
|
|
func (c groupsCategory) isUnion() bool {
|
|
return c == c.rootCat()
|
|
}
|
|
|
|
// isLeaf is true if the category is a GroupsItem category.
|
|
func (c groupsCategory) isLeaf() bool {
|
|
return c == c.leafCat()
|
|
}
|
|
|
|
// pathValues transforms the two paths to maps of identified properties.
|
|
//
|
|
// Example:
|
|
// [tenantID, service, groupID, site, siteID, category, folder, itemID]
|
|
// => {spFolder: folder, spItemID: itemID}
|
|
func (c groupsCategory) pathValues(
|
|
repo path.Path,
|
|
ent details.Entry,
|
|
cfg Config,
|
|
) (map[categorizer][]string, error) {
|
|
var (
|
|
folderCat, itemCat categorizer
|
|
itemID string
|
|
rFld string
|
|
)
|
|
|
|
if ent.Groups == nil {
|
|
return nil, clues.New("no Groups ItemInfo in details")
|
|
}
|
|
|
|
switch c {
|
|
case GroupsChannel, GroupsChannelMessage:
|
|
folderCat, itemCat = GroupsChannel, GroupsChannelMessage
|
|
rFld = ent.Groups.ParentPath
|
|
case GroupsConversation, GroupsConversationPost:
|
|
folderCat, itemCat = GroupsConversation, GroupsConversationPost
|
|
rFld = ent.Groups.ParentPath
|
|
case GroupsLibraryFolder, GroupsLibraryItem:
|
|
folderCat, itemCat = GroupsLibraryFolder, GroupsLibraryItem
|
|
rFld = ent.Groups.ParentPath
|
|
default:
|
|
return nil, clues.New("unrecognized groupsCategory").With("category", c)
|
|
}
|
|
|
|
item := ent.ItemRef
|
|
if len(item) == 0 {
|
|
item = repo.Item()
|
|
}
|
|
|
|
if cfg.OnlyMatchItemNames {
|
|
item = ent.ItemInfo.Groups.ItemName
|
|
}
|
|
|
|
result := map[categorizer][]string{
|
|
folderCat: {rFld},
|
|
itemCat: {item, ent.ShortRef},
|
|
}
|
|
|
|
if len(itemID) > 0 {
|
|
result[itemCat] = append(result[itemCat], itemID)
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// pathKeys returns the path keys recognized by the receiver's leaf type.
|
|
func (c groupsCategory) pathKeys() []categorizer {
|
|
return groupsLeafProperties[c.leafCat()].pathKeys
|
|
}
|
|
|
|
// PathType converts the category's leaf type into the matching path.CategoryType.
|
|
func (c groupsCategory) PathType() path.CategoryType {
|
|
return groupsLeafProperties[c.leafCat()].pathType
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Scopes
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// GroupsScope specifies the data available
|
|
// when interfacing with the Groups service.
|
|
type GroupsScope scope
|
|
|
|
// interface compliance checks
|
|
var _ scoper = &GroupsScope{}
|
|
|
|
// Category describes the type of the data in scope.
|
|
func (s GroupsScope) Category() groupsCategory {
|
|
return groupsCategory(getCategory(s))
|
|
}
|
|
|
|
// categorizer type is a generic wrapper around Category.
|
|
// Primarily used by scopes.go to for abstract comparisons.
|
|
func (s GroupsScope) categorizer() categorizer {
|
|
return s.Category()
|
|
}
|
|
|
|
// Matches returns true if the category is included in the scope's
|
|
// data type, and the target string matches that category's comparator.
|
|
func (s GroupsScope) Matches(cat groupsCategory, target string) bool {
|
|
return matches(s, cat, target)
|
|
}
|
|
|
|
// InfoCategory returns the category enum of the scope info.
|
|
// If the scope is not an info type, returns GroupsUnknownCategory.
|
|
func (s GroupsScope) InfoCategory() groupsCategory {
|
|
return groupsCategory(getInfoCategory(s))
|
|
}
|
|
|
|
// IncludeCategory checks whether the scope includes a
|
|
// certain category of data.
|
|
// Ex: to check if the scope includes file data:
|
|
// s.IncludesCategory(selector.GroupsFile)
|
|
func (s GroupsScope) IncludesCategory(cat groupsCategory) bool {
|
|
return categoryMatches(s.Category(), cat)
|
|
}
|
|
|
|
// returns true if the category is included in the scope's data type,
|
|
// and the value is set to Any().
|
|
func (s GroupsScope) IsAny(cat groupsCategory) bool {
|
|
return IsAnyTarget(s, cat)
|
|
}
|
|
|
|
// Get returns the data category in the scope. If the scope
|
|
// contains all data types for a user, it'll return the
|
|
// GroupsUser category.
|
|
func (s GroupsScope) Get(cat groupsCategory) []string {
|
|
return getCatValue(s, cat)
|
|
}
|
|
|
|
// sets a value by category to the scope. Only intended for internal use.
|
|
func (s GroupsScope) set(cat groupsCategory, v []string, opts ...option) GroupsScope {
|
|
os := []option{}
|
|
|
|
switch cat {
|
|
case GroupsChannel, GroupsConversation, GroupsLibraryFolder:
|
|
os = append(os, pathComparator())
|
|
}
|
|
|
|
return set(s, cat, v, append(os, opts...)...)
|
|
}
|
|
|
|
// setDefaults ensures that site scopes express `AnyTgt` for their child category types.
|
|
func (s GroupsScope) setDefaults() {
|
|
switch s.Category() {
|
|
case GroupsGroup:
|
|
s[GroupsChannel.String()] = passAny
|
|
s[GroupsChannelMessage.String()] = passAny
|
|
s[GroupsConversation.String()] = passAny
|
|
s[GroupsConversationPost.String()] = passAny
|
|
s[GroupsLibraryFolder.String()] = passAny
|
|
s[GroupsLibraryItem.String()] = passAny
|
|
case GroupsChannel:
|
|
s[GroupsChannelMessage.String()] = passAny
|
|
case GroupsLibraryFolder:
|
|
s[GroupsLibraryItem.String()] = passAny
|
|
case GroupsConversation:
|
|
s[GroupsConversationPost.String()] = passAny
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Backup Details Filtering
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// Reduce filters the entries in a details struct to only those that match the
|
|
// inclusions, filters, and exclusions in the selector.
|
|
func (s groups) Reduce(
|
|
ctx context.Context,
|
|
deets *details.Details,
|
|
errs *fault.Bus,
|
|
) *details.Details {
|
|
return reduce[GroupsScope](
|
|
ctx,
|
|
deets,
|
|
s.Selector,
|
|
map[path.CategoryType]groupsCategory{
|
|
path.ChannelMessagesCategory: GroupsChannelMessage,
|
|
path.ConversationPostsCategory: GroupsConversationPost,
|
|
path.LibrariesCategory: GroupsLibraryItem,
|
|
},
|
|
errs)
|
|
}
|
|
|
|
// matchesInfo handles the standard behavior when comparing a scope and an groupsInfo
|
|
// returns true if the scope and info match for the provided category.
|
|
func (s GroupsScope) matchesInfo(dii details.ItemInfo) bool {
|
|
var (
|
|
infoCat = s.InfoCategory()
|
|
i = ""
|
|
info = dii.Groups
|
|
)
|
|
|
|
if info == nil {
|
|
return false
|
|
}
|
|
|
|
acceptableItemType := -1
|
|
|
|
switch infoCat.leafCat() {
|
|
case GroupsLibraryItem:
|
|
acceptableItemType = int(details.SharePointLibrary)
|
|
case GroupsChannelMessage:
|
|
acceptableItemType = int(details.GroupsChannelMessage)
|
|
case GroupsConversationPost:
|
|
acceptableItemType = int(details.GroupsConversationPost)
|
|
}
|
|
|
|
switch infoCat {
|
|
case GroupsInfoSite:
|
|
ds := []string{}
|
|
|
|
if len(info.SiteID) > 0 {
|
|
ds = append(ds, info.SiteID)
|
|
}
|
|
|
|
if len(info.WebURL) > 0 {
|
|
ds = append(ds, info.WebURL)
|
|
}
|
|
|
|
return matchesAny(s, GroupsInfoSite, ds)
|
|
case GroupsInfoSiteLibraryDrive:
|
|
ds := []string{}
|
|
|
|
if len(info.DriveName) > 0 {
|
|
ds = append(ds, info.DriveName)
|
|
}
|
|
|
|
if len(info.DriveID) > 0 {
|
|
ds = append(ds, info.DriveID)
|
|
}
|
|
|
|
return matchesAny(s, GroupsInfoSiteLibraryDrive, ds)
|
|
case GroupsInfoLibraryItemCreatedAfter, GroupsInfoLibraryItemCreatedBefore:
|
|
i = dttm.Format(info.Created)
|
|
case GroupsInfoLibraryItemModifiedAfter, GroupsInfoLibraryItemModifiedBefore:
|
|
i = dttm.Format(info.Modified)
|
|
case GroupsInfoChannelMessageCreator:
|
|
i = info.Message.Creator
|
|
case GroupsInfoChannelMessageCreatedAfter, GroupsInfoChannelMessageCreatedBefore:
|
|
i = dttm.Format(info.Message.CreatedAt)
|
|
case GroupsInfoChannelMessageLastReplyAfter, GroupsInfoChannelMessageLastReplyBefore:
|
|
if info.LastReply.CreatedAt.IsZero() {
|
|
return false
|
|
}
|
|
|
|
i = dttm.Format(info.LastReply.CreatedAt)
|
|
}
|
|
|
|
return s.Matches(infoCat, i) && int(info.ItemType) == acceptableItemType
|
|
}
|