diff --git a/src/cli/backup/backup.go b/src/cli/backup/backup.go index 425b176e5..ed7c3a441 100644 --- a/src/cli/backup/backup.go +++ b/src/cli/backup/backup.go @@ -11,16 +11,16 @@ var subCommandFuncs = []func() *cobra.Command{ deleteCmd, } -var serviceCommands = []func(parent *cobra.Command) *cobra.Command{ +var serviceCommands = []func(cmd *cobra.Command) *cobra.Command{ addExchangeCommands, addOneDriveCommands, addSharePointCommands, } // AddCommands attaches all `corso backup * *` commands to the parent. -func AddCommands(parent *cobra.Command) { +func AddCommands(cmd *cobra.Command) { backupC := backupCmd() - parent.AddCommand(backupC) + cmd.AddCommand(backupC) for _, sc := range subCommandFuncs { subCommand := sc() diff --git a/src/cli/backup/exchange.go b/src/cli/backup/exchange.go index 1271a55ce..beeecca86 100644 --- a/src/cli/backup/exchange.go +++ b/src/cli/backup/exchange.go @@ -96,16 +96,16 @@ corso backup details exchange --backup 1234abcd-12ab-cd34-56de-1234abcd \ --user alice@example.com --contact-name Andy` ) -// called by backup.go to map parent subcommands to provider-specific handling. -func addExchangeCommands(parent *cobra.Command) *cobra.Command { +// called by backup.go to map subcommands to provider-specific handling. +func addExchangeCommands(cmd *cobra.Command) *cobra.Command { var ( c *cobra.Command fs *pflag.FlagSet ) - switch parent.Use { + switch cmd.Use { case createCommand: - c, fs = utils.AddCommand(parent, exchangeCreateCmd()) + c, fs = utils.AddCommand(cmd, exchangeCreateCmd()) c.Use = c.Use + " " + exchangeServiceCommandCreateUseSuffix c.Example = exchangeServiceCommandCreateExamples @@ -123,14 +123,14 @@ func addExchangeCommands(parent *cobra.Command) *cobra.Command { options.AddOperationFlags(c) case listCommand: - c, fs = utils.AddCommand(parent, exchangeListCmd()) + c, fs = utils.AddCommand(cmd, exchangeListCmd()) fs.StringVar(&backupID, "backup", "", "ID of the backup to retrieve.") case detailsCommand: - c, fs = utils.AddCommand(parent, exchangeDetailsCmd()) + c, fs = utils.AddCommand(cmd, exchangeDetailsCmd()) c.Use = c.Use + " " + exchangeServiceCommandDetailsUseSuffix c.Example = exchangeServiceCommandDetailsExamples @@ -218,7 +218,7 @@ func addExchangeCommands(parent *cobra.Command) *cobra.Command { "Select backup details for contacts whose contact name contains this value.") case deleteCommand: - c, fs = utils.AddCommand(parent, exchangeDeleteCmd()) + c, fs = utils.AddCommand(cmd, exchangeDeleteCmd()) c.Use = c.Use + " " + exchangeServiceCommandDeleteUseSuffix c.Example = exchangeServiceCommandDeleteExamples diff --git a/src/cli/backup/onedrive.go b/src/cli/backup/onedrive.go index 7ad598c58..4ee03a159 100644 --- a/src/cli/backup/onedrive.go +++ b/src/cli/backup/onedrive.go @@ -69,16 +69,16 @@ var ( fileModifiedBefore string ) -// called by backup.go to map parent subcommands to provider-specific handling. -func addOneDriveCommands(parent *cobra.Command) *cobra.Command { +// called by backup.go to map subcommands to provider-specific handling. +func addOneDriveCommands(cmd *cobra.Command) *cobra.Command { var ( c *cobra.Command fs *pflag.FlagSet ) - switch parent.Use { + switch cmd.Use { case createCommand: - c, fs = utils.AddCommand(parent, oneDriveCreateCmd()) + c, fs = utils.AddCommand(cmd, oneDriveCreateCmd()) c.Use = c.Use + " " + oneDriveServiceCommandCreateUseSuffix c.Example = oneDriveServiceCommandCreateExamples @@ -89,14 +89,14 @@ func addOneDriveCommands(parent *cobra.Command) *cobra.Command { options.AddOperationFlags(c) case listCommand: - c, fs = utils.AddCommand(parent, oneDriveListCmd()) + c, fs = utils.AddCommand(cmd, oneDriveListCmd()) fs.StringVar(&backupID, utils.BackupFN, "", "ID of the backup to retrieve.") case detailsCommand: - c, fs = utils.AddCommand(parent, oneDriveDetailsCmd()) + c, fs = utils.AddCommand(cmd, oneDriveDetailsCmd()) c.Use = c.Use + " " + oneDriveServiceCommandDetailsUseSuffix c.Example = oneDriveServiceCommandDetailsExamples @@ -139,7 +139,7 @@ func addOneDriveCommands(parent *cobra.Command) *cobra.Command { "Select backup details for files modified before this datetime.") case deleteCommand: - c, fs = utils.AddCommand(parent, oneDriveDeleteCmd()) + c, fs = utils.AddCommand(cmd, oneDriveDeleteCmd()) c.Use = c.Use + " " + oneDriveServiceCommandDeleteUseSuffix c.Example = oneDriveServiceCommandDeleteExamples diff --git a/src/cli/backup/sharepoint.go b/src/cli/backup/sharepoint.go index d6ce2b292..f20f0bcab 100644 --- a/src/cli/backup/sharepoint.go +++ b/src/cli/backup/sharepoint.go @@ -66,16 +66,16 @@ corso backup delete sharepoint --backup 1234abcd-12ab-cd34-56de-1234abcd` corso backup details sharepoint --backup 1234abcd-12ab-cd34-56de-1234abcd --site ` ) -// called by backup.go to map parent subcommands to provider-specific handling. -func addSharePointCommands(parent *cobra.Command) *cobra.Command { +// called by backup.go to map subcommands to provider-specific handling. +func addSharePointCommands(cmd *cobra.Command) *cobra.Command { var ( c *cobra.Command fs *pflag.FlagSet ) - switch parent.Use { + switch cmd.Use { case createCommand: - c, fs = utils.AddCommand(parent, sharePointCreateCmd(), utils.HideCommand()) + c, fs = utils.AddCommand(cmd, sharePointCreateCmd(), utils.HideCommand()) c.Use = c.Use + " " + sharePointServiceCommandCreateUseSuffix c.Example = sharePointServiceCommandCreateExamples @@ -91,14 +91,14 @@ func addSharePointCommands(parent *cobra.Command) *cobra.Command { options.AddOperationFlags(c) case listCommand: - c, fs = utils.AddCommand(parent, sharePointListCmd(), utils.HideCommand()) + c, fs = utils.AddCommand(cmd, sharePointListCmd(), utils.HideCommand()) fs.StringVar(&backupID, utils.BackupFN, "", "ID of the backup to retrieve.") case detailsCommand: - c, fs = utils.AddCommand(parent, sharePointDetailsCmd()) + c, fs = utils.AddCommand(cmd, sharePointDetailsCmd()) c.Use = c.Use + " " + sharePointServiceCommandDetailsUseSuffix c.Example = sharePointServiceCommandDetailsExamples @@ -128,7 +128,7 @@ func addSharePointCommands(parent *cobra.Command) *cobra.Command { // "Select backup details for items created after this datetime.") case deleteCommand: - c, fs = utils.AddCommand(parent, sharePointDeleteCmd(), utils.HideCommand()) + c, fs = utils.AddCommand(cmd, sharePointDeleteCmd(), utils.HideCommand()) c.Use = c.Use + " " + sharePointServiceCommandDeleteUseSuffix c.Example = sharePointServiceCommandDeleteExamples diff --git a/src/cli/help/env.go b/src/cli/help/env.go index 40e92ba0a..24e1b7a08 100644 --- a/src/cli/help/env.go +++ b/src/cli/help/env.go @@ -7,8 +7,8 @@ import ( ) // AddCommands attaches all `corso env * *` commands to the parent. -func AddCommands(parent *cobra.Command) { - parent.AddCommand(envCmd()) +func AddCommands(cmd *cobra.Command) { + cmd.AddCommand(envCmd()) } // The env command: purely a help display. diff --git a/src/cli/options/options.go b/src/cli/options/options.go index be67faf45..66b061267 100644 --- a/src/cli/options/options.go +++ b/src/cli/options/options.go @@ -12,16 +12,16 @@ var ( ) // AddOperationFlags adds command-local operation flags -func AddOperationFlags(parent *cobra.Command) { - fs := parent.Flags() +func AddOperationFlags(cmd *cobra.Command) { + fs := cmd.Flags() fs.BoolVar(&fastFail, "fast-fail", false, "stop processing immediately if any error occurs") // TODO: reveal this flag when fail-fast support is implemented cobra.CheckErr(fs.MarkHidden("fast-fail")) } // AddGlobalOperationFlags adds the global operations flag set. -func AddGlobalOperationFlags(parent *cobra.Command) { - fs := parent.PersistentFlags() +func AddGlobalOperationFlags(cmd *cobra.Command) { + fs := cmd.PersistentFlags() fs.BoolVar(&noStats, "no-stats", false, "disable anonymous usage statistics gathering") } diff --git a/src/cli/print/print.go b/src/cli/print/print.go index f417c8def..f8d95c570 100644 --- a/src/cli/print/print.go +++ b/src/cli/print/print.go @@ -43,8 +43,8 @@ func getRootCmd(ctx context.Context) *cobra.Command { } // adds the persistent flag --output to the provided command. -func AddOutputFlag(parent *cobra.Command) { - fs := parent.PersistentFlags() +func AddOutputFlag(cmd *cobra.Command) { + fs := cmd.PersistentFlags() fs.BoolVar(&outputAsJSON, "json", false, "output data in JSON format") fs.BoolVar(&outputAsJSONDebug, "json-debug", false, "output all internal and debugging data in JSON format") cobra.CheckErr(fs.MarkHidden("json-debug")) diff --git a/src/cli/repo/repo.go b/src/cli/repo/repo.go index 4a5695c7b..d60f9265a 100644 --- a/src/cli/repo/repo.go +++ b/src/cli/repo/repo.go @@ -9,12 +9,12 @@ const ( connectCommand = "connect" ) -var repoCommands = []func(parent *cobra.Command) *cobra.Command{ +var repoCommands = []func(cmd *cobra.Command) *cobra.Command{ addS3Commands, } // AddCommands attaches all `corso repo * *` commands to the parent. -func AddCommands(parent *cobra.Command) { +func AddCommands(cmd *cobra.Command) { var ( // Get new instances so that setting the context during tests works // properly. @@ -23,7 +23,7 @@ func AddCommands(parent *cobra.Command) { connectCmd = connectCmd() ) - parent.AddCommand(repoCmd) + cmd.AddCommand(repoCmd) repoCmd.AddCommand(initCmd) repoCmd.AddCommand(connectCmd) diff --git a/src/cli/repo/s3.go b/src/cli/repo/s3.go index 01b12bf53..2dbf4cb9f 100644 --- a/src/cli/repo/s3.go +++ b/src/cli/repo/s3.go @@ -26,22 +26,22 @@ var ( succeedIfExists bool ) -// called by repo.go to map parent subcommands to provider-specific handling. -func addS3Commands(parent *cobra.Command) *cobra.Command { +// called by repo.go to map subcommands to provider-specific handling. +func addS3Commands(cmd *cobra.Command) *cobra.Command { var ( c *cobra.Command fs *pflag.FlagSet ) - switch parent.Use { + switch cmd.Use { case initCommand: - c, fs = utils.AddCommand(parent, s3InitCmd()) + c, fs = utils.AddCommand(cmd, s3InitCmd()) case connectCommand: - c, fs = utils.AddCommand(parent, s3ConnectCmd()) + c, fs = utils.AddCommand(cmd, s3ConnectCmd()) } c.Use = c.Use + " " + s3ProviderCommandUseSuffix - c.SetUsageTemplate(parent.UsageTemplate()) + c.SetUsageTemplate(cmd.UsageTemplate()) // Flags addition ordering should follow the order we want them to appear in help and docs: // More generic and more frequently used flags take precedence. diff --git a/src/cli/restore/exchange.go b/src/cli/restore/exchange.go index 715234871..84d24945e 100644 --- a/src/cli/restore/exchange.go +++ b/src/cli/restore/exchange.go @@ -40,16 +40,16 @@ var ( eventSubject string ) -// called by restore.go to map parent subcommands to provider-specific handling. -func addExchangeCommands(parent *cobra.Command) *cobra.Command { +// called by restore.go to map subcommands to provider-specific handling. +func addExchangeCommands(cmd *cobra.Command) *cobra.Command { var ( c *cobra.Command fs *pflag.FlagSet ) - switch parent.Use { + switch cmd.Use { case restoreCommand: - c, fs = utils.AddCommand(parent, exchangeRestoreCmd()) + c, fs = utils.AddCommand(cmd, exchangeRestoreCmd()) c.Use = c.Use + " " + exchangeServiceCommandUseSuffix diff --git a/src/cli/restore/onedrive.go b/src/cli/restore/onedrive.go index e4621d965..a31bffccf 100644 --- a/src/cli/restore/onedrive.go +++ b/src/cli/restore/onedrive.go @@ -25,16 +25,16 @@ var ( fileModifiedBefore string ) -// called by restore.go to map parent subcommands to provider-specific handling. -func addOneDriveCommands(parent *cobra.Command) *cobra.Command { +// called by restore.go to map subcommands to provider-specific handling. +func addOneDriveCommands(cmd *cobra.Command) *cobra.Command { var ( c *cobra.Command fs *pflag.FlagSet ) - switch parent.Use { + switch cmd.Use { case restoreCommand: - c, fs = utils.AddCommand(parent, oneDriveRestoreCmd()) + c, fs = utils.AddCommand(cmd, oneDriveRestoreCmd()) c.Use = c.Use + " " + oneDriveServiceCommandUseSuffix diff --git a/src/cli/restore/restore.go b/src/cli/restore/restore.go index 541c9d37a..e6873f860 100644 --- a/src/cli/restore/restore.go +++ b/src/cli/restore/restore.go @@ -4,15 +4,16 @@ import ( "github.com/spf13/cobra" ) -var restoreCommands = []func(parent *cobra.Command) *cobra.Command{ +var restoreCommands = []func(cmd *cobra.Command) *cobra.Command{ addExchangeCommands, addOneDriveCommands, + addSharePointCommands, } // AddCommands attaches all `corso restore * *` commands to the parent. -func AddCommands(parent *cobra.Command) { +func AddCommands(cmd *cobra.Command) { restoreC := restoreCmd() - parent.AddCommand(restoreC) + cmd.AddCommand(restoreC) for _, addRestoreTo := range restoreCommands { addRestoreTo(restoreC) diff --git a/src/cli/restore/sharepoint.go b/src/cli/restore/sharepoint.go new file mode 100644 index 000000000..8cad8e272 --- /dev/null +++ b/src/cli/restore/sharepoint.go @@ -0,0 +1,161 @@ +package restore + +import ( + "github.com/pkg/errors" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + "github.com/alcionai/corso/src/cli/config" + "github.com/alcionai/corso/src/cli/options" + . "github.com/alcionai/corso/src/cli/print" + "github.com/alcionai/corso/src/cli/utils" + "github.com/alcionai/corso/src/internal/common" + "github.com/alcionai/corso/src/pkg/control" + "github.com/alcionai/corso/src/pkg/repository" + "github.com/alcionai/corso/src/pkg/selectors" +) + +var ( + site []string + libraryPaths []string + libraryItems []string +) + +// called by restore.go to map subcommands to provider-specific handling. +func addSharePointCommands(cmd *cobra.Command) *cobra.Command { + var ( + c *cobra.Command + fs *pflag.FlagSet + ) + + switch cmd.Use { + case restoreCommand: + c, fs = utils.AddCommand(cmd, sharePointRestoreCmd(), utils.HideCommand()) + + c.Use = c.Use + " " + sharePointServiceCommandUseSuffix + + // Flags addition ordering should follow the order we want them to appear in help and docs: + // More generic (ex: --site) and more frequently used flags take precedence. + fs.SortFlags = false + + fs.StringVar(&backupID, + utils.BackupFN, "", + "ID of the backup to restore. (required)") + cobra.CheckErr(c.MarkFlagRequired(utils.BackupFN)) + + fs.StringSliceVar(&site, + utils.SiteFN, nil, + "Restore data by site ID; accepts '"+utils.Wildcard+"' to select all sites.") + + // sharepoint hierarchy (path/name) flags + + fs.StringSliceVar( + &folderPaths, + utils.LibraryFN, nil, + "Restore library items by SharePoint library") + + fs.StringSliceVar( + &libraryItems, + utils.LibraryItemFN, nil, + "Restore library items by file name or ID") + + // sharepoint info flags + + // fs.StringVar( + // &fileCreatedAfter, + // utils.FileCreatedAfterFN, "", + // "Restore files created after this datetime") + + // others + options.AddOperationFlags(c) + } + + return c +} + +const ( + sharePointServiceCommand = "sharepoint" + sharePointServiceCommandUseSuffix = "--backup " + + //nolint:lll + sharePointServiceCommandRestoreExamples = `# Restore file with ID 98765abcdef +corso restore sharepoint --backup 1234abcd-12ab-cd34-56de-1234abcd --file 98765abcdef + +# Restore 's file named "ServerRenderTemplate.xsl in "Display Templates/Style Sheets" from a specific backup +corso restore sharepoint --backup 1234abcd-12ab-cd34-56de-1234abcd \ + --site --file "ServerRenderTemplate.xsl" --folder "Display Templates/Style Sheets" + +# Restore all files from that were created before 2020 when captured in a specific backup +corso restore sharepoint --backup 1234abcd-12ab-cd34-56de-1234abcd + --site --folder "Display Templates/Style Sheets" --file-created-before 2020-01-01T00:00:00` +) + +// `corso restore sharepoint [...]` +func sharePointRestoreCmd() *cobra.Command { + return &cobra.Command{ + Use: sharePointServiceCommand, + Short: "Restore M365 SharePoint service data", + RunE: restoreSharePointCmd, + Args: cobra.NoArgs, + Example: sharePointServiceCommandRestoreExamples, + } +} + +// processes an sharepoint service restore. +func restoreSharePointCmd(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + + if utils.HasNoFlagsAndShownHelp(cmd) { + return nil + } + + opts := utils.SharePointOpts{ + Sites: site, + LibraryPaths: libraryPaths, + LibraryItems: libraryItems, + // FileCreatedAfter: fileCreatedAfter, + + Populated: utils.GetPopulatedFlags(cmd), + } + + if err := utils.ValidateSharePointRestoreFlags(backupID, opts); err != nil { + return err + } + + s, a, err := config.GetStorageAndAccount(ctx, true, nil) + if err != nil { + return Only(ctx, err) + } + + r, err := repository.Connect(ctx, a, s, options.Control()) + if err != nil { + return Only(ctx, errors.Wrapf(err, "Failed to connect to the %s repository", s.Provider)) + } + + defer utils.CloseRepo(ctx, r) + + sel := selectors.NewSharePointRestore() + utils.IncludeSharePointRestoreDataSelectors(sel, opts) + utils.FilterSharePointRestoreInfoSelectors(sel, opts) + + // if no selector flags were specified, get all data in the service. + if len(sel.Scopes()) == 0 { + sel.Include(sel.Sites(selectors.Any())) + } + + restoreDest := control.DefaultRestoreDestination(common.SimpleDateTimeOneDrive) + + ro, err := r.NewRestore(ctx, backupID, sel.Selector, restoreDest) + if err != nil { + return Only(ctx, errors.Wrap(err, "Failed to initialize SharePoint restore")) + } + + ds, err := ro.Run(ctx) + if err != nil { + return Only(ctx, errors.Wrap(err, "Failed to run SharePoint restore")) + } + + ds.PrintEntries(ctx) + + return nil +} diff --git a/src/cli/restore/sharepoint_test.go b/src/cli/restore/sharepoint_test.go new file mode 100644 index 000000000..a0e298a83 --- /dev/null +++ b/src/cli/restore/sharepoint_test.go @@ -0,0 +1,50 @@ +package restore + +import ( + "testing" + + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + + "github.com/alcionai/corso/src/internal/tester" +) + +type SharePointSuite struct { + suite.Suite +} + +func TestSharePointSuite(t *testing.T) { + suite.Run(t, new(SharePointSuite)) +} + +func (suite *SharePointSuite) TestAddSharePointCommands() { + expectUse := sharePointServiceCommand + " " + sharePointServiceCommandUseSuffix + + table := []struct { + name string + use string + expectUse string + expectShort string + expectRunE func(*cobra.Command, []string) error + }{ + {"restore onedrive", restoreCommand, expectUse, sharePointRestoreCmd().Short, restoreSharePointCmd}, + } + for _, test := range table { + suite.T().Run(test.name, func(t *testing.T) { + cmd := &cobra.Command{Use: test.use} + + c := addSharePointCommands(cmd) + require.NotNil(t, c) + + cmds := cmd.Commands() + require.Len(t, cmds, 1) + + child := cmds[0] + assert.Equal(t, test.expectUse, child.Use) + assert.Equal(t, test.expectShort, child.Short) + tester.AreSameFunc(t, test.expectRunE, child.RunE) + }) + } +} diff --git a/src/cmd/factory/exchange.go b/src/cmd/factory/exchange.go index 04691221d..bf175a622 100644 --- a/src/cmd/factory/exchange.go +++ b/src/cmd/factory/exchange.go @@ -30,10 +30,10 @@ var ( } ) -func addExchangeCommands(parent *cobra.Command) { - parent.AddCommand(emailsCmd) - parent.AddCommand(eventsCmd) - parent.AddCommand(contactsCmd) +func addExchangeCommands(cmd *cobra.Command) { + cmd.AddCommand(emailsCmd) + cmd.AddCommand(eventsCmd) + cmd.AddCommand(contactsCmd) } func handleExchangeEmailFactory(cmd *cobra.Command, args []string) error { diff --git a/src/cmd/factory/onedrive.go b/src/cmd/factory/onedrive.go index c4bc5e01f..a76d222b9 100644 --- a/src/cmd/factory/onedrive.go +++ b/src/cmd/factory/onedrive.go @@ -13,8 +13,8 @@ var filesCmd = &cobra.Command{ RunE: handleOneDriveFileFactory, } -func addOneDriveCommands(parent *cobra.Command) { - parent.AddCommand(filesCmd) +func addOneDriveCommands(cmd *cobra.Command) { + cmd.AddCommand(filesCmd) } func handleOneDriveFileFactory(cmd *cobra.Command, args []string) error { diff --git a/src/internal/observe/observe.go b/src/internal/observe/observe.go index 0d66f1825..1e27794d2 100644 --- a/src/internal/observe/observe.go +++ b/src/internal/observe/observe.go @@ -38,8 +38,8 @@ func init() { // adds the persistent boolean flag --hide-progress to the provided command. // This is a hack for help displays. Due to seeding the context, we also // need to parse the configuration before we execute the command. -func AddProgressBarFlags(parent *cobra.Command) { - fs := parent.PersistentFlags() +func AddProgressBarFlags(cmd *cobra.Command) { + fs := cmd.PersistentFlags() fs.Bool(hideProgressBarsFN, false, "turn off the progress bar displays") fs.Bool(retainProgressBarsFN, false, "retain the progress bar displays after completion") } diff --git a/src/pkg/logger/logger.go b/src/pkg/logger/logger.go index 9f73175ff..13982463a 100644 --- a/src/pkg/logger/logger.go +++ b/src/pkg/logger/logger.go @@ -39,8 +39,8 @@ const ( // defaults to "info". // This is a hack for help displays. Due to seeding the context, we also // need to parse the log level before we execute the command. -func AddLogLevelFlag(parent *cobra.Command) { - fs := parent.PersistentFlags() +func AddLogLevelFlag(cmd *cobra.Command) { + fs := cmd.PersistentFlags() fs.StringVar(&llFlag, logLevelFN, "info", "set the log level to debug|info|warn|error") fs.Bool(