corso/src/pkg/selectors/onedrive.go
Keepers 666813865e
case sensitive selection for item IDs (#3397)
Use a strictEquals matcher for item selection when
not matching on item names (no name match assumes
canonical system id match, which is case sensitive).

---

#### Does this PR need a docs update or release note?

- [x]  No

#### Type of change

- [x] 🐛 Bugfix

#### Issue(s)

* #3313

#### Test Plan

- [x]  Unit test
- [x] 💚 E2E
2023-05-16 18:15:18 +00:00

551 lines
17 KiB
Go

package selectors
import (
"context"
"fmt"
"github.com/alcionai/clues"
"github.com/alcionai/corso/src/internal/common/dttm"
"github.com/alcionai/corso/src/pkg/backup/details"
"github.com/alcionai/corso/src/pkg/fault"
"github.com/alcionai/corso/src/pkg/filters"
"github.com/alcionai/corso/src/pkg/path"
)
// ---------------------------------------------------------------------------
// Selectors
// ---------------------------------------------------------------------------
type (
// oneDrive provides an api for selecting
// data scopes applicable to the OneDrive service.
oneDrive struct {
Selector
}
// OneDriveBackup provides an api for selecting
// data scopes applicable to the OneDrive service,
// plus backup-specific methods.
OneDriveBackup struct {
oneDrive
}
// OneDriveRestorep provides an api for selecting
// data scopes applicable to the OneDrive service,
// plus restore-specific methods.
OneDriveRestore struct {
oneDrive
}
)
var (
_ Reducer = &OneDriveRestore{}
_ pathCategorier = &OneDriveRestore{}
)
// NewOneDriveBackup produces a new Selector with the service set to ServiceOneDrive.
func NewOneDriveBackup(users []string) *OneDriveBackup {
src := OneDriveBackup{
oneDrive{
newSelector(ServiceOneDrive, users),
},
}
return &src
}
// ToOneDriveBackup transforms the generic selector into an OneDriveBackup.
// Errors if the service defined by the selector is not ServiceOneDrive.
func (s Selector) ToOneDriveBackup() (*OneDriveBackup, error) {
if s.Service != ServiceOneDrive {
return nil, badCastErr(ServiceOneDrive, s.Service)
}
src := OneDriveBackup{oneDrive{s}}
return &src, nil
}
func (s OneDriveBackup) SplitByResourceOwner(users []string) []OneDriveBackup {
sels := splitByResourceOwner[OneDriveScope](s.Selector, users, OneDriveUser)
ss := make([]OneDriveBackup, 0, len(sels))
for _, sel := range sels {
ss = append(ss, OneDriveBackup{oneDrive{sel}})
}
return ss
}
// NewOneDriveRestore produces a new Selector with the service set to ServiceOneDrive.
func NewOneDriveRestore(users []string) *OneDriveRestore {
src := OneDriveRestore{
oneDrive{
newSelector(ServiceOneDrive, users),
},
}
return &src
}
// ToOneDriveRestore transforms the generic selector into an OneDriveRestore.
// Errors if the service defined by the selector is not ServiceOneDrive.
func (s Selector) ToOneDriveRestore() (*OneDriveRestore, error) {
if s.Service != ServiceOneDrive {
return nil, badCastErr(ServiceOneDrive, s.Service)
}
src := OneDriveRestore{oneDrive{s}}
return &src, nil
}
func (s OneDriveRestore) SplitByResourceOwner(users []string) []OneDriveRestore {
sels := splitByResourceOwner[OneDriveScope](s.Selector, users, OneDriveUser)
ss := make([]OneDriveRestore, 0, len(sels))
for _, sel := range sels {
ss = append(ss, OneDriveRestore{oneDrive{sel}})
}
return ss
}
// PathCategories produces the aggregation of discrete users described by each type of scope.
func (s oneDrive) PathCategories() selectorPathCategories {
return selectorPathCategories{
Excludes: pathCategoriesIn[OneDriveScope, oneDriveCategory](s.Excludes),
Filters: pathCategoriesIn[OneDriveScope, oneDriveCategory](s.Filters),
Includes: pathCategoriesIn[OneDriveScope, oneDriveCategory](s.Includes),
}
}
// ---------------------------------------------------------------------------
// Stringers and Concealers
// ---------------------------------------------------------------------------
func (s OneDriveScope) Conceal() string { return conceal(s) }
func (s OneDriveScope) Format(fs fmt.State, r rune) { format(s, fs, r) }
func (s OneDriveScope) String() string { return conceal(s) }
func (s OneDriveScope) 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(u1, f1, m1) => only excludes a file if it is owned by user u1,
// located in folder f1, and ID'd as m1. 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: User(u1) automatically cascades to all folders and files owned
// by u1.
func (s *oneDrive) Include(scopes ...[]OneDriveScope) {
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(u1, f1, m1) => only excludes a file if it is owned by user u1,
// located in folder f1, and ID'd as m1. 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: User(u1) automatically cascades to all folders and files owned
// by u1.
func (s *oneDrive) Exclude(scopes ...[]OneDriveScope) {
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(u1, f1, m1) => only excludes a file if it is owned by user u1,
// located in folder f1, and ID'd as m1. 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: User(u1) automatically cascades to all folders and files owned
// by u1.
func (s *oneDrive) Filter(scopes ...[]OneDriveScope) {
s.Filters = appendScopes(s.Filters, scopes...)
}
// Scopes retrieves the list of oneDriveScopes in the selector.
func (s *oneDrive) Scopes() []OneDriveScope {
return scopes[OneDriveScope](s.Selector)
}
// -------------------
// Scope Factories
// Retrieves all OneDrive data.
// One scope is created per user 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 *oneDrive) AllData() []OneDriveScope {
scopes := []OneDriveScope{}
scopes = append(scopes, makeScope[OneDriveScope](OneDriveFolder, Any()))
return scopes
}
// Folders produces one or more OneDrive folder 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 folder scopes.
func (s *oneDrive) Folders(folders []string, opts ...option) []OneDriveScope {
var (
scopes = []OneDriveScope{}
os = append([]option{pathComparator()}, opts...)
)
scopes = append(
scopes,
makeScope[OneDriveScope](OneDriveFolder, folders, os...))
return scopes
}
// Items produces one or more OneDrive 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 folder scopes.
func (s *oneDrive) Items(folders, items []string, opts ...option) []OneDriveScope {
scopes := []OneDriveScope{}
scopes = append(
scopes,
makeScope[OneDriveScope](OneDriveItem, items, defaultItemOptions(s.Cfg)...).
set(OneDriveFolder, folders, opts...))
return scopes
}
// -------------------
// Filter Factories
// CreatedAfter produces a OneDrive item created-after info scope.
// Matches any item where the created 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 *oneDrive) CreatedAfter(timeStrings string) []OneDriveScope {
return []OneDriveScope{
makeInfoScope[OneDriveScope](
OneDriveItem,
FileInfoCreatedAfter,
[]string{timeStrings},
filters.Less),
}
}
// CreatedBefore produces a OneDrive item created-before info scope.
// Matches any item where the created time is before 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 *oneDrive) CreatedBefore(timeStrings string) []OneDriveScope {
return []OneDriveScope{
makeInfoScope[OneDriveScope](
OneDriveItem,
FileInfoCreatedBefore,
[]string{timeStrings},
filters.Greater),
}
}
// ModifiedAfter produces a OneDrive item modified-after info scope.
// Matches any item where the modified 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 *oneDrive) ModifiedAfter(timeStrings string) []OneDriveScope {
return []OneDriveScope{
makeInfoScope[OneDriveScope](
OneDriveItem,
FileInfoModifiedAfter,
[]string{timeStrings},
filters.Less),
}
}
// ModifiedBefore produces a OneDrive item modified-before info scope.
// Matches any item where the modified time is before 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 *oneDrive) ModifiedBefore(timeStrings string) []OneDriveScope {
return []OneDriveScope{
makeInfoScope[OneDriveScope](
OneDriveItem,
FileInfoModifiedBefore,
[]string{timeStrings},
filters.Greater),
}
}
// ---------------------------------------------------------------------------
// Categories
// ---------------------------------------------------------------------------
// oneDriveCategory enumerates the type of the lowest level
// of data () in a scope.
type oneDriveCategory string
// interface compliance checks
var _ categorizer = OneDriveCategoryUnknown
const (
OneDriveCategoryUnknown oneDriveCategory = ""
// types of data in OneDrive
OneDriveUser oneDriveCategory = "OneDriveUser"
OneDriveItem oneDriveCategory = "OneDriveItem"
OneDriveFolder oneDriveCategory = "OneDriveFolder"
// details.ItemInfo comparables
FileInfoCreatedAfter oneDriveCategory = "FileInfoCreatedAfter"
FileInfoCreatedBefore oneDriveCategory = "FileInfoCreatedBefore"
FileInfoModifiedAfter oneDriveCategory = "FileInfoModifiedAfter"
FileInfoModifiedBefore oneDriveCategory = "FileInfoModifiedBefore"
)
// oneDriveLeafProperties describes common metadata of the leaf categories
var oneDriveLeafProperties = map[categorizer]leafProperty{
OneDriveItem: {
pathKeys: []categorizer{OneDriveFolder, OneDriveItem},
pathType: path.FilesCategory,
},
OneDriveUser: { // the root category must be represented, even though it isn't a leaf
pathKeys: []categorizer{OneDriveUser},
pathType: path.UnknownCategory,
},
}
func (c oneDriveCategory) 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 oneDriveCategory) leafCat() categorizer {
switch c {
case OneDriveFolder, OneDriveItem,
FileInfoCreatedAfter, FileInfoCreatedBefore,
FileInfoModifiedAfter, FileInfoModifiedBefore:
return OneDriveItem
}
return c
}
// rootCat returns the root category type.
func (c oneDriveCategory) rootCat() categorizer {
return OneDriveUser
}
// unknownCat returns the unknown category type.
func (c oneDriveCategory) unknownCat() categorizer {
return OneDriveCategoryUnknown
}
// isUnion returns true if c is a user
func (c oneDriveCategory) isUnion() bool {
return c == c.rootCat()
}
// isLeaf is true if the category is a OneDriveItem category.
func (c oneDriveCategory) isLeaf() bool {
// return c == c.leafCat()??
return c == OneDriveItem
}
// pathValues transforms the two paths to maps of identified properties.
//
// Example:
// [tenantID, service, userPN, category, folder, fileID]
// => {odFolder: folder, odFileID: fileID}
func (c oneDriveCategory) pathValues(
repo path.Path,
ent details.Entry,
cfg Config,
) (map[categorizer][]string, error) {
if ent.OneDrive == nil {
return nil, clues.New("no OneDrive ItemInfo in details")
}
// Ignore `drives/<driveID>/root:` for folder comparison
rFld := ent.OneDrive.ParentPath
item := ent.ItemRef
if len(item) == 0 {
item = repo.Item()
}
if cfg.OnlyMatchItemNames {
item = ent.ItemInfo.OneDrive.ItemName
}
result := map[categorizer][]string{
OneDriveFolder: {rFld},
OneDriveItem: {item, ent.ShortRef},
}
if len(ent.LocationRef) > 0 {
result[OneDriveFolder] = append(result[OneDriveFolder], ent.LocationRef)
}
return result, nil
}
// pathKeys returns the path keys recognized by the receiver's leaf type.
func (c oneDriveCategory) pathKeys() []categorizer {
return oneDriveLeafProperties[c.leafCat()].pathKeys
}
// PathType converts the category's leaf type into the matching path.CategoryType.
func (c oneDriveCategory) PathType() path.CategoryType {
return oneDriveLeafProperties[c.leafCat()].pathType
}
// ---------------------------------------------------------------------------
// Scopes
// ---------------------------------------------------------------------------
// OneDriveScope specifies the data available
// when interfacing with the OneDrive service.
type OneDriveScope scope
// interface compliance checks
var _ scoper = &OneDriveScope{}
// Category describes the type of the data in scope.
func (s OneDriveScope) Category() oneDriveCategory {
return oneDriveCategory(getCategory(s))
}
// categorizer type is a generic wrapper around Category.
// Primarily used by scopes.go to for abstract comparisons.
func (s OneDriveScope) categorizer() categorizer {
return s.Category()
}
// InfoCategory returns the category enum of the scope info.
// If the scope is not an info type, returns OneDriveUnknownCategory.
func (s OneDriveScope) InfoCategory() oneDriveCategory {
return oneDriveCategory(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.OneDriveFile)
func (s OneDriveScope) IncludesCategory(cat oneDriveCategory) bool {
return categoryMatches(s.Category(), cat)
}
// 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 OneDriveScope) Matches(cat oneDriveCategory, target string) bool {
return matches(s, cat, target)
}
// returns true if the category is included in the scope's data type,
// and the value is set to Any().
func (s OneDriveScope) IsAny(cat oneDriveCategory) 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
// OneDriveUser category.
func (s OneDriveScope) Get(cat oneDriveCategory) []string {
return getCatValue(s, cat)
}
// sets a value by category to the scope. Only intended for internal use.
func (s OneDriveScope) set(cat oneDriveCategory, v []string, opts ...option) OneDriveScope {
os := []option{}
if cat == OneDriveFolder {
os = append(os, pathComparator())
}
return set(s, cat, v, append(os, opts...)...)
}
// setDefaults ensures that user scopes express `AnyTgt` for their child category types.
func (s OneDriveScope) setDefaults() {
switch s.Category() {
case OneDriveUser:
s[OneDriveFolder.String()] = passAny
s[OneDriveItem.String()] = passAny
case OneDriveFolder:
s[OneDriveItem.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 oneDrive) Reduce(
ctx context.Context,
deets *details.Details,
errs *fault.Bus,
) *details.Details {
return reduce[OneDriveScope](
ctx,
deets,
s.Selector,
map[path.CategoryType]oneDriveCategory{
path.FilesCategory: OneDriveItem,
},
errs)
}
// matchesInfo handles the standard behavior when comparing a scope and an oneDriveInfo
// returns true if the scope and info match for the provided category.
func (s OneDriveScope) matchesInfo(dii details.ItemInfo) bool {
info := dii.OneDrive
if info == nil {
return false
}
infoCat := s.InfoCategory()
i := ""
switch infoCat {
case FileInfoCreatedAfter, FileInfoCreatedBefore:
i = dttm.Format(info.Created)
case FileInfoModifiedAfter, FileInfoModifiedBefore:
i = dttm.Format(info.Modified)
}
return s.Matches(infoCat, i)
}