Adds the sharepoint service to /pkg/path. Currently uses the "Files" category for its category type, which is just a placeholder for kicking off development. Additionally, uncomments selector tests that were dependent upon the path service declaration.
484 lines
11 KiB
Go
484 lines
11 KiB
Go
package selectors
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/alcionai/corso/src/pkg/backup/details"
|
|
"github.com/alcionai/corso/src/pkg/filters"
|
|
"github.com/alcionai/corso/src/pkg/path"
|
|
)
|
|
|
|
type service int
|
|
|
|
//go:generate stringer -type=service -linecomment
|
|
const (
|
|
ServiceUnknown service = iota // Unknown Service
|
|
ServiceExchange // Exchange
|
|
ServiceOneDrive // OneDrive
|
|
ServiceSharePoint // SharePoint
|
|
)
|
|
|
|
var serviceToPathType = map[service]path.ServiceType{
|
|
ServiceUnknown: path.UnknownService,
|
|
ServiceExchange: path.ExchangeService,
|
|
ServiceOneDrive: path.OneDriveService,
|
|
ServiceSharePoint: path.SharePointService,
|
|
}
|
|
|
|
var (
|
|
ErrorBadSelectorCast = errors.New("wrong selector service type")
|
|
ErrorNoMatchingItems = errors.New("no items match the specified selectors")
|
|
)
|
|
|
|
const (
|
|
scopeKeyCategory = "category"
|
|
scopeKeyInfoFilter = "info_filter"
|
|
scopeKeyDataType = "type"
|
|
)
|
|
|
|
// The granularity exprerssed by the scope. Groups imply non-item granularity,
|
|
// such as a directory. Items are individual files or objects.
|
|
const (
|
|
// AnyTgt is the target value used to select "any data of <type>"
|
|
// Ex: {user: u1, events: AnyTgt) => all events for user u1.
|
|
// In the event that "*" conflicts with a user value, such as a
|
|
// folder named "*", calls to corso should escape the value with "\*"
|
|
AnyTgt = "*"
|
|
// NoneTgt is the target value used to select "no data of <type>"
|
|
// This is primarily a fallback for empty values. Adding NoneTgt or
|
|
// None() to any selector will force all matches() checks on that
|
|
// selector to fail.
|
|
// Ex: {user: u1, events: NoneTgt} => matches nothing.
|
|
NoneTgt = ""
|
|
delimiter = ","
|
|
)
|
|
|
|
var (
|
|
passAny = filters.Pass()
|
|
failAny = filters.Fail()
|
|
)
|
|
|
|
// All is the resource name that gets output when the resource is AnyTgt.
|
|
// It is not used aside from printing resources.
|
|
const All = "All"
|
|
|
|
type Reducer interface {
|
|
Reduce(context.Context, *details.Details) *details.Details
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Selector
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// The core selector. Has no api for setting or retrieving data.
|
|
// Is only used to pass along more specific selector instances.
|
|
type Selector struct {
|
|
// The service scope of the data. Exchange, Teams, SharePoint, etc.
|
|
Service service `json:"service,omitempty"`
|
|
// A slice of exclusion scopes. Exclusions apply globally to all
|
|
// inclusions/filters, with any-match behavior.
|
|
Excludes []scope `json:"exclusions,omitempty"`
|
|
// A slice of filter scopes. All inclusions must also match ALL filters.
|
|
Filters []scope `json:"filters,omitempty"`
|
|
// A slice of inclusion scopes. Comparators must match either one of these,
|
|
// or all filters, to be included.
|
|
Includes []scope `json:"includes,omitempty"`
|
|
}
|
|
|
|
// helper for specific selector instance constructors.
|
|
func newSelector(s service) Selector {
|
|
return Selector{
|
|
Service: s,
|
|
Excludes: []scope{},
|
|
Includes: []scope{},
|
|
}
|
|
}
|
|
|
|
func (s Selector) String() string {
|
|
bs, err := json.Marshal(s)
|
|
if err != nil {
|
|
return "error"
|
|
}
|
|
|
|
return string(bs)
|
|
}
|
|
|
|
// appendScopes iterates through each scope in the list of scope slices,
|
|
// calling setDefaults() to ensure it is completely populated, and appends
|
|
// those scopes to the `to` slice.
|
|
func appendScopes[T scopeT](to []scope, scopes ...[]T) []scope {
|
|
if len(to) == 0 {
|
|
to = []scope{}
|
|
}
|
|
|
|
for _, scopeSl := range scopes {
|
|
for _, s := range scopeSl {
|
|
s.setDefaults()
|
|
to = append(to, scope(s))
|
|
}
|
|
}
|
|
|
|
return to
|
|
}
|
|
|
|
// scopes retrieves the list of scopes in the selector.
|
|
// future TODO: if Inclues is nil, return filters.
|
|
func scopes[T scopeT](s Selector) []T {
|
|
scopes := []T{}
|
|
|
|
for _, v := range s.Includes {
|
|
scopes = append(scopes, T(v))
|
|
}
|
|
|
|
return scopes
|
|
}
|
|
|
|
// discreteScopes retrieves the list of scopes in the selector.
|
|
// for any scope in the `Includes` set, if scope.IsAny(rootCat),
|
|
// then that category's value is replaced with the provided set of
|
|
// discrete identifiers.
|
|
// If discreteIDs is an empty slice, returns the normal scopes(s).
|
|
// future TODO: if Includes is nil, return filters.
|
|
func discreteScopes[T scopeT, C categoryT](
|
|
s Selector,
|
|
rootCat C,
|
|
discreteIDs []string,
|
|
) []T {
|
|
sl := []T{}
|
|
|
|
if len(discreteIDs) == 0 {
|
|
return scopes[T](s)
|
|
}
|
|
|
|
for _, v := range s.Includes {
|
|
t := T(v)
|
|
|
|
if isAnyTarget(t, rootCat) {
|
|
w := T{}
|
|
for k, v := range t {
|
|
w[k] = v
|
|
}
|
|
|
|
set(w, rootCat, discreteIDs)
|
|
t = w
|
|
}
|
|
|
|
sl = append(sl, t)
|
|
}
|
|
|
|
return sl
|
|
}
|
|
|
|
// Returns the path.ServiceType matching the selector service.
|
|
func (s Selector) PathService() path.ServiceType {
|
|
return serviceToPathType[s.Service]
|
|
}
|
|
|
|
// Reduce is a quality-of-life interpreter that allows Reduce to be called
|
|
// from the generic selector by interpreting the selector service type rather
|
|
// than have the caller make that interpretation. Returns an error if the
|
|
// service is unsupported.
|
|
func (s Selector) Reduce(ctx context.Context, deets *details.Details) (*details.Details, error) {
|
|
var (
|
|
r Reducer
|
|
err error
|
|
)
|
|
|
|
switch s.Service {
|
|
case ServiceExchange:
|
|
r, err = s.ToExchangeRestore()
|
|
case ServiceOneDrive:
|
|
r, err = s.ToOneDriveRestore()
|
|
case ServiceSharePoint:
|
|
r, err = s.ToSharePointRestore()
|
|
default:
|
|
return nil, errors.New("service not supported: " + s.Service.String())
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return r.Reduce(ctx, deets), nil
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Printing Selectors for Human Reading
|
|
// ---------------------------------------------------------------------------
|
|
|
|
type Printable struct {
|
|
Service string `json:"service"`
|
|
Excludes map[string][]string `json:"excludes,omitempty"`
|
|
Filters map[string][]string `json:"filters,omitempty"`
|
|
Includes map[string][]string `json:"includes,omitempty"`
|
|
}
|
|
|
|
// ToPrintable creates the minimized display of a selector, formatted for human readability.
|
|
func (s Selector) ToPrintable() Printable {
|
|
switch s.Service {
|
|
case ServiceExchange:
|
|
r, err := s.ToExchangeRestore()
|
|
if err != nil {
|
|
return Printable{}
|
|
}
|
|
|
|
return r.Printable()
|
|
|
|
case ServiceOneDrive:
|
|
r, err := s.ToOneDriveBackup()
|
|
if err != nil {
|
|
return Printable{}
|
|
}
|
|
|
|
return r.Printable()
|
|
|
|
case ServiceSharePoint:
|
|
r, err := s.ToSharePointBackup()
|
|
if err != nil {
|
|
return Printable{}
|
|
}
|
|
|
|
return r.Printable()
|
|
}
|
|
|
|
return Printable{}
|
|
}
|
|
|
|
// toPrintable creates the minimized display of a selector, formatted for human readability.
|
|
func toPrintable[T scopeT](s Selector) Printable {
|
|
return Printable{
|
|
Service: s.Service.String(),
|
|
Excludes: toResourceTypeMap[T](s.Excludes),
|
|
Filters: toResourceTypeMap[T](s.Filters),
|
|
Includes: toResourceTypeMap[T](s.Includes),
|
|
}
|
|
}
|
|
|
|
// Resources generates a tabular-readable output of the resources in Printable.
|
|
// Only the first (arbitrarily picked) resource is displayed. All others are
|
|
// simply counted. If no inclusions exist, uses Filters. If no filters exist,
|
|
// defaults to "None".
|
|
// Resource refers to the top-level entity in the service. User for Exchange,
|
|
// Site for sharepoint, etc.
|
|
func (p Printable) Resources() string {
|
|
s := resourcesShortFormat(p.Includes)
|
|
if len(s) == 0 {
|
|
s = resourcesShortFormat(p.Filters)
|
|
}
|
|
|
|
if len(s) == 0 {
|
|
s = "None"
|
|
}
|
|
|
|
return s
|
|
}
|
|
|
|
// returns a string with the resources in the map. Shortened to the first resource key,
|
|
// plus, if more exist, " (len-1 more)"
|
|
func resourcesShortFormat(m map[string][]string) string {
|
|
var s string
|
|
|
|
for k := range m {
|
|
s = k
|
|
break
|
|
}
|
|
|
|
if len(s) > 0 && len(m) > 1 {
|
|
s = fmt.Sprintf("%s (%d more)", s, len(m)-1)
|
|
}
|
|
|
|
return s
|
|
}
|
|
|
|
// Transforms the slice to a single map.
|
|
// Keys are each map's scopeKeyResource value.
|
|
// Values are the set of all scopeKeyDataTypes for a given resource.
|
|
func toResourceTypeMap[T scopeT](s []scope) map[string][]string {
|
|
if len(s) == 0 {
|
|
return nil
|
|
}
|
|
|
|
r := make(map[string][]string)
|
|
|
|
for _, sc := range s {
|
|
t := T(sc)
|
|
res := sc[t.categorizer().rootCat().String()]
|
|
k := res.Target
|
|
|
|
if res.Target == AnyTgt {
|
|
k = All
|
|
}
|
|
|
|
r[k] = addToSet(r[k], split(sc[scopeKeyDataType].Target))
|
|
}
|
|
|
|
return r
|
|
}
|
|
|
|
// returns v if set is empty,
|
|
// unions v with set, otherwise.
|
|
func addToSet(set []string, v []string) []string {
|
|
if len(set) == 0 {
|
|
return v
|
|
}
|
|
|
|
for _, vv := range v {
|
|
var matched bool
|
|
|
|
for _, s := range set {
|
|
if vv == s {
|
|
matched = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !matched {
|
|
set = append(set, vv)
|
|
}
|
|
}
|
|
|
|
return set
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
type scopeConfig struct {
|
|
usePathFilter bool
|
|
usePrefixFilter bool
|
|
}
|
|
|
|
type option func(*scopeConfig)
|
|
|
|
func (sc *scopeConfig) populate(opts ...option) {
|
|
for _, opt := range opts {
|
|
opt(sc)
|
|
}
|
|
}
|
|
|
|
// PrefixMatch ensures the selector uses a Prefix comparator, instead
|
|
// of contains or equals. Will not override a default Any() or None()
|
|
// comparator.
|
|
func PrefixMatch() option {
|
|
return func(sc *scopeConfig) {
|
|
sc.usePrefixFilter = true
|
|
}
|
|
}
|
|
|
|
// pathType is an internal-facing option. It is assumed that scope
|
|
// constructors will provide the pathType option whenever a folder-
|
|
// level scope (ie, a scope that compares path hierarchies) is created.
|
|
func pathType() option {
|
|
return func(sc *scopeConfig) {
|
|
sc.usePathFilter = true
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func badCastErr(cast, is service) error {
|
|
return errors.Wrapf(ErrorBadSelectorCast, "%s service is not %s", cast, is)
|
|
}
|
|
|
|
func join(s ...string) string {
|
|
return strings.Join(s, delimiter)
|
|
}
|
|
|
|
func split(s string) []string {
|
|
return strings.Split(s, delimiter)
|
|
}
|
|
|
|
// if the provided slice contains Any, returns [Any]
|
|
// if the slice contains None, returns [None]
|
|
// if the slice contains Any and None, returns the first
|
|
// if the slice is empty, returns [None]
|
|
// otherwise returns the input
|
|
func clean(s []string) []string {
|
|
if len(s) == 0 {
|
|
return None()
|
|
}
|
|
|
|
for _, e := range s {
|
|
if e == AnyTgt {
|
|
return Any()
|
|
}
|
|
|
|
if e == NoneTgt {
|
|
return None()
|
|
}
|
|
}
|
|
|
|
return s
|
|
}
|
|
|
|
// filterize turns the slice into a filter.
|
|
// if the input is Any(), returns a passAny filter.
|
|
// if the input is None(), returns a failAny filter.
|
|
// if the scopeConfig specifies a filter, use that filter.
|
|
// if the input is len(1), returns an Equals filter.
|
|
// otherwise returns a Contains filter.
|
|
func filterize(sc scopeConfig, s ...string) filters.Filter {
|
|
s = clean(s)
|
|
|
|
if len(s) == 0 || s[0] == NoneTgt {
|
|
return failAny
|
|
}
|
|
|
|
if s[0] == AnyTgt {
|
|
return passAny
|
|
}
|
|
|
|
if sc.usePathFilter {
|
|
if sc.usePrefixFilter {
|
|
return filters.PathPrefix(s)
|
|
}
|
|
|
|
return filters.PathContains(s)
|
|
}
|
|
|
|
if sc.usePrefixFilter {
|
|
return filters.Prefix(join(s...))
|
|
}
|
|
|
|
if len(s) == 1 {
|
|
return filters.Equal(s[0])
|
|
}
|
|
|
|
return filters.Contains(join(s...))
|
|
}
|
|
|
|
type filterFunc func(string) filters.Filter
|
|
|
|
// wrapFilter produces a func that filterizes the input by:
|
|
// - cleans the input string
|
|
// - normalizes the cleaned input (returns anyFail if empty, allFail if *)
|
|
// - joins the string
|
|
// - and generates a filter with the joined input.
|
|
func wrapFilter(ff filterFunc) func([]string) filters.Filter {
|
|
return func(s []string) filters.Filter {
|
|
s = clean(s)
|
|
|
|
if len(s) == 1 {
|
|
if s[0] == AnyTgt {
|
|
return passAny
|
|
}
|
|
|
|
if s[0] == NoneTgt {
|
|
return failAny
|
|
}
|
|
}
|
|
|
|
ss := join(s...)
|
|
|
|
return ff(ss)
|
|
}
|
|
}
|