diff --git a/CHANGELOG.md b/CHANGELOG.md index 3cf04ba6c..b65e0ccbf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Items not being deleted if they were created and deleted during item enumeration of a OneDrive backup. - Enable compression for all data uploaded by kopia. - SharePoint --folder selectors correctly return items. +- Fix Exchange cli args for filtering items ## [v0.6.1] (beta) - 2023-03-21 diff --git a/src/cli/backup/backup.go b/src/cli/backup/backup.go index a6e8c3af8..f407f084f 100644 --- a/src/cli/backup/backup.go +++ b/src/cli/backup/backup.go @@ -60,13 +60,6 @@ func AddCommands(cmd *cobra.Command) { // common flags and flag attachers for commands // --------------------------------------------------------------------------- -var ( - fileCreatedAfter string - fileCreatedBefore string - fileModifiedAfter string - fileModifiedBefore string -) - // list output filter flags var ( failedItemsFN = "failed-items" diff --git a/src/cli/backup/exchange.go b/src/cli/backup/exchange.go index 58fad9e29..81c3cad64 100644 --- a/src/cli/backup/exchange.go +++ b/src/cli/backup/exchange.go @@ -24,11 +24,6 @@ import ( // setup and globals // ------------------------------------------------------------------------------------------------ -// exchange bucket info from flags -var ( - exchangeData []string -) - const ( dataContacts = "contacts" dataEmail = "email" @@ -81,6 +76,8 @@ func addExchangeCommands(cmd *cobra.Command) *cobra.Command { switch cmd.Use { case createCommand: c, fs = utils.AddCommand(cmd, exchangeCreateCmd()) + fs.SortFlags = false + options.AddFeatureToggle(cmd, options.DisableIncrementals()) c.Use = c.Use + " " + exchangeServiceCommandCreateUseSuffix @@ -89,11 +86,7 @@ func addExchangeCommands(cmd *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. utils.AddUserFlag(c) - - fs.StringSliceVar( - &exchangeData, - utils.DataFN, nil, - "Select one or more types of data to backup: "+dataEmail+", "+dataContacts+", or "+dataEvents) + utils.AddDataFlag(c, []string{dataEmail, dataContacts, dataEvents}, false) options.AddFetchParallelismFlag(c) options.AddOperationFlags(c) @@ -107,7 +100,8 @@ func addExchangeCommands(cmd *cobra.Command) *cobra.Command { addRecoveredErrorsFN(c) case detailsCommand: - c, _ = utils.AddCommand(cmd, exchangeDetailsCmd()) + c, fs = utils.AddCommand(cmd, exchangeDetailsCmd()) + fs.SortFlags = false c.Use = c.Use + " " + exchangeServiceCommandDetailsUseSuffix c.Example = exchangeServiceCommandDetailsExamples @@ -120,7 +114,8 @@ func addExchangeCommands(cmd *cobra.Command) *cobra.Command { utils.AddExchangeDetailsAndRestoreFlags(c) case deleteCommand: - c, _ = utils.AddCommand(cmd, exchangeDeleteCmd()) + c, fs = utils.AddCommand(cmd, exchangeDeleteCmd()) + fs.SortFlags = false c.Use = c.Use + " " + exchangeServiceCommandDeleteUseSuffix c.Example = exchangeServiceCommandDeleteExamples @@ -153,7 +148,7 @@ func createExchangeCmd(cmd *cobra.Command, args []string) error { return nil } - if err := validateExchangeBackupCreateFlags(utils.User, exchangeData); err != nil { + if err := validateExchangeBackupCreateFlags(utils.User, utils.CategoryData); err != nil { return err } @@ -164,7 +159,7 @@ func createExchangeCmd(cmd *cobra.Command, args []string) error { defer utils.CloseRepo(ctx, r) - sel := exchangeBackupCreateSelectors(utils.User, exchangeData) + sel := exchangeBackupCreateSelectors(utils.User, utils.CategoryData) // TODO: log/print recoverable errors errs := fault.New(false) @@ -267,30 +262,7 @@ func detailsExchangeCmd(cmd *cobra.Command, args []string) error { } ctx := cmd.Context() - opts := utils.ExchangeOpts{ - Users: utils.User, - - Contact: utils.Contact, - ContactFolder: utils.ContactFolder, - ContactName: utils.ContactName, - - Email: utils.Email, - EmailFolder: utils.EmailFolder, - EmailReceivedAfter: utils.EmailReceivedAfter, - EmailReceivedBefore: utils.EmailReceivedBefore, - EmailSender: utils.EmailSender, - EmailSubject: utils.EmailSubject, - EventOrganizer: utils.EventOrganizer, - - Event: utils.Event, - EventCalendar: utils.EventCalendar, - EventRecurs: utils.EventRecurs, - EventStartsAfter: utils.EventStartsAfter, - EventStartsBefore: utils.EventStartsBefore, - EventSubject: utils.EventSubject, - - Populated: utils.GetPopulatedFlags(cmd), - } + opts := utils.MakeExchangeOpts(cmd) r, _, err := getAccountAndConnect(ctx) if err != nil { diff --git a/src/cli/backup/exchange_e2e_test.go b/src/cli/backup/exchange_e2e_test.go index 5dc70e357..f25e0ecfd 100644 --- a/src/cli/backup/exchange_e2e_test.go +++ b/src/cli/backup/exchange_e2e_test.go @@ -184,7 +184,7 @@ func (suite *BackupExchangeE2ESuite) TestExchangeBackupCmd() { "backup", "create", "exchange", "--config-file", suite.cfgFP, "--"+utils.UserFN, suite.m365UserID, - "--"+utils.DataFN, set.String()) + "--"+utils.CategoryDataFN, set.String()) cli.BuildCommandTree(cmd) cmd.SetOut(&recorder) @@ -221,7 +221,7 @@ func (suite *BackupExchangeE2ESuite) TestExchangeBackupCmd_UserNotInTenant() { "backup", "create", "exchange", "--config-file", suite.cfgFP, "--"+utils.UserFN, "foo@nothere.com", - "--"+utils.DataFN, set.String()) + "--"+utils.CategoryDataFN, set.String()) cli.BuildCommandTree(cmd) cmd.SetOut(&recorder) diff --git a/src/cli/backup/onedrive.go b/src/cli/backup/onedrive.go index 642c990ca..befaacf76 100644 --- a/src/cli/backup/onedrive.go +++ b/src/cli/backup/onedrive.go @@ -65,7 +65,9 @@ func addOneDriveCommands(cmd *cobra.Command) *cobra.Command { switch cmd.Use { case createCommand: - c, _ = utils.AddCommand(cmd, oneDriveCreateCmd()) + c, fs = utils.AddCommand(cmd, oneDriveCreateCmd()) + fs.SortFlags = false + options.AddFeatureToggle(cmd, options.EnablePermissionsBackup()) c.Use = c.Use + " " + oneDriveServiceCommandCreateUseSuffix @@ -84,7 +86,8 @@ func addOneDriveCommands(cmd *cobra.Command) *cobra.Command { addRecoveredErrorsFN(c) case detailsCommand: - c, _ = utils.AddCommand(cmd, oneDriveDetailsCmd()) + c, fs = utils.AddCommand(cmd, oneDriveDetailsCmd()) + fs.SortFlags = false c.Use = c.Use + " " + oneDriveServiceCommandDetailsUseSuffix c.Example = oneDriveServiceCommandDetailsExamples @@ -94,7 +97,8 @@ func addOneDriveCommands(cmd *cobra.Command) *cobra.Command { utils.AddOneDriveDetailsAndRestoreFlags(c) case deleteCommand: - c, _ = utils.AddCommand(cmd, oneDriveDeleteCmd()) + c, fs = utils.AddCommand(cmd, oneDriveDeleteCmd()) + fs.SortFlags = false c.Use = c.Use + " " + oneDriveServiceCommandDeleteUseSuffix c.Example = oneDriveServiceCommandDeleteExamples @@ -219,17 +223,7 @@ func detailsOneDriveCmd(cmd *cobra.Command, args []string) error { } ctx := cmd.Context() - opts := utils.OneDriveOpts{ - Users: utils.User, - FileNames: utils.FileName, - FolderPaths: utils.FolderPath, - FileCreatedAfter: utils.FileCreatedAfter, - FileCreatedBefore: utils.FileCreatedBefore, - FileModifiedAfter: utils.FileModifiedAfter, - FileModifiedBefore: utils.FileModifiedBefore, - - Populated: utils.GetPopulatedFlags(cmd), - } + opts := utils.MakeOneDriveOpts(cmd) r, _, err := getAccountAndConnect(ctx) if err != nil { diff --git a/src/cli/backup/sharepoint.go b/src/cli/backup/sharepoint.go index 59f1c8e0b..de32575bb 100644 --- a/src/cli/backup/sharepoint.go +++ b/src/cli/backup/sharepoint.go @@ -25,9 +25,6 @@ import ( // setup and globals // ------------------------------------------------------------------------------------------------ -// sharePoint bucket info from flags -var sharepointData []string - const ( dataLibraries = "libraries" dataPages = "pages" @@ -76,18 +73,14 @@ func addSharePointCommands(cmd *cobra.Command) *cobra.Command { switch cmd.Use { case createCommand: c, fs = utils.AddCommand(cmd, sharePointCreateCmd()) + fs.SortFlags = false c.Use = c.Use + " " + sharePointServiceCommandCreateUseSuffix c.Example = sharePointServiceCommandCreateExamples utils.AddSiteFlag(c) utils.AddSiteIDFlag(c) - - fs.StringSliceVar( - &sharepointData, - utils.DataFN, nil, - "Select one or more types of data to backup: "+dataLibraries+" or "+dataPages+".") - cobra.CheckErr(fs.MarkHidden(utils.DataFN)) + utils.AddDataFlag(c, []string{dataLibraries}, true) options.AddOperationFlags(c) @@ -101,7 +94,8 @@ func addSharePointCommands(cmd *cobra.Command) *cobra.Command { addRecoveredErrorsFN(c) case detailsCommand: - c, _ = utils.AddCommand(cmd, sharePointDetailsCmd()) + c, fs = utils.AddCommand(cmd, sharePointDetailsCmd()) + fs.SortFlags = false c.Use = c.Use + " " + sharePointServiceCommandDetailsUseSuffix c.Example = sharePointServiceCommandDetailsExamples @@ -111,7 +105,8 @@ func addSharePointCommands(cmd *cobra.Command) *cobra.Command { utils.AddSharePointDetailsAndRestoreFlags(c) case deleteCommand: - c, _ = utils.AddCommand(cmd, sharePointDeleteCmd()) + c, fs = utils.AddCommand(cmd, sharePointDeleteCmd()) + fs.SortFlags = false c.Use = c.Use + " " + sharePointServiceCommandDeleteUseSuffix c.Example = sharePointServiceCommandDeleteExamples @@ -145,7 +140,7 @@ func createSharePointCmd(cmd *cobra.Command, args []string) error { return nil } - if err := validateSharePointBackupCreateFlags(utils.SiteID, utils.WebURL, sharepointData); err != nil { + if err := validateSharePointBackupCreateFlags(utils.SiteID, utils.WebURL, utils.CategoryData); err != nil { return err } @@ -164,7 +159,7 @@ func createSharePointCmd(cmd *cobra.Command, args []string) error { return Only(ctx, clues.Wrap(err, "Failed to connect to Microsoft APIs")) } - sel, err := sharePointBackupCreateSelectors(ctx, utils.SiteID, utils.WebURL, sharepointData, gc) + sel, err := sharePointBackupCreateSelectors(ctx, utils.SiteID, utils.WebURL, utils.CategoryData, gc) if err != nil { return Only(ctx, clues.Wrap(err, "Retrieving up sharepoint sites by ID and URL")) } @@ -325,19 +320,7 @@ func detailsSharePointCmd(cmd *cobra.Command, args []string) error { } ctx := cmd.Context() - opts := utils.SharePointOpts{ - FolderPath: utils.FolderPath, - FileName: utils.FileName, - Library: utils.Library, - SiteID: utils.SiteID, - WebURL: utils.WebURL, - FileCreatedAfter: fileCreatedAfter, - FileCreatedBefore: fileCreatedBefore, - FileModifiedAfter: fileModifiedAfter, - FileModifiedBefore: fileModifiedBefore, - - Populated: utils.GetPopulatedFlags(cmd), - } + opts := utils.MakeSharePointOpts(cmd) r, _, err := getAccountAndConnect(ctx) if err != nil { diff --git a/src/cli/cli.go b/src/cli/cli.go index 916b14014..9202e5367 100644 --- a/src/cli/cli.go +++ b/src/cli/cli.go @@ -121,6 +121,7 @@ func CorsoCommand() *cobra.Command { func BuildCommandTree(cmd *cobra.Command) { // want to order flags explicitly cmd.PersistentFlags().SortFlags = false + utils.AddRunModeFlag(cmd, true) cmd.Flags().BoolP("version", "v", false, "current version info") cmd.PersistentPreRunE = preRun @@ -129,7 +130,6 @@ func BuildCommandTree(cmd *cobra.Command) { observe.AddProgressBarFlags(cmd) print.AddOutputFlag(cmd) options.AddGlobalOperationFlags(cmd) - cmd.SetUsageTemplate(indentExamplesTemplate(corsoCmd.UsageTemplate())) cmd.CompletionOptions.DisableDefaultCmd = true diff --git a/src/cli/config/config.go b/src/cli/config/config.go index d6c9830b5..e48f6f5ce 100644 --- a/src/cli/config/config.go +++ b/src/cli/config/config.go @@ -75,9 +75,7 @@ func AddConfigFlags(cmd *cobra.Command) { fs := cmd.PersistentFlags() fs.StringVar( &configFilePathFlag, - "config-file", - displayDefaultFP, - "config file location") + "config-file", displayDefaultFP, "config file location") } // --------------------------------------------------------------------------------------------------------- diff --git a/src/cli/restore/exchange.go b/src/cli/restore/exchange.go index d31b67a71..08b512309 100644 --- a/src/cli/restore/exchange.go +++ b/src/cli/restore/exchange.go @@ -16,30 +16,6 @@ import ( "github.com/alcionai/corso/src/pkg/repository" ) -// exchange bucket info from flags -var ( - user []string - - contact []string - contactFolder []string - contactName string - - email []string - emailFolder []string - emailReceivedAfter string - emailReceivedBefore string - emailSender string - emailSubject string - - event []string - eventCalendar []string - eventOrganizer string - eventRecurs string - eventStartsAfter string - eventStartsBefore string - eventSubject string -) - // called by restore.go to map subcommands to provider-specific handling. func addExchangeCommands(cmd *cobra.Command) *cobra.Command { var ( @@ -106,26 +82,10 @@ func restoreExchangeCmd(cmd *cobra.Command, args []string) error { return nil } - opts := utils.ExchangeOpts{ - Contact: contact, - ContactFolder: contactFolder, - Email: email, - EmailFolder: emailFolder, - Event: event, - EventCalendar: eventCalendar, - Users: user, - ContactName: contactName, - EmailReceivedAfter: emailReceivedAfter, - EmailReceivedBefore: emailReceivedBefore, - EmailSender: emailSender, - EmailSubject: emailSubject, - EventOrganizer: eventOrganizer, - EventRecurs: eventRecurs, - EventStartsAfter: eventStartsAfter, - EventStartsBefore: eventStartsBefore, - EventSubject: eventSubject, + opts := utils.MakeExchangeOpts(cmd) - Populated: utils.GetPopulatedFlags(cmd), + if utils.RunMode == utils.RunModeFlagTest { + return nil } if err := utils.ValidateExchangeRestoreFlags(utils.BackupID, opts); err != nil { diff --git a/src/cli/restore/exchange_test.go b/src/cli/restore/exchange_test.go index d2d6c5d4d..11c968a3c 100644 --- a/src/cli/restore/exchange_test.go +++ b/src/cli/restore/exchange_test.go @@ -1,13 +1,17 @@ package restore import ( + "bytes" "testing" + "github.com/alcionai/clues" "github.com/spf13/cobra" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" + "github.com/alcionai/corso/src/cli/utils" + "github.com/alcionai/corso/src/cli/utils/testdata" "github.com/alcionai/corso/src/internal/tester" ) @@ -37,6 +41,10 @@ func (suite *ExchangeUnitSuite) TestAddExchangeCommands() { cmd := &cobra.Command{Use: test.use} + // normally a persisten flag from the root. + // required to ensure a dry run. + utils.AddRunModeFlag(cmd, true) + c := addExchangeCommands(cmd) require.NotNil(t, c) @@ -47,6 +55,59 @@ func (suite *ExchangeUnitSuite) TestAddExchangeCommands() { assert.Equal(t, test.expectUse, child.Use) assert.Equal(t, test.expectShort, child.Short) tester.AreSameFunc(t, test.expectRunE, child.RunE) + + // Test arg parsing for few args + cmd.SetArgs([]string{ + "exchange", + "--" + utils.RunModeFN, utils.RunModeFlagTest, + "--" + utils.BackupFN, testdata.BackupInpt, + + "--" + utils.ContactFN, testdata.FlgInpts(testdata.ContactInpt), + "--" + utils.ContactFolderFN, testdata.FlgInpts(testdata.ContactFldInpt), + "--" + utils.ContactNameFN, testdata.ContactNameInpt, + + "--" + utils.EmailFN, testdata.FlgInpts(testdata.EmailInpt), + "--" + utils.EmailFolderFN, testdata.FlgInpts(testdata.EmailFldInpt), + "--" + utils.EmailReceivedAfterFN, testdata.EmailReceivedAfterInpt, + "--" + utils.EmailReceivedBeforeFN, testdata.EmailReceivedBeforeInpt, + "--" + utils.EmailSenderFN, testdata.EmailSenderInpt, + "--" + utils.EmailSubjectFN, testdata.EmailSubjectInpt, + + "--" + utils.EventFN, testdata.FlgInpts(testdata.EventInpt), + "--" + utils.EventCalendarFN, testdata.FlgInpts(testdata.EventCalInpt), + "--" + utils.EventOrganizerFN, testdata.EventOrganizerInpt, + "--" + utils.EventRecursFN, testdata.EventRecursInpt, + "--" + utils.EventStartsAfterFN, testdata.EventStartsAfterInpt, + "--" + utils.EventStartsBeforeFN, testdata.EventStartsBeforeInpt, + "--" + utils.EventSubjectFN, testdata.EventSubjectInpt, + }) + + cmd.SetOut(new(bytes.Buffer)) // drop output + cmd.SetErr(new(bytes.Buffer)) // drop output + err := cmd.Execute() + assert.NoError(t, err, clues.ToCore(err)) + + opts := utils.MakeExchangeOpts(cmd) + assert.Equal(t, testdata.BackupInpt, utils.BackupID) + + assert.ElementsMatch(t, testdata.ContactInpt, opts.Contact) + assert.ElementsMatch(t, testdata.ContactFldInpt, opts.ContactFolder) + assert.Equal(t, testdata.ContactNameInpt, opts.ContactName) + + assert.ElementsMatch(t, testdata.EmailInpt, opts.Email) + assert.ElementsMatch(t, testdata.EmailFldInpt, opts.EmailFolder) + assert.Equal(t, testdata.EmailReceivedAfterInpt, opts.EmailReceivedAfter) + assert.Equal(t, testdata.EmailReceivedBeforeInpt, opts.EmailReceivedBefore) + assert.Equal(t, testdata.EmailSenderInpt, opts.EmailSender) + assert.Equal(t, testdata.EmailSubjectInpt, opts.EmailSubject) + + assert.ElementsMatch(t, testdata.EventInpt, opts.Event) + assert.ElementsMatch(t, testdata.EventCalInpt, opts.EventCalendar) + assert.Equal(t, testdata.EventOrganizerInpt, opts.EventOrganizer) + assert.Equal(t, testdata.EventRecursInpt, opts.EventRecurs) + assert.Equal(t, testdata.EventStartsAfterInpt, opts.EventStartsAfter) + assert.Equal(t, testdata.EventStartsBeforeInpt, opts.EventStartsBefore) + assert.Equal(t, testdata.EventSubjectInpt, opts.EventSubject) }) } } diff --git a/src/cli/restore/onedrive.go b/src/cli/restore/onedrive.go index a5fa84975..89b6f0083 100644 --- a/src/cli/restore/onedrive.go +++ b/src/cli/restore/onedrive.go @@ -84,16 +84,10 @@ func restoreOneDriveCmd(cmd *cobra.Command, args []string) error { return nil } - opts := utils.OneDriveOpts{ - Users: user, - FileNames: utils.FileName, - FolderPaths: utils.FolderPath, - FileCreatedAfter: utils.FileCreatedAfter, - FileCreatedBefore: utils.FileCreatedBefore, - FileModifiedAfter: utils.FileModifiedAfter, - FileModifiedBefore: utils.FileModifiedBefore, + opts := utils.MakeOneDriveOpts(cmd) - Populated: utils.GetPopulatedFlags(cmd), + if utils.RunMode == utils.RunModeFlagTest { + return nil } if err := utils.ValidateOneDriveRestoreFlags(utils.BackupID, opts); err != nil { diff --git a/src/cli/restore/onedrive_test.go b/src/cli/restore/onedrive_test.go index 05a2a8cc5..df1073a69 100644 --- a/src/cli/restore/onedrive_test.go +++ b/src/cli/restore/onedrive_test.go @@ -1,14 +1,17 @@ package restore import ( + "bytes" "testing" + "github.com/alcionai/clues" "github.com/spf13/cobra" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" "github.com/alcionai/corso/src/cli/utils" + "github.com/alcionai/corso/src/cli/utils/testdata" "github.com/alcionai/corso/src/internal/tester" ) @@ -38,6 +41,10 @@ func (suite *OneDriveUnitSuite) TestAddOneDriveCommands() { cmd := &cobra.Command{Use: test.use} + // normally a persisten flag from the root. + // required to ensure a dry run. + utils.AddRunModeFlag(cmd, true) + c := addOneDriveCommands(cmd) require.NotNil(t, c) @@ -49,8 +56,33 @@ func (suite *OneDriveUnitSuite) TestAddOneDriveCommands() { assert.Equal(t, test.expectShort, child.Short) tester.AreSameFunc(t, test.expectRunE, child.RunE) - assert.NotNil(t, c.Flag(utils.BackupFN)) - assert.NotNil(t, c.Flag("restore-permissions")) + cmd.SetArgs([]string{ + "onedrive", + "--" + utils.RunModeFN, utils.RunModeFlagTest, + "--" + utils.BackupFN, testdata.BackupInpt, + + "--" + utils.FileFN, testdata.FlgInpts(testdata.FileNamesInpt), + "--" + utils.FolderFN, testdata.FlgInpts(testdata.FolderPathsInpt), + "--" + utils.FileCreatedAfterFN, testdata.FileCreatedAfterInpt, + "--" + utils.FileCreatedBeforeFN, testdata.FileCreatedBeforeInpt, + "--" + utils.FileModifiedAfterFN, testdata.FileModifiedAfterInpt, + "--" + utils.FileModifiedBeforeFN, testdata.FileModifiedBeforeInpt, + }) + + cmd.SetOut(new(bytes.Buffer)) // drop output + cmd.SetErr(new(bytes.Buffer)) // drop output + err := cmd.Execute() + assert.NoError(t, err, clues.ToCore(err)) + + opts := utils.MakeOneDriveOpts(cmd) + assert.Equal(t, testdata.BackupInpt, utils.BackupID) + + assert.ElementsMatch(t, testdata.FileNamesInpt, opts.FileNames) + assert.ElementsMatch(t, testdata.FolderPathsInpt, opts.FolderPaths) + assert.Equal(t, testdata.FileCreatedAfterInpt, opts.FileCreatedAfter) + assert.Equal(t, testdata.FileCreatedBeforeInpt, opts.FileCreatedBefore) + assert.Equal(t, testdata.FileModifiedAfterInpt, opts.FileModifiedAfter) + assert.Equal(t, testdata.FileModifiedBeforeInpt, opts.FileModifiedBefore) }) } } diff --git a/src/cli/restore/sharepoint.go b/src/cli/restore/sharepoint.go index 1d74cdb61..9194a0e0b 100644 --- a/src/cli/restore/sharepoint.go +++ b/src/cli/restore/sharepoint.go @@ -16,13 +16,6 @@ import ( "github.com/alcionai/corso/src/pkg/repository" ) -var ( - listItems []string - listPaths []string - pageFolders []string - pages []string -) - // called by restore.go to map subcommands to provider-specific handling. func addSharePointCommands(cmd *cobra.Command) *cobra.Command { var ( @@ -90,21 +83,10 @@ func restoreSharePointCmd(cmd *cobra.Command, args []string) error { return nil } - opts := utils.SharePointOpts{ - FileName: utils.FileName, - FolderPath: utils.FolderPath, - Library: utils.Library, - ListItem: listItems, - ListPath: listPaths, - PageFolder: pageFolders, - Page: pages, - SiteID: utils.SiteID, - WebURL: utils.WebURL, - FileCreatedAfter: utils.FileCreatedAfter, - FileCreatedBefore: utils.FileCreatedBefore, - FileModifiedAfter: utils.FileModifiedAfter, - FileModifiedBefore: utils.FileModifiedBefore, - Populated: utils.GetPopulatedFlags(cmd), + opts := utils.MakeSharePointOpts(cmd) + + if utils.RunMode == utils.RunModeFlagTest { + return nil } if err := utils.ValidateSharePointRestoreFlags(utils.BackupID, opts); err != nil { diff --git a/src/cli/restore/sharepoint_test.go b/src/cli/restore/sharepoint_test.go index 413090231..73d0e259c 100644 --- a/src/cli/restore/sharepoint_test.go +++ b/src/cli/restore/sharepoint_test.go @@ -1,13 +1,17 @@ package restore import ( + "bytes" "testing" + "github.com/alcionai/clues" "github.com/spf13/cobra" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" + "github.com/alcionai/corso/src/cli/utils" + "github.com/alcionai/corso/src/cli/utils/testdata" "github.com/alcionai/corso/src/internal/tester" ) @@ -37,6 +41,10 @@ func (suite *SharePointUnitSuite) TestAddSharePointCommands() { cmd := &cobra.Command{Use: test.use} + // normally a persisten flag from the root. + // required to ensure a dry run. + utils.AddRunModeFlag(cmd, true) + c := addSharePointCommands(cmd) require.NotNil(t, c) @@ -47,6 +55,48 @@ func (suite *SharePointUnitSuite) TestAddSharePointCommands() { assert.Equal(t, test.expectUse, child.Use) assert.Equal(t, test.expectShort, child.Short) tester.AreSameFunc(t, test.expectRunE, child.RunE) + + cmd.SetArgs([]string{ + "sharepoint", + "--" + utils.RunModeFN, utils.RunModeFlagTest, + "--" + utils.BackupFN, testdata.BackupInpt, + + "--" + utils.LibraryFN, testdata.LibraryInpt, + "--" + utils.FileFN, testdata.FlgInpts(testdata.FileNamesInpt), + "--" + utils.FolderFN, testdata.FlgInpts(testdata.FolderPathsInpt), + "--" + utils.FileCreatedAfterFN, testdata.FileCreatedAfterInpt, + "--" + utils.FileCreatedBeforeFN, testdata.FileCreatedBeforeInpt, + "--" + utils.FileModifiedAfterFN, testdata.FileModifiedAfterInpt, + "--" + utils.FileModifiedBeforeFN, testdata.FileModifiedBeforeInpt, + + "--" + utils.ListItemFN, testdata.FlgInpts(testdata.ListItemInpt), + "--" + utils.ListFolderFN, testdata.FlgInpts(testdata.ListFolderInpt), + + "--" + utils.PageFN, testdata.FlgInpts(testdata.PageInpt), + "--" + utils.PageFolderFN, testdata.FlgInpts(testdata.PageFolderInpt), + }) + + cmd.SetOut(new(bytes.Buffer)) // drop output + cmd.SetErr(new(bytes.Buffer)) // drop output + err := cmd.Execute() + assert.NoError(t, err, clues.ToCore(err)) + + opts := utils.MakeSharePointOpts(cmd) + assert.Equal(t, testdata.BackupInpt, utils.BackupID) + + assert.Equal(t, testdata.LibraryInpt, opts.Library) + assert.ElementsMatch(t, testdata.FileNamesInpt, opts.FileName) + assert.ElementsMatch(t, testdata.FolderPathsInpt, opts.FolderPath) + assert.Equal(t, testdata.FileCreatedAfterInpt, opts.FileCreatedAfter) + assert.Equal(t, testdata.FileCreatedBeforeInpt, opts.FileCreatedBefore) + assert.Equal(t, testdata.FileModifiedAfterInpt, opts.FileModifiedAfter) + assert.Equal(t, testdata.FileModifiedBeforeInpt, opts.FileModifiedBefore) + + assert.ElementsMatch(t, testdata.ListItemInpt, opts.ListItem) + assert.ElementsMatch(t, testdata.ListFolderInpt, opts.ListFolder) + + assert.ElementsMatch(t, testdata.PageInpt, opts.Page) + assert.ElementsMatch(t, testdata.PageFolderInpt, opts.PageFolder) }) } } diff --git a/src/cli/utils/exchange.go b/src/cli/utils/exchange.go index 63a815d0a..559e9d856 100644 --- a/src/cli/utils/exchange.go +++ b/src/cli/utils/exchange.go @@ -52,27 +52,58 @@ var ( ) type ExchangeOpts struct { - Contact []string - ContactFolder []string + Users []string + + Contact []string + ContactFolder []string + ContactName string + Email []string EmailFolder []string - Event []string - EventCalendar []string - Users []string - ContactName string EmailReceivedAfter string EmailReceivedBefore string EmailSender string EmailSubject string - EventOrganizer string - EventRecurs string - EventStartsAfter string - EventStartsBefore string - EventSubject string + + Event []string + EventCalendar []string + EventOrganizer string + EventRecurs string + EventStartsAfter string + EventStartsBefore string + EventSubject string Populated PopulatedFlags } +// populates an ExchangeOpts struct with the command's current flags. +func MakeExchangeOpts(cmd *cobra.Command) ExchangeOpts { + return ExchangeOpts{ + Users: User, + + Contact: Contact, + ContactFolder: ContactFolder, + ContactName: ContactName, + + Email: Email, + EmailFolder: EmailFolder, + EmailReceivedAfter: EmailReceivedAfter, + EmailReceivedBefore: EmailReceivedBefore, + EmailSender: EmailSender, + EmailSubject: EmailSubject, + + Event: Event, + EventCalendar: EventCalendar, + EventOrganizer: EventOrganizer, + EventRecurs: EventRecurs, + EventStartsAfter: EventStartsAfter, + EventStartsBefore: EventStartsBefore, + EventSubject: EventSubject, + + Populated: GetPopulatedFlags(cmd), + } +} + // AddExchangeDetailsAndRestoreFlags adds flags that are common to both the // details and restore commands. func AddExchangeDetailsAndRestoreFlags(cmd *cobra.Command) { diff --git a/src/cli/utils/flags.go b/src/cli/utils/flags.go index 1fb7ad5cf..4f183aecd 100644 --- a/src/cli/utils/flags.go +++ b/src/cli/utils/flags.go @@ -1,7 +1,9 @@ package utils import ( + "fmt" "strconv" + "strings" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -12,6 +14,10 @@ import ( // common flag vars var ( + // RunMode describes the type of run, such as: + // flagtest, dry, run. Should default to 'run'. + RunMode string + BackupID string FolderPath []string @@ -27,19 +33,25 @@ var ( WebURL []string User []string + + // for selection of data by category. eg: `--data email,contacts` + CategoryData []string ) // common flag names const ( - BackupFN = "backup" - DataFN = "data" - LibraryFN = "library" - SiteFN = "site" // site only accepts WebURL values - SiteIDFN = "site-id" // site-id accepts actual site ids - UserFN = "user" + RunModeFN = "run-mode" - FileFN = "file" - FolderFN = "folder" + BackupFN = "backup" + CategoryDataFN = "data" + + SiteFN = "site" // site only accepts WebURL values + SiteIDFN = "site-id" // site-id accepts actual site ids + UserFN = "user" + + LibraryFN = "library" + FileFN = "file" + FolderFN = "folder" FileCreatedAfterFN = "file-created-after" FileCreatedBeforeFN = "file-created-before" @@ -47,6 +59,12 @@ const ( FileModifiedBeforeFN = "file-modified-before" ) +// well-knwon flag values +const ( + RunModeFlagTest = "flag-test" + RunModeRun = "run" +) + // AddBackupIDFlag adds the --backup flag. func AddBackupIDFlag(cmd *cobra.Command, require bool) { cmd.Flags().StringVar(&BackupID, BackupFN, "", "ID of the backup to retrieve.") @@ -56,6 +74,47 @@ func AddBackupIDFlag(cmd *cobra.Command, require bool) { } } +func AddDataFlag(cmd *cobra.Command, allowed []string, hide bool) { + var ( + allowedMsg string + fs = cmd.Flags() + ) + + switch len(allowed) { + case 0: + return + case 1: + allowedMsg = allowed[0] + case 2: + allowedMsg = fmt.Sprintf("%s or %s", allowed[0], allowed[1]) + default: + allowedMsg = fmt.Sprintf( + "%s or %s", + strings.Join(allowed[:len(allowed)-1], ", "), + allowed[len(allowed)-1]) + } + + fs.StringSliceVar( + &CategoryData, + CategoryDataFN, nil, + "Select one or more types of data to backup: "+allowedMsg+".") + + if hide { + cobra.CheckErr(fs.MarkHidden(CategoryDataFN)) + } +} + +// AddRunModeFlag adds the hidden --run-mode flag. +func AddRunModeFlag(cmd *cobra.Command, persistent bool) { + fs := cmd.Flags() + if persistent { + fs = cmd.PersistentFlags() + } + + fs.StringVar(&RunMode, RunModeFN, "run", "What mode to run: dry, test, run. Defaults to run.") + cobra.CheckErr(fs.MarkHidden(RunModeFN)) +} + // AddUserFlag adds the --user flag. func AddUserFlag(cmd *cobra.Command) { cmd.Flags().StringSliceVar( diff --git a/src/cli/utils/onedrive.go b/src/cli/utils/onedrive.go index 0b4a2e489..c13e4afdd 100644 --- a/src/cli/utils/onedrive.go +++ b/src/cli/utils/onedrive.go @@ -8,7 +8,8 @@ import ( ) type OneDriveOpts struct { - Users []string + Users []string + FileNames []string FolderPaths []string FileCreatedAfter string @@ -19,6 +20,21 @@ type OneDriveOpts struct { Populated PopulatedFlags } +func MakeOneDriveOpts(cmd *cobra.Command) OneDriveOpts { + return OneDriveOpts{ + Users: User, + + FileNames: FileName, + FolderPaths: FolderPath, + FileCreatedAfter: FileCreatedAfter, + FileCreatedBefore: FileCreatedBefore, + FileModifiedAfter: FileModifiedAfter, + FileModifiedBefore: FileModifiedBefore, + + Populated: GetPopulatedFlags(cmd), + } +} + // AddOneDriveDetailsAndRestoreFlags adds flags that are common to both the // details and restore commands. func AddOneDriveDetailsAndRestoreFlags(cmd *cobra.Command) { diff --git a/src/cli/utils/sharepoint.go b/src/cli/utils/sharepoint.go index 3844bcccf..0357b0a01 100644 --- a/src/cli/utils/sharepoint.go +++ b/src/cli/utils/sharepoint.go @@ -8,82 +8,91 @@ import ( ) const ( + ListFolderFN = "list" ListItemFN = "list-item" - ListFN = "list" PageFolderFN = "page-folder" - PagesFN = "page" + PageFN = "page" ) // flag population variables var ( + ListFolder []string + ListItem []string PageFolder []string Page []string ) type SharePointOpts struct { - Library string - FileName []string // for libraries, to duplicate onedrive interface - FolderPath []string // for libraries, to duplicate onedrive interface - - ListItem []string - ListPath []string - - PageFolder []string - Page []string - SiteID []string WebURL []string + Library string + FileName []string // for libraries, to duplicate onedrive interface + FolderPath []string // for libraries, to duplicate onedrive interface FileCreatedAfter string FileCreatedBefore string FileModifiedAfter string FileModifiedBefore string + ListFolder []string + ListItem []string + + PageFolder []string + Page []string + Populated PopulatedFlags } +func MakeSharePointOpts(cmd *cobra.Command) SharePointOpts { + return SharePointOpts{ + SiteID: SiteID, + WebURL: WebURL, + + Library: Library, + FileName: FileName, + FolderPath: FolderPath, + FileCreatedAfter: FileCreatedAfter, + FileCreatedBefore: FileCreatedBefore, + FileModifiedAfter: FileModifiedAfter, + FileModifiedBefore: FileModifiedBefore, + + ListFolder: ListFolder, + ListItem: ListItem, + + Page: Page, + PageFolder: PageFolder, + + Populated: GetPopulatedFlags(cmd), + } +} + // AddSharePointDetailsAndRestoreFlags adds flags that are common to both the // details and restore commands. func AddSharePointDetailsAndRestoreFlags(cmd *cobra.Command) { fs := cmd.Flags() + // libraries + fs.StringVar( &Library, LibraryFN, "", "Select only this library; defaults to all libraries.") - fs.StringSliceVar( &FolderPath, FolderFN, nil, "Select by folder; defaults to root.") - fs.StringSliceVar( &FileName, FileFN, nil, "Select by file name.") - - fs.StringSliceVar( - &PageFolder, - PageFolderFN, nil, - "Select pages by folder name; accepts '"+Wildcard+"' to select all folders.") - cobra.CheckErr(fs.MarkHidden(PageFolderFN)) - - fs.StringSliceVar( - &Page, - PagesFN, nil, - "Select pages by item name; accepts '"+Wildcard+"' to select all pages within the site.") - cobra.CheckErr(fs.MarkHidden(PagesFN)) - fs.StringVar( &FileCreatedAfter, FileCreatedAfterFN, "", "Select files created after this datetime.") - fs.StringVar( &FileCreatedBefore, FileCreatedBeforeFN, "", "Select files created before this datetime.") - fs.StringVar( &FileModifiedAfter, FileModifiedAfterFN, "", @@ -92,6 +101,32 @@ func AddSharePointDetailsAndRestoreFlags(cmd *cobra.Command) { &FileModifiedBefore, FileModifiedBeforeFN, "", "Select files modified before this datetime.") + + // lists + + fs.StringSliceVar( + &ListFolder, + ListFolderFN, nil, + "Select lists by name; accepts '"+Wildcard+"' to select all lists.") + cobra.CheckErr(fs.MarkHidden(ListFolderFN)) + fs.StringSliceVar( + &ListItem, + ListItemFN, nil, + "Select lists by item name; accepts '"+Wildcard+"' to select all lists.") + cobra.CheckErr(fs.MarkHidden(ListItemFN)) + + // pages + + fs.StringSliceVar( + &PageFolder, + PageFolderFN, nil, + "Select pages by folder name; accepts '"+Wildcard+"' to select all pages.") + cobra.CheckErr(fs.MarkHidden(PageFolderFN)) + fs.StringSliceVar( + &Page, + PageFN, nil, + "Select pages by item name; accepts '"+Wildcard+"' to select all pages.") + cobra.CheckErr(fs.MarkHidden(PageFN)) } // ValidateSharePointRestoreFlags checks common flags for correctness and interdependencies @@ -140,7 +175,7 @@ func IncludeSharePointRestoreDataSelectors(opts SharePointOpts) *selectors.Share lfp, lfn := len(opts.FolderPath), len(opts.FileName) ls, lwu := len(opts.SiteID), len(opts.WebURL) - slp, sli := len(opts.ListPath), len(opts.ListItem) + slp, sli := len(opts.ListFolder), len(opts.ListItem) pf, pi := len(opts.PageFolder), len(opts.Page) if ls == 0 { @@ -176,8 +211,8 @@ func IncludeSharePointRestoreDataSelectors(opts SharePointOpts) *selectors.Share opts.ListItem = selectors.Any() } - opts.ListPath = trimFolderSlash(opts.ListPath) - containsFolders, prefixFolders := splitFoldersIntoContainsAndPrefix(opts.ListPath) + opts.ListFolder = trimFolderSlash(opts.ListFolder) + containsFolders, prefixFolders := splitFoldersIntoContainsAndPrefix(opts.ListFolder) if len(containsFolders) > 0 { sel.Include(sel.ListItems(containsFolders, opts.ListItem)) diff --git a/src/cli/utils/sharepoint_test.go b/src/cli/utils/sharepoint_test.go index a622fb4e1..07fe5df09 100644 --- a/src/cli/utils/sharepoint_test.go +++ b/src/cli/utils/sharepoint_test.go @@ -56,7 +56,7 @@ func (suite *SharePointUtilsSuite) TestIncludeSharePointRestoreDataSelectors() { FileName: single, FolderPath: single, ListItem: single, - ListPath: single, + ListFolder: single, SiteID: single, WebURL: single, }, @@ -108,7 +108,7 @@ func (suite *SharePointUtilsSuite) TestIncludeSharePointRestoreDataSelectors() { FileName: empty, FolderPath: empty, ListItem: empty, - ListPath: containsOnly, + ListFolder: containsOnly, SiteID: empty, WebURL: empty, }, @@ -117,14 +117,14 @@ func (suite *SharePointUtilsSuite) TestIncludeSharePointRestoreDataSelectors() { { name: "list prefixes", opts: utils.SharePointOpts{ - ListPath: prefixOnly, + ListFolder: prefixOnly, }, expectIncludeLen: 1, }, { name: "list prefixes and contains", opts: utils.SharePointOpts{ - ListPath: containsAndPrefix, + ListFolder: containsAndPrefix, }, expectIncludeLen: 2, }, diff --git a/src/cli/utils/testdata/flags.go b/src/cli/utils/testdata/flags.go new file mode 100644 index 000000000..89a497d6d --- /dev/null +++ b/src/cli/utils/testdata/flags.go @@ -0,0 +1,46 @@ +package testdata + +import "strings" + +func FlgInpts(in []string) string { return strings.Join(in, ",") } + +var ( + BackupInpt = "backup-id" + + UsersInpt = []string{"users1", "users2"} + SiteIDInpt = []string{"siteID1", "siteID2"} + WebURLInpt = []string{"webURL1", "webURL2"} + + ContactInpt = []string{"contact1", "contact2"} + ContactFldInpt = []string{"contactFld1", "contactFld2"} + ContactNameInpt = "contactName" + + EmailInpt = []string{"mail1", "mail2"} + EmailFldInpt = []string{"mailFld1", "mailFld2"} + EmailReceivedAfterInpt = "mailReceivedAfter" + EmailReceivedBeforeInpt = "mailReceivedBefore" + EmailSenderInpt = "mailSender" + EmailSubjectInpt = "mailSubjet" + + EventInpt = []string{"event1", "event2"} + EventCalInpt = []string{"eventCal1", "eventCal2"} + EventOrganizerInpt = "eventOrganizer" + EventRecursInpt = "eventRecurs" + EventStartsAfterInpt = "eventStartsAfter" + EventStartsBeforeInpt = "eventStartsBefore" + EventSubjectInpt = "eventSubject" + + LibraryInpt = "library" + FileNamesInpt = []string{"fileName1", "fileName2"} + FolderPathsInpt = []string{"folderPath1", "folderPath2"} + FileCreatedAfterInpt = "fileCreatedAfter" + FileCreatedBeforeInpt = "fileCreatedBefore" + FileModifiedAfterInpt = "fileModifiedAfter" + FileModifiedBeforeInpt = "fileModifiedBefore" + + ListFolderInpt = []string{"listFolder1", "listFolder2"} + ListItemInpt = []string{"listItem1", "listItem2"} + + PageFolderInpt = []string{"pageFolder1", "pageFolder2"} + PageInpt = []string{"page1", "page2"} +)