filter backup details by flags (#371)

* filter backup details by flags

`backup details` should have its output filtered by the flags provided by
the user.  In addition, the selector's FilterDetails should maintain
information (esp service info) about the entries, rather than slicing them
down to only the path reference.
This commit is contained in:
Keepers 2022-07-20 16:25:28 -06:00 committed by GitHub
parent a823231afd
commit 6224a92e7a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 191 additions and 36 deletions

View File

@ -109,7 +109,8 @@ func createExchangeCmd(cmd *cobra.Command, args []string) error {
if utils.HasNoFlagsAndShownHelp(cmd) { if utils.HasNoFlagsAndShownHelp(cmd) {
return nil return nil
} }
if err := validateBackupCreateFlags(exchangeAll, user, exchangeData); err != nil {
if err := validateExchangeBackupCreateFlags(exchangeAll, user, exchangeData); err != nil {
return err return err
} }
@ -176,7 +177,7 @@ func exchangeBackupCreateSelectors(all bool, users, data []string) selectors.Sel
return sel.Selector return sel.Selector
} }
func validateBackupCreateFlags(all bool, users, data []string) error { func validateExchangeBackupCreateFlags(all bool, users, data []string) error {
if len(users) == 0 && !all { if len(users) == 0 && !all {
return errors.New("requries one or more --user ids, the wildcard --user *, or the --all flag.") return errors.New("requries one or more --user ids, the wildcard --user *, or the --all flag.")
} }
@ -253,6 +254,22 @@ var exchangeDetailsCmd = &cobra.Command{
func detailsExchangeCmd(cmd *cobra.Command, args []string) error { func detailsExchangeCmd(cmd *cobra.Command, args []string) error {
ctx := cmd.Context() ctx := cmd.Context()
if utils.HasNoFlagsAndShownHelp(cmd) {
return nil
}
if err := validateExchangeBackupDetailFlags(
contact,
contactFolder,
email,
emailFolder,
event,
user,
backupID,
); err != nil {
return err
}
s, acct, err := config.GetStorageAndAccount(true, nil) s, acct, err := config.GetStorageAndAccount(true, nil)
if err != nil { if err != nil {
return err return err
@ -278,7 +295,14 @@ func detailsExchangeCmd(cmd *cobra.Command, args []string) error {
return errors.Wrap(err, "Failed to get backup details in the repository") return errors.Wrap(err, "Failed to get backup details in the repository")
} }
print.Entries(d.Entries) sel := exchangeBackupDetailSelectors(contact, contactFolder, email, emailFolder, event, user)
erSel, err := sel.ToExchangeRestore()
if err != nil {
return err
}
ds := erSel.FilterDetails(d)
print.Entries(ds.Entries)
return nil return nil
} }

View File

@ -93,7 +93,7 @@ func (suite *ExchangeSuite) TestValidateBackupCreateFlags() {
} }
for _, test := range table { for _, test := range table {
suite.T().Run(test.name, func(t *testing.T) { suite.T().Run(test.name, func(t *testing.T) {
test.expect(t, validateBackupCreateFlags(test.all, test.user, test.data)) test.expect(t, validateExchangeBackupCreateFlags(test.all, test.user, test.data))
}) })
} }
} }

View File

@ -2,6 +2,7 @@ package operations
import ( import (
"context" "context"
"strings"
"time" "time"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -95,7 +96,13 @@ func (op *RestoreOperation) Run(ctx context.Context) error {
// format the details and retrieve the items from kopia // format the details and retrieve the items from kopia
fds := er.FilterDetails(d) fds := er.FilterDetails(d)
dcs, err := op.kopia.RestoreMultipleItems(ctx, b.SnapshotID, fds) // todo: use path pkg for this
fdsPaths := fds.Paths()
paths := make([][]string, len(fdsPaths))
for i := range fdsPaths {
paths[i] = strings.Split(fdsPaths[i], "/")
}
dcs, err := op.kopia.RestoreMultipleItems(ctx, b.SnapshotID, paths)
if err != nil { if err != nil {
stats.readErr = errors.Wrap(err, "retrieving service data") stats.readErr = errors.Wrap(err, "retrieving service data")
return stats.readErr return stats.readErr

View File

@ -77,6 +77,16 @@ type DetailsEntry struct {
ItemInfo ItemInfo
} }
// Paths returns the list of Paths extracted from the Entriess slice.
func (dm DetailsModel) Paths() []string {
ents := dm.Entries
r := make([]string, len(ents))
for i := range ents {
r[i] = ents[i].RepoRef
}
return r
}
// Headers returns the human-readable names of properties in a DetailsEntry // Headers returns the human-readable names of properties in a DetailsEntry
// for printing out to a terminal in a columnar display. // for printing out to a terminal in a columnar display.
func (de DetailsEntry) Headers() []string { func (de DetailsEntry) Headers() []string {

View File

@ -106,3 +106,42 @@ func (suite *BackupSuite) TestDetailsEntry_HeadersValues() {
}) })
} }
} }
func (suite *BackupSuite) TestDetailsModel_Path() {
table := []struct {
name string
ents []backup.DetailsEntry
expect []string
}{
{
name: "nil entries",
ents: nil,
expect: []string{},
},
{
name: "single entry",
ents: []backup.DetailsEntry{
{RepoRef: "abcde"},
},
expect: []string{"abcde"},
},
{
name: "multiple entries",
ents: []backup.DetailsEntry{
{RepoRef: "abcde"},
{RepoRef: "12345"},
},
expect: []string{"abcde", "12345"},
},
}
for _, test := range table {
suite.T().Run(test.name, func(t *testing.T) {
d := backup.Details{
DetailsModel: backup.DetailsModel{
Entries: test.ents,
},
}
assert.DeepEqual(t, test.expect, d.Paths())
})
}
}

View File

@ -391,7 +391,7 @@ var categoryPathSet = map[exchangeCategory][]exchangeCategory{
// includesPath returns true if all filters in the scope match the path. // includesPath returns true if all filters in the scope match the path.
func (s exchangeScope) includesPath(cat exchangeCategory, path []string) bool { func (s exchangeScope) includesPath(cat exchangeCategory, path []string) bool {
ids := idPath(cat, path) ids := exchangeIDPath(cat, path)
for _, c := range categoryPathSet[cat] { for _, c := range categoryPathSet[cat] {
target := s.Get(c) target := s.Get(c)
if len(target) == 0 { if len(target) == 0 {
@ -408,9 +408,18 @@ func (s exchangeScope) includesPath(cat exchangeCategory, path []string) bool {
return true return true
} }
// includesInfo returns true if all filters in the scope match the info.
func (s exchangeScope) includesInfo(cat exchangeCategory, info *backup.ExchangeInfo) bool {
// todo: implement once filters used in scopes
if info == nil {
return false
}
return false
}
// excludesPath returns true if all filters in the scope match the path. // excludesPath returns true if all filters in the scope match the path.
func (s exchangeScope) excludesPath(cat exchangeCategory, path []string) bool { func (s exchangeScope) excludesPath(cat exchangeCategory, path []string) bool {
ids := idPath(cat, path) ids := exchangeIDPath(cat, path)
for _, c := range categoryPathSet[cat] { for _, c := range categoryPathSet[cat] {
target := s.Get(c) target := s.Get(c)
if len(target) == 0 { if len(target) == 0 {
@ -427,6 +436,15 @@ func (s exchangeScope) excludesPath(cat exchangeCategory, path []string) bool {
return false return false
} }
// excludesInfo returns true if all filters in the scope matche the info.
func (s exchangeScope) excludesInfo(cat exchangeCategory, info *backup.ExchangeInfo) bool {
// todo: implement once filters used in scopes
if info == nil {
return false
}
return false
}
// temporary helper until filters replace string values for scopes. // temporary helper until filters replace string values for scopes.
func contains(super []string, sub string) bool { func contains(super []string, sub string) bool {
for _, s := range super { for _, s := range super {
@ -446,7 +464,7 @@ func contains(super []string, sub string) bool {
// Example: // Example:
// [tenantID, userID, "mail", mailFolder, mailID] // [tenantID, userID, "mail", mailFolder, mailID]
// => {exchUser: userID, exchMailFolder: mailFolder, exchMail: mailID} // => {exchUser: userID, exchMailFolder: mailFolder, exchMail: mailID}
func idPath(cat exchangeCategory, path []string) map[exchangeCategory]string { func exchangeIDPath(cat exchangeCategory, path []string) map[exchangeCategory]string {
m := map[exchangeCategory]string{} m := map[exchangeCategory]string{}
if len(path) == 0 { if len(path) == 0 {
return m return m
@ -484,7 +502,7 @@ func idPath(cat exchangeCategory, path []string) map[exchangeCategory]string {
// FilterDetails reduces the entries in a backupDetails struct to only // FilterDetails reduces the entries in a backupDetails struct to only
// those that match the inclusions and exclusions in the selector. // those that match the inclusions and exclusions in the selector.
func (s *ExchangeRestore) FilterDetails(deets *backup.Details) [][]string { func (s *ExchangeRestore) FilterDetails(deets *backup.Details) *backup.Details {
if deets == nil { if deets == nil {
return nil return nil
} }
@ -492,9 +510,10 @@ func (s *ExchangeRestore) FilterDetails(deets *backup.Details) [][]string {
entIncs := exchangeScopesByCategory(s.Includes) entIncs := exchangeScopesByCategory(s.Includes)
entExcs := exchangeScopesByCategory(s.Excludes) entExcs := exchangeScopesByCategory(s.Excludes)
refs := [][]string{} ents := []backup.DetailsEntry{}
for _, ent := range deets.Entries { for _, ent := range deets.Entries {
// todo: use Path pkg for this
path := strings.Split(ent.RepoRef, "/") path := strings.Split(ent.RepoRef, "/")
// not all paths will be len=3. Most should be longer. // not all paths will be len=3. Most should be longer.
// This just protects us from panicing four lines later. // This just protects us from panicing four lines later.
@ -513,14 +532,16 @@ func (s *ExchangeRestore) FilterDetails(deets *backup.Details) [][]string {
matched := matchExchangeEntry( matched := matchExchangeEntry(
cat, cat,
path, path,
ent.Exchange,
entIncs[cat.String()], entIncs[cat.String()],
entExcs[cat.String()]) entExcs[cat.String()])
if matched { if matched {
refs = append(refs, path) ents = append(ents, ent)
} }
} }
return refs deets.Entries = ents
return deets
} }
// groups each scope by its category of data (contact, event, or mail). // groups each scope by its category of data (contact, event, or mail).
@ -548,10 +569,15 @@ func exchangeScopesByCategory(scopes []map[string]string) map[string][]exchangeS
// compare each path to the included and excluded exchange scopes. Returns true // compare each path to the included and excluded exchange scopes. Returns true
// if the path is included, and not excluded. // if the path is included, and not excluded.
func matchExchangeEntry(cat exchangeCategory, path []string, incs, excs []exchangeScope) bool { func matchExchangeEntry(
cat exchangeCategory,
path []string,
info *backup.ExchangeInfo,
incs, excs []exchangeScope,
) bool {
var included bool var included bool
for _, inc := range incs { for _, inc := range incs {
if inc.includesPath(cat, path) { if inc.includesPath(cat, path) || inc.includesInfo(cat, info) {
included = true included = true
break break
} }
@ -562,7 +588,7 @@ func matchExchangeEntry(cat exchangeCategory, path []string, incs, excs []exchan
var excluded bool var excluded bool
for _, exc := range excs { for _, exc := range excs {
if exc.excludesPath(cat, path) { if exc.excludesPath(cat, path) || exc.excludesInfo(cat, info) {
excluded = true excluded = true
break break
} }

View File

@ -1,7 +1,6 @@
package selectors package selectors
import ( import (
"strings"
"testing" "testing"
"github.com/alcionai/corso/pkg/backup" "github.com/alcionai/corso/pkg/backup"
@ -475,6 +474,32 @@ func (suite *ExchangeSourceSuite) TestExchangeScope_Get() {
} }
} }
func (suite *ExchangeSourceSuite) TestExchangeScope_IncludesInfo() {
const (
TODO = "this is a placeholder, awaiting implemenation of filters"
)
var (
es = NewExchangeRestore()
)
table := []struct {
name string
scope []exchangeScope
info *backup.ExchangeInfo
expect assert.BoolAssertionFunc
}{
{"all user's items", es.Users(All()), nil, assert.False}, // false while a todo
}
for _, test := range table {
suite.T().Run(test.name, func(t *testing.T) {
scopes := extendExchangeScopeValues(All(), test.scope)
for _, scope := range scopes {
test.expect(t, scope.includesInfo(ExchangeMail, test.info))
}
})
}
}
func (suite *ExchangeSourceSuite) TestExchangeScope_IncludesPath() { func (suite *ExchangeSourceSuite) TestExchangeScope_IncludesPath() {
const ( const (
usr = "userID" usr = "userID"
@ -517,6 +542,32 @@ func (suite *ExchangeSourceSuite) TestExchangeScope_IncludesPath() {
} }
} }
func (suite *ExchangeSourceSuite) TestExchangeScope_ExcludesInfo() {
const (
TODO = "this is a placeholder, awaiting implemenation of filters"
)
var (
es = NewExchangeRestore()
)
table := []struct {
name string
scope []exchangeScope
info *backup.ExchangeInfo
expect assert.BoolAssertionFunc
}{
{"all user's items", es.Users(All()), nil, assert.False}, // false while a todo
}
for _, test := range table {
suite.T().Run(test.name, func(t *testing.T) {
scopes := extendExchangeScopeValues(None(), test.scope)
for _, scope := range scopes {
test.expect(t, scope.excludesInfo(ExchangeMail, test.info))
}
})
}
}
func (suite *ExchangeSourceSuite) TestExchangeScope_ExcludesPath() { func (suite *ExchangeSourceSuite) TestExchangeScope_ExcludesPath() {
const ( const (
usr = "userID" usr = "userID"
@ -623,18 +674,14 @@ func (suite *ExchangeSourceSuite) TestExchangeRestore_FilterDetails() {
event = "tid/uid/event/eid" event = "tid/uid/event/eid"
mail = "tid/uid/mail/mfld/mid" mail = "tid/uid/mail/mfld/mid"
) )
split := func(s ...string) [][]string { arr := func(s ...string) []string {
r := [][]string{} return s
for _, ss := range s {
r = append(r, strings.Split(ss, "/"))
}
return r
} }
table := []struct { table := []struct {
name string name string
deets *backup.Details deets *backup.Details
makeSelector func() *ExchangeRestore makeSelector func() *ExchangeRestore
expect [][]string expect []string
}{ }{
{ {
"no refs", "no refs",
@ -644,7 +691,7 @@ func (suite *ExchangeSourceSuite) TestExchangeRestore_FilterDetails() {
er.Include(er.Users(All())) er.Include(er.Users(All()))
return er return er
}, },
[][]string{}, []string{},
}, },
{ {
"contact only", "contact only",
@ -654,7 +701,7 @@ func (suite *ExchangeSourceSuite) TestExchangeRestore_FilterDetails() {
er.Include(er.Users(All())) er.Include(er.Users(All()))
return er return er
}, },
split(contact), arr(contact),
}, },
{ {
"event only", "event only",
@ -664,7 +711,7 @@ func (suite *ExchangeSourceSuite) TestExchangeRestore_FilterDetails() {
er.Include(er.Users(All())) er.Include(er.Users(All()))
return er return er
}, },
split(event), arr(event),
}, },
{ {
"mail only", "mail only",
@ -674,7 +721,7 @@ func (suite *ExchangeSourceSuite) TestExchangeRestore_FilterDetails() {
er.Include(er.Users(All())) er.Include(er.Users(All()))
return er return er
}, },
split(mail), arr(mail),
}, },
{ {
"all", "all",
@ -684,7 +731,7 @@ func (suite *ExchangeSourceSuite) TestExchangeRestore_FilterDetails() {
er.Include(er.Users(All())) er.Include(er.Users(All()))
return er return er
}, },
split(contact, event, mail), arr(contact, event, mail),
}, },
{ {
"only match contact", "only match contact",
@ -694,7 +741,7 @@ func (suite *ExchangeSourceSuite) TestExchangeRestore_FilterDetails() {
er.Include(er.Contacts([]string{"uid"}, []string{"cfld"}, []string{"cid"})) er.Include(er.Contacts([]string{"uid"}, []string{"cfld"}, []string{"cid"}))
return er return er
}, },
split(contact), arr(contact),
}, },
{ {
"only match event", "only match event",
@ -704,7 +751,7 @@ func (suite *ExchangeSourceSuite) TestExchangeRestore_FilterDetails() {
er.Include(er.Events([]string{"uid"}, []string{"eid"})) er.Include(er.Events([]string{"uid"}, []string{"eid"}))
return er return er
}, },
split(event), arr(event),
}, },
{ {
"only match mail", "only match mail",
@ -714,7 +761,7 @@ func (suite *ExchangeSourceSuite) TestExchangeRestore_FilterDetails() {
er.Include(er.Mails([]string{"uid"}, []string{"mfld"}, []string{"mid"})) er.Include(er.Mails([]string{"uid"}, []string{"mfld"}, []string{"mid"}))
return er return er
}, },
split(mail), arr(mail),
}, },
{ {
"exclude contact", "exclude contact",
@ -725,7 +772,7 @@ func (suite *ExchangeSourceSuite) TestExchangeRestore_FilterDetails() {
er.Exclude(er.Contacts([]string{"uid"}, []string{"cfld"}, []string{"cid"})) er.Exclude(er.Contacts([]string{"uid"}, []string{"cfld"}, []string{"cid"}))
return er return er
}, },
split(event, mail), arr(event, mail),
}, },
{ {
"exclude event", "exclude event",
@ -736,7 +783,7 @@ func (suite *ExchangeSourceSuite) TestExchangeRestore_FilterDetails() {
er.Exclude(er.Events([]string{"uid"}, []string{"eid"})) er.Exclude(er.Events([]string{"uid"}, []string{"eid"}))
return er return er
}, },
split(contact, mail), arr(contact, mail),
}, },
{ {
"exclude mail", "exclude mail",
@ -747,14 +794,15 @@ func (suite *ExchangeSourceSuite) TestExchangeRestore_FilterDetails() {
er.Exclude(er.Mails([]string{"uid"}, []string{"mfld"}, []string{"mid"})) er.Exclude(er.Mails([]string{"uid"}, []string{"mfld"}, []string{"mid"}))
return er return er
}, },
split(contact, event), arr(contact, event),
}, },
} }
for _, test := range table { for _, test := range table {
suite.T().Run(test.name, func(t *testing.T) { suite.T().Run(test.name, func(t *testing.T) {
sel := test.makeSelector() sel := test.makeSelector()
results := sel.FilterDetails(test.deets) results := sel.FilterDetails(test.deets)
assert.Equal(t, test.expect, results) paths := results.Paths()
assert.Equal(t, test.expect, paths)
}) })
} }
} }
@ -804,6 +852,7 @@ func (suite *ExchangeSourceSuite) TestExchangeScopesByCategory() {
} }
func (suite *ExchangeSourceSuite) TestMatchExchangeEntry() { func (suite *ExchangeSourceSuite) TestMatchExchangeEntry() {
var TODO_EXCHANGE_INFO *backup.ExchangeInfo
const ( const (
mail = "mailID" mail = "mailID"
cat = ExchangeMail cat = ExchangeMail
@ -846,7 +895,7 @@ func (suite *ExchangeSourceSuite) TestMatchExchangeEntry() {
} }
for _, test := range table { for _, test := range table {
suite.T().Run(test.name, func(t *testing.T) { suite.T().Run(test.name, func(t *testing.T) {
test.expect(t, matchExchangeEntry(cat, path, test.includes, test.excludes)) test.expect(t, matchExchangeEntry(cat, path, TODO_EXCHANGE_INFO, test.includes, test.excludes))
}) })
} }
} }