From 65d6780906617640c0cfb1cbae96fd2c9a738775 Mon Sep 17 00:00:00 2001 From: Keepers Date: Fri, 14 Oct 2022 11:01:27 -0600 Subject: [PATCH] check for flag population instead of empty string (#1075) Adds a processor that confirms whether user has added a value for a flag in the cmd, or if it is the default value. This map of valued flags is added to the service opts structs to for validation. Also migrates many service flags to utils as consts so that these values can be maintained as consistent across packages. --- src/cli/backup/exchange.go | 62 +++---- src/cli/backup/exchange_integration_test.go | 13 +- src/cli/backup/onedrive.go | 43 ++--- src/cli/restore/exchange.go | 54 ++++--- src/cli/restore/exchange_integration_test.go | 13 +- src/cli/restore/onedrive.go | 36 +++-- src/cli/utils/exchange.go | 56 +++++-- src/cli/utils/exchange_test.go | 162 +++++++++---------- src/cli/utils/flags.go | 52 ++++++ src/cli/utils/onedrive.go | 43 +++-- src/cli/utils/testdata/opts.go | 76 ++++++++- src/cli/utils/utils.go | 36 +---- 12 files changed, 396 insertions(+), 250 deletions(-) create mode 100644 src/cli/utils/flags.go diff --git a/src/cli/backup/exchange.go b/src/cli/backup/exchange.go index dc4af7f48..11f29a3e3 100644 --- a/src/cli/backup/exchange.go +++ b/src/cli/backup/exchange.go @@ -110,11 +110,11 @@ func addExchangeCommands(parent *cobra.Command) *cobra.Command { // More generic (ex: --user) and more frequently used flags take precedence. fs.StringSliceVar( &user, - "user", nil, + utils.UserFN, nil, "Backup Exchange data by user ID; accepts '"+utils.Wildcard+"' to select all users") fs.StringSliceVar( &exchangeData, - "data", nil, + utils.DataFN, nil, "Select one or more types of data to backup: "+dataEmail+", "+dataContacts+", or "+dataEvents) options.AddOperationFlags(c) @@ -134,83 +134,83 @@ func addExchangeCommands(parent *cobra.Command) *cobra.Command { // Flags addition ordering should follow the order we want them to appear in help and docs: // More generic (ex: --user) and more frequently used flags take precedence. fs.StringVar(&backupID, - "backup", "", + utils.BackupFN, "", "ID of the backup to explore. (required)") - cobra.CheckErr(c.MarkFlagRequired("backup")) + cobra.CheckErr(c.MarkFlagRequired(utils.BackupFN)) fs.StringSliceVar( &user, - "user", nil, + utils.UserFN, nil, "Select backup details by user ID; accepts '"+utils.Wildcard+"' to select all users.") // email flags fs.StringSliceVar( &email, - "email", nil, + utils.EmailFN, nil, "Select backup details for emails by email ID; accepts '"+utils.Wildcard+"' to select all emails.") fs.StringSliceVar( &emailFolder, - "email-folder", nil, + utils.EmailFolderFN, nil, "Select backup details for emails within a folder; accepts '"+utils.Wildcard+"' to select all email folders.") fs.StringVar( &emailSubject, - "email-subject", "", + utils.EmailSubjectFN, "", "Select backup details for emails with a subject containing this value.") fs.StringVar( &emailSender, - "email-sender", "", + utils.EmailSenderFN, "", "Select backup details for emails from a specific sender.") fs.StringVar( &emailReceivedAfter, - "email-received-after", "", + utils.EmailReceivedAfterFN, "", "Select backup details for emails received after this datetime.") fs.StringVar( &emailReceivedBefore, - "email-received-before", "", + utils.EmailReceivedBeforeFN, "", "Select backup details for emails received before this datetime.") // event flags fs.StringSliceVar( &event, - "event", nil, + utils.EventFN, nil, "Select backup details for events by event ID; accepts '"+utils.Wildcard+"' to select all events.") fs.StringSliceVar( &eventCalendar, - "event-calendar", nil, + utils.EventCalendarFN, nil, "Select backup details for events under a calendar; accepts '"+utils.Wildcard+"' to select all events.") fs.StringVar( &eventSubject, - "event-subject", "", + utils.EventSubjectFN, "", "Select backup details for events with a subject containing this value.") fs.StringVar( &eventOrganizer, - "event-organizer", "", + utils.EventOrganizerFN, "", "Select backup details for events from a specific organizer.") fs.StringVar( &eventRecurs, - "event-recurs", "", + utils.EventRecursFN, "", "Select backup details for recurring events. Use `--event-recurs false` to select non-recurring events.") fs.StringVar( &eventStartsAfter, - "event-starts-after", "", + utils.EventStartsAfterFN, "", "Select backup details for events starting after this datetime.") fs.StringVar( &eventStartsBefore, - "event-starts-before", "", + utils.EventStartsBeforeFN, "", "Select backup details for events starting before this datetime.") // contact flags fs.StringSliceVar( &contact, - "contact", nil, + utils.ContactFN, nil, "Select backup details for contacts by contact ID; accepts '"+utils.Wildcard+"' to select all contacts.") fs.StringSliceVar( &contactFolder, - "contact-folder", nil, + utils.ContactFolderFN, nil, "Select backup details for contacts within a folder; accepts '"+utils.Wildcard+"' to select all contact folders.") fs.StringVar( &contactName, - "contact-name", "", + utils.ContactNameFN, "", "Select backup details for contacts whose contact name contains this value.") case deleteCommand: @@ -219,8 +219,10 @@ func addExchangeCommands(parent *cobra.Command) *cobra.Command { c.Use = c.Use + " " + exchangeServiceCommandDeleteUseSuffix c.Example = exchangeServiceCommandDeleteExamples - fs.StringVar(&backupID, "backup", "", "ID of the backup to delete. (required)") - cobra.CheckErr(c.MarkFlagRequired("backup")) + fs.StringVar(&backupID, + utils.BackupFN, "", + "ID of the backup to delete. (required)") + cobra.CheckErr(c.MarkFlagRequired(utils.BackupFN)) } return c @@ -401,12 +403,12 @@ func detailsExchangeCmd(cmd *cobra.Command, args []string) error { ctx := cmd.Context() opts := utils.ExchangeOpts{ - Contacts: contact, - ContactFolders: contactFolder, - Emails: email, - EmailFolders: emailFolder, - Events: event, - EventCalendars: eventCalendar, + Contact: contact, + ContactFolder: contactFolder, + Email: email, + EmailFolder: emailFolder, + Event: event, + EventCalendar: eventCalendar, Users: user, ContactName: contactName, EmailReceivedAfter: emailReceivedAfter, @@ -418,6 +420,8 @@ func detailsExchangeCmd(cmd *cobra.Command, args []string) error { EventStartsAfter: eventStartsAfter, EventStartsBefore: eventStartsBefore, EventSubject: eventSubject, + + Populated: utils.GetPopulatedFlags(cmd), } s, acct, err := config.GetStorageAndAccount(ctx, true, nil) diff --git a/src/cli/backup/exchange_integration_test.go b/src/cli/backup/exchange_integration_test.go index 4593d2175..39e888971 100644 --- a/src/cli/backup/exchange_integration_test.go +++ b/src/cli/backup/exchange_integration_test.go @@ -14,6 +14,7 @@ import ( "github.com/alcionai/corso/src/cli" "github.com/alcionai/corso/src/cli/config" "github.com/alcionai/corso/src/cli/print" + "github.com/alcionai/corso/src/cli/utils" "github.com/alcionai/corso/src/internal/connector/exchange" "github.com/alcionai/corso/src/internal/operations" "github.com/alcionai/corso/src/internal/tester" @@ -108,8 +109,8 @@ func (suite *BackupExchangeIntegrationSuite) TestExchangeBackupCmd() { cmd := tester.StubRootCmd( "backup", "create", "exchange", "--config-file", suite.cfgFP, - "--user", suite.m365UserID, - "--data", set.String()) + "--"+utils.UserFN, suite.m365UserID, + "--"+utils.DataFN, set.String()) cli.BuildCommandTree(cmd) cmd.SetOut(&recorder) @@ -327,7 +328,7 @@ func (suite *PreparedBackupExchangeIntegrationSuite) TestExchangeDetailsCmd() { cmd := tester.StubRootCmd( "backup", "details", "exchange", "--config-file", suite.cfgFP, - "--backup", string(bID)) + "--"+utils.BackupFN, string(bID)) cli.BuildCommandTree(cmd) cmd.SetOut(&suite.recorder) @@ -358,7 +359,7 @@ func (suite *PreparedBackupExchangeIntegrationSuite) TestExchangeDetailsCmd() { } // At least the prefix of the path should be encoded as folders. - assert.Greater(suite.T(), foundFolders, 4) + assert.Greater(t, foundFolders, 4) }) } } @@ -441,7 +442,7 @@ func (suite *BackupDeleteExchangeIntegrationSuite) TestExchangeBackupDeleteCmd() cmd := tester.StubRootCmd( "backup", "delete", "exchange", "--config-file", suite.cfgFP, - "--backup", string(suite.backupOp.Results.BackupID)) + "--"+utils.BackupFN, string(suite.backupOp.Results.BackupID)) cli.BuildCommandTree(cmd) // run the command @@ -467,7 +468,7 @@ func (suite *BackupDeleteExchangeIntegrationSuite) TestExchangeBackupDeleteCmd_U cmd := tester.StubRootCmd( "backup", "delete", "exchange", "--config-file", suite.cfgFP, - "--backup", uuid.NewString()) + "--"+utils.BackupFN, uuid.NewString()) cli.BuildCommandTree(cmd) // unknown backupIDs should error since the modelStore can't find the backup diff --git a/src/cli/backup/onedrive.go b/src/cli/backup/onedrive.go index 08a4252a8..701c9d449 100644 --- a/src/cli/backup/onedrive.go +++ b/src/cli/backup/onedrive.go @@ -79,7 +79,8 @@ func addOneDriveCommands(parent *cobra.Command) *cobra.Command { c.Use = c.Use + " " + oneDriveServiceCommandCreateUseSuffix c.Example = oneDriveServiceCommandCreateExamples - fs.StringArrayVar(&user, "user", nil, + fs.StringArrayVar(&user, + utils.UserFN, nil, "Backup OneDrive data by user ID; accepts '"+utils.Wildcard+"' to select all users. (required)") options.AddOperationFlags(c) @@ -96,39 +97,41 @@ func addOneDriveCommands(parent *cobra.Command) *cobra.Command { c.Use = c.Use + " " + oneDriveServiceCommandDetailsUseSuffix c.Example = oneDriveServiceCommandDetailsExamples - fs.StringVar(&backupID, "backup", "", "ID of the backup to explore. (required)") - cobra.CheckErr(c.MarkFlagRequired("backup")) + fs.StringVar(&backupID, + utils.BackupFN, "", + "ID of the backup to explore. (required)") + cobra.CheckErr(c.MarkFlagRequired(utils.BackupFN)) // onedrive hierarchy flags fs.StringSliceVar( &folderPaths, - "folder", nil, + utils.FolderFN, nil, "Select backup details by OneDrive folder; defaults to root.") fs.StringSliceVar( &fileNames, - "file", nil, + utils.FileFN, nil, "Select backup details by file name or ID.") // onedrive info flags fs.StringVar( &fileCreatedAfter, - "file-created-after", "", + utils.FileCreatedAfterFN, "", "Select backup details for files created after this datetime.") fs.StringVar( &fileCreatedBefore, - "file-created-before", "", + utils.FileCreatedBeforeFN, "", "Select backup details for files created before this datetime.") fs.StringVar( &fileModifiedAfter, - "file-modified-after", "", + utils.FileModifiedAfterFN, "", "Select backup details for files modified after this datetime.") fs.StringVar( &fileModifiedBefore, - "file-modified-before", "", + utils.FileModifiedBeforeFN, "", "Select backup details for files modified before this datetime.") case deleteCommand: @@ -137,8 +140,10 @@ func addOneDriveCommands(parent *cobra.Command) *cobra.Command { c.Use = c.Use + " " + oneDriveServiceCommandDeleteUseSuffix c.Example = oneDriveServiceCommandDeleteExamples - fs.StringVar(&backupID, "backup", "", "ID of the backup to delete. (required)") - cobra.CheckErr(c.MarkFlagRequired("backup")) + fs.StringVar(&backupID, + utils.BackupFN, "", + "ID of the backup to delete. (required)") + cobra.CheckErr(c.MarkFlagRequired(utils.BackupFN)) } return c @@ -311,13 +316,15 @@ func detailsOneDriveCmd(cmd *cobra.Command, args []string) error { defer utils.CloseRepo(ctx, r) opts := utils.OneDriveOpts{ - Users: user, - Paths: folderPaths, - Names: fileNames, - CreatedAfter: fileCreatedAfter, - CreatedBefore: fileCreatedBefore, - ModifiedAfter: fileModifiedAfter, - ModifiedBefore: fileModifiedBefore, + Users: user, + Paths: folderPaths, + Names: fileNames, + FileCreatedAfter: fileCreatedAfter, + FileCreatedBefore: fileCreatedBefore, + FileModifiedAfter: fileModifiedAfter, + FileModifiedBefore: fileModifiedBefore, + + Populated: utils.GetPopulatedFlags(cmd), } ds, err := runDetailsOneDriveCmd(ctx, r, backupID, opts) diff --git a/src/cli/restore/exchange.go b/src/cli/restore/exchange.go index db106e7a2..5a8c73e40 100644 --- a/src/cli/restore/exchange.go +++ b/src/cli/restore/exchange.go @@ -56,79 +56,81 @@ func addExchangeCommands(parent *cobra.Command) *cobra.Command { // Flags addition ordering should follow the order we want them to appear in help and docs: // More generic (ex: --user) and more frequently used flags take precedence. // general flags - fs.StringVar(&backupID, "backup", "", "ID of the backup to restore. (required)") - cobra.CheckErr(c.MarkFlagRequired("backup")) + fs.StringVar(&backupID, + utils.BackupFN, "", + "ID of the backup to restore. (required)") + cobra.CheckErr(c.MarkFlagRequired(utils.BackupFN)) fs.StringSliceVar(&user, - "user", nil, + utils.UserFN, nil, "Restore data by user ID; accepts '"+utils.Wildcard+"' to select all users.") // email flags fs.StringSliceVar(&email, - "email", nil, + utils.EmailFN, nil, "Restore emails by ID; accepts '"+utils.Wildcard+"' to select all emails.") fs.StringSliceVar( &emailFolder, - "email-folder", nil, + utils.EmailFolderFN, nil, "Restore emails within a folder; accepts '"+utils.Wildcard+"' to select all email folders.") fs.StringVar( &emailSubject, - "email-subject", "", + utils.EmailSubjectFN, "", "Restore emails with a subject containing this value.") fs.StringVar( &emailSender, - "email-sender", "", + utils.EmailSenderFN, "", "Restore emails from a specific sender.") fs.StringVar( &emailReceivedAfter, - "email-received-after", "", + utils.EmailReceivedAfterFN, "", "Restore emails received after this datetime.") fs.StringVar( &emailReceivedBefore, - "email-received-before", "", + utils.EmailReceivedBeforeFN, "", "Restore emails received before this datetime.") // event flags fs.StringSliceVar(&event, - "event", nil, + utils.EventFN, nil, "Restore events by event ID; accepts '"+utils.Wildcard+"' to select all events.") fs.StringSliceVar( &eventCalendar, - "event-calendar", nil, + utils.EventCalendarFN, nil, "Restore events under a calendar; accepts '"+utils.Wildcard+"' to select all event calendars.") fs.StringVar( &eventSubject, - "event-subject", "", + utils.EventSubjectFN, "", "Restore events with a subject containing this value.") fs.StringVar( &eventOrganizer, - "event-organizer", "", + utils.EventOrganizerFN, "", "Restore events from a specific organizer.") fs.StringVar( &eventRecurs, - "event-recurs", "", + utils.EventRecursFN, "", "Restore recurring events. Use `--event-recurs false` to restore non-recurring events.") fs.StringVar( &eventStartsAfter, - "event-starts-after", "", + utils.EventStartsAfterFN, "", "Restore events starting after this datetime.") fs.StringVar( &eventStartsBefore, - "event-starts-before", "", + utils.EventStartsBeforeFN, "", "Restore events starting before this datetime.") // contacts flags fs.StringSliceVar( &contact, - "contact", nil, + utils.ContactFN, nil, "Restore contacts by contact ID; accepts '"+utils.Wildcard+"' to select all contacts.") fs.StringSliceVar( &contactFolder, - "contact-folder", nil, + utils.ContactFolderFN, nil, "Restore contacts within a folder; accepts '"+utils.Wildcard+"' to select all contact folders.") fs.StringVar( &contactName, - "contact-name", "", + utils.ContactNameFN, "", "Restore contacts whose contact name contains this value.") // others @@ -177,12 +179,12 @@ func restoreExchangeCmd(cmd *cobra.Command, args []string) error { } opts := utils.ExchangeOpts{ - Contacts: contact, - ContactFolders: contactFolder, - Emails: email, - EmailFolders: emailFolder, - Events: event, - EventCalendars: eventCalendar, + Contact: contact, + ContactFolder: contactFolder, + Email: email, + EmailFolder: emailFolder, + Event: event, + EventCalendar: eventCalendar, Users: user, ContactName: contactName, EmailReceivedAfter: emailReceivedAfter, @@ -194,6 +196,8 @@ func restoreExchangeCmd(cmd *cobra.Command, args []string) error { EventStartsAfter: eventStartsAfter, EventStartsBefore: eventStartsBefore, EventSubject: eventSubject, + + Populated: utils.GetPopulatedFlags(cmd), } if err := utils.ValidateExchangeRestoreFlags(backupID, opts); err != nil { diff --git a/src/cli/restore/exchange_integration_test.go b/src/cli/restore/exchange_integration_test.go index 79f68178b..b020d73f4 100644 --- a/src/cli/restore/exchange_integration_test.go +++ b/src/cli/restore/exchange_integration_test.go @@ -10,6 +10,7 @@ import ( "github.com/alcionai/corso/src/cli" "github.com/alcionai/corso/src/cli/config" + "github.com/alcionai/corso/src/cli/utils" "github.com/alcionai/corso/src/internal/connector/exchange" "github.com/alcionai/corso/src/internal/operations" "github.com/alcionai/corso/src/internal/tester" @@ -136,7 +137,7 @@ func (suite *RestoreExchangeIntegrationSuite) TestExchangeRestoreCmd() { cmd := tester.StubRootCmd( "restore", "exchange", "--config-file", suite.cfgFP, - "--backup", string(suite.backupOps[set].Results.BackupID)) + "--"+utils.BackupFN, string(suite.backupOps[set].Results.BackupID)) cli.BuildCommandTree(cmd) // run the command @@ -160,15 +161,15 @@ func (suite *RestoreExchangeIntegrationSuite) TestExchangeRestoreCmd_badTimeFlag var timeFilter string switch set { case email: - timeFilter = "--email-received-after" + timeFilter = "--" + utils.EmailReceivedAfterFN case events: - timeFilter = "--event-starts-after" + timeFilter = "--" + utils.EventStartsAfterFN } cmd := tester.StubRootCmd( "restore", "exchange", "--config-file", suite.cfgFP, - "--backup", string(suite.backupOps[set].Results.BackupID), + "--"+utils.BackupFN, string(suite.backupOps[set].Results.BackupID), timeFilter, "smarf") cli.BuildCommandTree(cmd) @@ -192,13 +193,13 @@ func (suite *RestoreExchangeIntegrationSuite) TestExchangeRestoreCmd_badBoolFlag var timeFilter string switch set { case events: - timeFilter = "--event-recurs" + timeFilter = "--" + utils.EventRecursFN } cmd := tester.StubRootCmd( "restore", "exchange", "--config-file", suite.cfgFP, - "--backup", string(suite.backupOps[set].Results.BackupID), + "--"+utils.BackupFN, string(suite.backupOps[set].Results.BackupID), timeFilter, "wingbat") cli.BuildCommandTree(cmd) diff --git a/src/cli/restore/onedrive.go b/src/cli/restore/onedrive.go index 63b2d9685..f27ae2a40 100644 --- a/src/cli/restore/onedrive.go +++ b/src/cli/restore/onedrive.go @@ -42,43 +42,45 @@ func addOneDriveCommands(parent *cobra.Command) *cobra.Command { // More generic (ex: --user) and more frequently used flags take precedence. fs.SortFlags = false - fs.StringVar(&backupID, "backup", "", "ID of the backup to restore. (required)") - cobra.CheckErr(c.MarkFlagRequired("backup")) + fs.StringVar(&backupID, + utils.BackupFN, "", + "ID of the backup to restore. (required)") + cobra.CheckErr(c.MarkFlagRequired(utils.BackupFN)) fs.StringSliceVar(&user, - "user", nil, + utils.UserFN, nil, "Restore data by user ID; accepts '"+utils.Wildcard+"' to select all users.") // onedrive hierarchy (path/name) flags fs.StringSliceVar( &folderPaths, - "folder", nil, + utils.FolderFN, nil, "Restore items by OneDrive folder; defaults to root") fs.StringSliceVar( &fileNames, - "file", nil, + utils.FileFN, nil, "Restore items by file name or ID") // onedrive info flags fs.StringVar( &fileCreatedAfter, - "file-created-after", "", + utils.FileCreatedAfterFN, "", "Restore files created after this datetime") fs.StringVar( &fileCreatedBefore, - "file-created-before", "", + utils.FileCreatedBeforeFN, "", "Restore files created before this datetime") fs.StringVar( &fileModifiedAfter, - "file-modified-after", "", + utils.FileModifiedAfterFN, "", "Restore files modified after this datetime") fs.StringVar( &fileModifiedBefore, - "file-modified-before", "", + utils.FileModifiedBeforeFN, "", "Restore files modified before this datetime") // others @@ -124,13 +126,15 @@ func restoreOneDriveCmd(cmd *cobra.Command, args []string) error { } opts := utils.OneDriveOpts{ - Users: user, - Paths: folderPaths, - Names: fileNames, - CreatedAfter: fileCreatedAfter, - CreatedBefore: fileCreatedBefore, - ModifiedAfter: fileModifiedAfter, - ModifiedBefore: fileModifiedBefore, + Users: user, + Paths: folderPaths, + Names: fileNames, + FileCreatedAfter: fileCreatedAfter, + FileCreatedBefore: fileCreatedBefore, + FileModifiedAfter: fileModifiedAfter, + FileModifiedBefore: fileModifiedBefore, + + Populated: utils.GetPopulatedFlags(cmd), } if err := utils.ValidateOneDriveRestoreFlags(backupID, opts); err != nil { diff --git a/src/cli/utils/exchange.go b/src/cli/utils/exchange.go index 9c66aea7e..b29dabf1a 100644 --- a/src/cli/utils/exchange.go +++ b/src/cli/utils/exchange.go @@ -6,13 +6,33 @@ import ( "github.com/alcionai/corso/src/pkg/selectors" ) +// flag names +const ( + ContactFN = "contacts" + ContactFolderFN = "contact-folder" + EmailFN = "email" + EmailFolderFN = "email-folder" + EventFN = "events" + EventCalendarFN = "event-calendar" + ContactNameFN = "contact-name" + EmailReceivedAfterFN = "email-received-after" + EmailReceivedBeforeFN = "email-received-before" + EmailSenderFN = "email-sender" + EmailSubjectFN = "email-subject" + EventOrganizerFN = "event-organizer" + EventRecursFN = "event-recurs" + EventStartsAfterFN = "event-starts-after" + EventStartsBeforeFN = "event-starts-before" + EventSubjectFN = "event-subject" +) + type ExchangeOpts struct { - Contacts []string - ContactFolders []string - Emails []string - EmailFolders []string - Events []string - EventCalendars []string + Contact []string + ContactFolder []string + Email []string + EmailFolder []string + Event []string + EventCalendar []string Users []string ContactName string EmailReceivedAfter string @@ -24,6 +44,8 @@ type ExchangeOpts struct { EventStartsAfter string EventStartsBefore string EventSubject string + + Populated PopulatedFlags } // AddExchangeInclude adds the scope of the provided values to the selector's @@ -77,23 +99,23 @@ func ValidateExchangeRestoreFlags(backupID string, opts ExchangeOpts) error { return errors.New("a backup ID is required") } - if !IsValidTimeFormat(opts.EmailReceivedAfter) { + if _, ok := opts.Populated[EmailReceivedAfterFN]; ok && !IsValidTimeFormat(opts.EmailReceivedAfter) { return errors.New("invalid time format for email-received-after") } - if !IsValidTimeFormat(opts.EmailReceivedBefore) { + if _, ok := opts.Populated[EmailReceivedBeforeFN]; ok && !IsValidTimeFormat(opts.EmailReceivedBefore) { return errors.New("invalid time format for email-received-before") } - if !IsValidTimeFormat(opts.EventStartsAfter) { + if _, ok := opts.Populated[EventStartsAfterFN]; ok && !IsValidTimeFormat(opts.EventStartsAfter) { return errors.New("invalid time format for event-starts-after") } - if !IsValidTimeFormat(opts.EventStartsBefore) { + if _, ok := opts.Populated[EventStartsBeforeFN]; ok && !IsValidTimeFormat(opts.EventStartsBefore) { return errors.New("invalid time format for event-starts-before") } - if !IsValidBool(opts.EventRecurs) { + if _, ok := opts.Populated[EventRecursFN]; ok && !IsValidBool(opts.EventRecurs) { return errors.New("invalid format for event-recurs") } @@ -106,9 +128,9 @@ func IncludeExchangeRestoreDataSelectors( sel *selectors.ExchangeRestore, opts ExchangeOpts, ) { - lc, lcf := len(opts.Contacts), len(opts.ContactFolders) - le, lef := len(opts.Emails), len(opts.EmailFolders) - lev, lec := len(opts.Events), len(opts.EventCalendars) + lc, lcf := len(opts.Contact), len(opts.ContactFolder) + le, lef := len(opts.Email), len(opts.EmailFolder) + lev, lec := len(opts.Event), len(opts.EventCalendar) // either scope the request to a set of users if lc+lcf+le+lef+lev+lec == 0 { if len(opts.Users) == 0 { @@ -121,9 +143,9 @@ func IncludeExchangeRestoreDataSelectors( } // or add selectors for each type of data - AddExchangeInclude(sel, opts.Users, opts.ContactFolders, opts.Contacts, sel.Contacts) - AddExchangeInclude(sel, opts.Users, opts.EmailFolders, opts.Emails, sel.Mails) - AddExchangeInclude(sel, opts.Users, opts.EventCalendars, opts.Events, sel.Events) + AddExchangeInclude(sel, opts.Users, opts.ContactFolder, opts.Contact, sel.Contacts) + AddExchangeInclude(sel, opts.Users, opts.EmailFolder, opts.Email, sel.Mails) + AddExchangeInclude(sel, opts.Users, opts.EventCalendar, opts.Event, sel.Events) } // FilterExchangeRestoreInfoSelectors builds the common info-selector filters. diff --git a/src/cli/utils/exchange_test.go b/src/cli/utils/exchange_test.go index 9f5dca66c..c355e789e 100644 --- a/src/cli/utils/exchange_test.go +++ b/src/cli/utils/exchange_test.go @@ -94,207 +94,207 @@ func (suite *ExchangeUtilsSuite) TestIncludeExchangeBackupDetailDataSelectors() { name: "any users, any data", opts: utils.ExchangeOpts{ - Contacts: a, - ContactFolders: a, - Emails: a, - EmailFolders: a, - Events: a, - EventCalendars: a, - Users: a, + Contact: a, + ContactFolder: a, + Email: a, + EmailFolder: a, + Event: a, + EventCalendar: a, + Users: a, }, expectIncludeLen: 3, }, { name: "any users, any folders", opts: utils.ExchangeOpts{ - ContactFolders: a, - EmailFolders: a, - EventCalendars: a, - Users: a, + ContactFolder: a, + EmailFolder: a, + EventCalendar: a, + Users: a, }, expectIncludeLen: 3, }, { name: "single user, single of each data", opts: utils.ExchangeOpts{ - Contacts: stub, - ContactFolders: stub, - Emails: stub, - EmailFolders: stub, - Events: stub, - EventCalendars: stub, - Users: stub, + Contact: stub, + ContactFolder: stub, + Email: stub, + EmailFolder: stub, + Event: stub, + EventCalendar: stub, + Users: stub, }, expectIncludeLen: 3, }, { name: "single user, single of each folder", opts: utils.ExchangeOpts{ - ContactFolders: stub, - EmailFolders: stub, - EventCalendars: stub, - Users: stub, + ContactFolder: stub, + EmailFolder: stub, + EventCalendar: stub, + Users: stub, }, expectIncludeLen: 3, }, { name: "any users, contacts", opts: utils.ExchangeOpts{ - Contacts: a, - ContactFolders: stub, - Users: a, + Contact: a, + ContactFolder: stub, + Users: a, }, expectIncludeLen: 1, }, { name: "single user, contacts", opts: utils.ExchangeOpts{ - Contacts: stub, - ContactFolders: stub, - Users: stub, + Contact: stub, + ContactFolder: stub, + Users: stub, }, expectIncludeLen: 1, }, { name: "any users, emails", opts: utils.ExchangeOpts{ - Emails: a, - EmailFolders: stub, - Users: a, + Email: a, + EmailFolder: stub, + Users: a, }, expectIncludeLen: 1, }, { name: "single user, emails", opts: utils.ExchangeOpts{ - Emails: stub, - EmailFolders: stub, - Users: stub, + Email: stub, + EmailFolder: stub, + Users: stub, }, expectIncludeLen: 1, }, { name: "any users, events", opts: utils.ExchangeOpts{ - Events: a, - EventCalendars: a, - Users: a, + Event: a, + EventCalendar: a, + Users: a, }, expectIncludeLen: 1, }, { name: "single user, events", opts: utils.ExchangeOpts{ - Events: stub, - EventCalendars: stub, - Users: stub, + Event: stub, + EventCalendar: stub, + Users: stub, }, expectIncludeLen: 1, }, { name: "any users, contacts + email", opts: utils.ExchangeOpts{ - Contacts: a, - ContactFolders: a, - Emails: a, - EmailFolders: a, - Users: a, + Contact: a, + ContactFolder: a, + Email: a, + EmailFolder: a, + Users: a, }, expectIncludeLen: 2, }, { name: "single users, contacts + email", opts: utils.ExchangeOpts{ - Contacts: stub, - ContactFolders: stub, - Emails: stub, - EmailFolders: stub, - Users: stub, + Contact: stub, + ContactFolder: stub, + Email: stub, + EmailFolder: stub, + Users: stub, }, expectIncludeLen: 2, }, { name: "any users, email + event", opts: utils.ExchangeOpts{ - Emails: a, - EmailFolders: a, - Events: a, - EventCalendars: a, - Users: a, + Email: a, + EmailFolder: a, + Event: a, + EventCalendar: a, + Users: a, }, expectIncludeLen: 2, }, { name: "single users, email + event", opts: utils.ExchangeOpts{ - Emails: stub, - EmailFolders: stub, - Events: stub, - EventCalendars: stub, - Users: stub, + Email: stub, + EmailFolder: stub, + Event: stub, + EventCalendar: stub, + Users: stub, }, expectIncludeLen: 2, }, { name: "any users, event + contact", opts: utils.ExchangeOpts{ - Contacts: a, - ContactFolders: a, - Events: a, - EventCalendars: a, - Users: a, + Contact: a, + ContactFolder: a, + Event: a, + EventCalendar: a, + Users: a, }, expectIncludeLen: 2, }, { name: "single users, event + contact", opts: utils.ExchangeOpts{ - Contacts: stub, - ContactFolders: stub, - Events: stub, - EventCalendars: stub, - Users: stub, + Contact: stub, + ContactFolder: stub, + Event: stub, + EventCalendar: stub, + Users: stub, }, expectIncludeLen: 2, }, { name: "many users, events", opts: utils.ExchangeOpts{ - Events: many, - EventCalendars: many, - Users: many, + Event: many, + EventCalendar: many, + Users: many, }, expectIncludeLen: 1, }, { name: "many users, events + contacts", opts: utils.ExchangeOpts{ - Contacts: many, - ContactFolders: many, - Events: many, - EventCalendars: many, - Users: many, + Contact: many, + ContactFolder: many, + Event: many, + EventCalendar: many, + Users: many, }, expectIncludeLen: 2, }, { name: "mail, no folder or user", opts: utils.ExchangeOpts{ - Emails: stub, + Email: stub, }, expectIncludeLen: 1, }, { name: "contacts, no folder or user", opts: utils.ExchangeOpts{ - Contacts: stub, + Contact: stub, }, expectIncludeLen: 1, }, { name: "event, no folder or user", opts: utils.ExchangeOpts{ - Events: stub, + Event: stub, }, expectIncludeLen: 1, }, diff --git a/src/cli/utils/flags.go b/src/cli/utils/flags.go new file mode 100644 index 000000000..1730eaeda --- /dev/null +++ b/src/cli/utils/flags.go @@ -0,0 +1,52 @@ +package utils + +import ( + "strconv" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + "github.com/alcionai/corso/src/internal/common" +) + +type PopulatedFlags map[string]struct{} + +func (fs PopulatedFlags) populate(pf *pflag.Flag) { + if pf == nil { + return + } + + if pf.Changed { + fs[pf.Name] = struct{}{} + } +} + +// GetPopulatedFlags returns a map of flags that have been +// populated by the user. Entry keys match the flag's long +// name. Values are empty. +func GetPopulatedFlags(cmd *cobra.Command) PopulatedFlags { + pop := PopulatedFlags{} + + fs := cmd.Flags() + if fs == nil { + return pop + } + + fs.VisitAll(pop.populate) + + return pop +} + +// IsValidTimeFormat returns true if the input is recognized as a +// supported format by the common time parser. +func IsValidTimeFormat(in string) bool { + _, err := common.ParseTime(in) + return err == nil +} + +// IsValidTimeFormat returns true if the input is recognized as a +// boolean. +func IsValidBool(in string) bool { + _, err := strconv.ParseBool(in) + return err == nil +} diff --git a/src/cli/utils/onedrive.go b/src/cli/utils/onedrive.go index 3ce444e3a..324a54c6c 100644 --- a/src/cli/utils/onedrive.go +++ b/src/cli/utils/onedrive.go @@ -6,14 +6,27 @@ import ( "github.com/alcionai/corso/src/pkg/selectors" ) +const ( + FileFN = "file" + FolderFN = "folders" + NamesFN = "names" + PathsFN = "paths" + FileCreatedAfterFN = "file-created-after" + FileCreatedBeforeFN = "file-created-before" + FileModifiedAfterFN = "file-modified-after" + FileModifiedBeforeFN = "file-modified-before" +) + type OneDriveOpts struct { - Users []string - Names []string - Paths []string - CreatedAfter string - CreatedBefore string - ModifiedAfter string - ModifiedBefore string + Users []string + Names []string + Paths []string + FileCreatedAfter string + FileCreatedBefore string + FileModifiedAfter string + FileModifiedBefore string + + Populated PopulatedFlags } // ValidateOneDriveRestoreFlags checks common flags for correctness and interdependencies @@ -22,19 +35,19 @@ func ValidateOneDriveRestoreFlags(backupID string, opts OneDriveOpts) error { return errors.New("a backup ID is required") } - if !IsValidTimeFormat(opts.CreatedAfter) { + if _, ok := opts.Populated[FileCreatedAfterFN]; ok && !IsValidTimeFormat(opts.FileCreatedAfter) { return errors.New("invalid time format for created-after") } - if !IsValidTimeFormat(opts.CreatedBefore) { + if _, ok := opts.Populated[FileCreatedBeforeFN]; ok && !IsValidTimeFormat(opts.FileCreatedBefore) { return errors.New("invalid time format for created-before") } - if !IsValidTimeFormat(opts.ModifiedAfter) { + if _, ok := opts.Populated[FileModifiedAfterFN]; ok && !IsValidTimeFormat(opts.FileModifiedAfter) { return errors.New("invalid time format for modified-after") } - if !IsValidTimeFormat(opts.ModifiedBefore) { + if _, ok := opts.Populated[FileModifiedAfterFN]; ok && !IsValidTimeFormat(opts.FileModifiedBefore) { return errors.New("invalid time format for modified-before") } @@ -90,8 +103,8 @@ func FilterOneDriveRestoreInfoSelectors( sel *selectors.OneDriveRestore, opts OneDriveOpts, ) { - AddOneDriveFilter(sel, opts.CreatedAfter, sel.CreatedAfter) - AddOneDriveFilter(sel, opts.CreatedBefore, sel.CreatedBefore) - AddOneDriveFilter(sel, opts.ModifiedAfter, sel.ModifiedAfter) - AddOneDriveFilter(sel, opts.ModifiedBefore, sel.ModifiedBefore) + AddOneDriveFilter(sel, opts.FileCreatedAfter, sel.CreatedAfter) + AddOneDriveFilter(sel, opts.FileCreatedBefore, sel.CreatedBefore) + AddOneDriveFilter(sel, opts.FileModifiedAfter, sel.ModifiedAfter) + AddOneDriveFilter(sel, opts.FileModifiedBefore, sel.ModifiedBefore) } diff --git a/src/cli/utils/testdata/opts.go b/src/cli/utils/testdata/opts.go index c4113bbb5..6305cdd7e 100644 --- a/src/cli/utils/testdata/opts.go +++ b/src/cli/utils/testdata/opts.go @@ -31,30 +31,90 @@ var ( Name: "BadEmailReceiveAfter", Opts: utils.ExchangeOpts{ EmailReceivedAfter: "foo", + Populated: utils.PopulatedFlags{ + utils.EmailReceivedAfterFN: struct{}{}, + }, + }, + }, + { + Name: "EmptyEmailReceiveAfter", + Opts: utils.ExchangeOpts{ + EmailReceivedAfter: "", + Populated: utils.PopulatedFlags{ + utils.EmailReceivedAfterFN: struct{}{}, + }, }, }, { Name: "BadEmailReceiveBefore", Opts: utils.ExchangeOpts{ EmailReceivedBefore: "foo", + Populated: utils.PopulatedFlags{ + utils.EmailReceivedBeforeFN: struct{}{}, + }, + }, + }, + { + Name: "EmptyEmailReceiveBefore", + Opts: utils.ExchangeOpts{ + EmailReceivedBefore: "", + Populated: utils.PopulatedFlags{ + utils.EmailReceivedBeforeFN: struct{}{}, + }, }, }, { Name: "BadEventRecurs", Opts: utils.ExchangeOpts{ EventRecurs: "foo", + Populated: utils.PopulatedFlags{ + utils.EventRecursFN: struct{}{}, + }, + }, + }, + { + Name: "EmptyEventRecurs", + Opts: utils.ExchangeOpts{ + EventRecurs: "", + Populated: utils.PopulatedFlags{ + utils.EventRecursFN: struct{}{}, + }, }, }, { Name: "BadEventStartsAfter", Opts: utils.ExchangeOpts{ EventStartsAfter: "foo", + Populated: utils.PopulatedFlags{ + utils.EventStartsAfterFN: struct{}{}, + }, + }, + }, + { + Name: "EmptyEventStartsAfter", + Opts: utils.ExchangeOpts{ + EventStartsAfter: "", + Populated: utils.PopulatedFlags{ + utils.EventStartsAfterFN: struct{}{}, + }, }, }, { Name: "BadEventStartsBefore", Opts: utils.ExchangeOpts{ EventStartsBefore: "foo", + Populated: utils.PopulatedFlags{ + utils.EventStartsBeforeFN: struct{}{}, + }, + }, + }, + { + Name: "EmptyEventStartsBefore", + Opts: utils.ExchangeOpts{ + EventStartsBefore: "", + Populated: utils.PopulatedFlags{ + utils.EventStartsBeforeFN: struct{}{}, + }, }, }, } @@ -68,14 +128,14 @@ var ( Name: "Emails", Expected: testdata.ExchangeEmailItems, Opts: utils.ExchangeOpts{ - Emails: selectors.Any(), + Email: selectors.Any(), }, }, { Name: "EmailsFolderPrefixMatch", Expected: testdata.ExchangeEmailItems, Opts: utils.ExchangeOpts{ - EmailFolders: []string{testdata.ExchangeEmailInboxPath.Folder()}, + EmailFolder: []string{testdata.ExchangeEmailInboxPath.Folder()}, }, }, { @@ -109,21 +169,21 @@ var ( Name: "MailID", Expected: []details.DetailsEntry{testdata.ExchangeEmailItems[0]}, Opts: utils.ExchangeOpts{ - Emails: []string{testdata.ExchangeEmailItemPath1.Item()}, + Email: []string{testdata.ExchangeEmailItemPath1.Item()}, }, }, { Name: "MailShortRef", Expected: []details.DetailsEntry{testdata.ExchangeEmailItems[0]}, Opts: utils.ExchangeOpts{ - Emails: []string{testdata.ExchangeEmailItemPath1.ShortRef()}, + Email: []string{testdata.ExchangeEmailItemPath1.ShortRef()}, }, }, { Name: "MultipleMailShortRef", Expected: testdata.ExchangeEmailItems, Opts: utils.ExchangeOpts{ - Emails: []string{ + Email: []string{ testdata.ExchangeEmailItemPath1.ShortRef(), testdata.ExchangeEmailItemPath2.ShortRef(), }, @@ -134,7 +194,7 @@ var ( Expected: []details.DetailsEntry{testdata.ExchangeEmailItems[0]}, Opts: utils.ExchangeOpts{ EmailSubject: "foo", - Events: selectors.Any(), + Event: selectors.Any(), }, }, { @@ -152,8 +212,8 @@ var ( testdata.ExchangeEventsItems[0], }, Opts: utils.ExchangeOpts{ - Emails: []string{testdata.ExchangeEmailItemPath1.ShortRef()}, - Events: []string{testdata.ExchangeEventsItemPath1.ShortRef()}, + Email: []string{testdata.ExchangeEmailItemPath1.ShortRef()}, + Event: []string{testdata.ExchangeEventsItemPath1.ShortRef()}, }, }, } diff --git a/src/cli/utils/utils.go b/src/cli/utils/utils.go index b5039751a..072b21484 100644 --- a/src/cli/utils/utils.go +++ b/src/cli/utils/utils.go @@ -4,15 +4,20 @@ import ( "context" "errors" "fmt" - "strconv" "github.com/spf13/cobra" "github.com/spf13/pflag" - "github.com/alcionai/corso/src/internal/common" "github.com/alcionai/corso/src/pkg/repository" ) +// common flag names +const ( + BackupFN = "backup" + DataFN = "data" + UserFN = "user" +) + const ( Wildcard = "*" ) @@ -59,30 +64,3 @@ func AddCommand(parent, c *cobra.Command) (*cobra.Command, *pflag.FlagSet) { return c, c.Flags() } - -// IsValidTimeFormat returns true if the input is regonized as a -// supported format by the common time parser. Returns true if -// the input is zero valued, which indicates that the flag was not -// called. -func IsValidTimeFormat(in string) bool { - if len(in) == 0 { - return true - } - - _, err := common.ParseTime(in) - - return err == nil -} - -// IsValidTimeFormat returns true if the input is regonized as a -// boolean. Returns true if the input is zero valued, which -// indicates that the flag was not called. -func IsValidBool(in string) bool { - if len(in) == 0 { - return true - } - - _, err := strconv.ParseBool(in) - - return err == nil -}