diff --git a/src/cli/backup/backup.go b/src/cli/backup/backup.go index c8df39902..56b5c5ef4 100644 --- a/src/cli/backup/backup.go +++ b/src/cli/backup/backup.go @@ -39,6 +39,7 @@ var serviceCommands = []func(cmd *cobra.Command) *cobra.Command{ addExchangeCommands, addOneDriveCommands, addSharePointCommands, + addTeamsCommands, } // AddCommands attaches all `corso backup * *` commands to the parent. diff --git a/src/cli/backup/groups.go b/src/cli/backup/groups.go new file mode 100644 index 000000000..3f1f83eb7 --- /dev/null +++ b/src/cli/backup/groups.go @@ -0,0 +1,230 @@ +package backup + +import ( + "github.com/alcionai/clues" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + "github.com/alcionai/corso/src/cli/flags" + . "github.com/alcionai/corso/src/cli/print" + "github.com/alcionai/corso/src/cli/utils" + "github.com/alcionai/corso/src/pkg/path" +) + +// ------------------------------------------------------------------------------------------------ +// setup and globals +// ------------------------------------------------------------------------------------------------ + +const ( + groupsServiceCommand = "groups" + groupsServiceCommandCreateUseSuffix = "--group | '" + flags.Wildcard + "'" + groupsServiceCommandDeleteUseSuffix = "--backup " + groupsServiceCommandDetailsUseSuffix = "--backup " +) + +// TODO: correct examples +const ( + groupsServiceCommandCreateExamples = `# Backup all Groups data for Alice +corso backup create groups --group alice@example.com + +# Backup only Groups contacts for Alice and Bob +corso backup create groups --group engineering,sales --data contacts + +# Backup all Groups data for all M365 users +corso backup create groups --group '*'` + + groupsServiceCommandDeleteExamples = `# Delete Groups backup with ID 1234abcd-12ab-cd34-56de-1234abcd +corso backup delete groups --backup 1234abcd-12ab-cd34-56de-1234abcd` + + groupsServiceCommandDetailsExamples = `# Explore items in Alice's latest backup (1234abcd...) +corso backup details groups --backup 1234abcd-12ab-cd34-56de-1234abcd + +# Explore calendar events occurring after start of 2022 +corso backup details groups --backup 1234abcd-12ab-cd34-56de-1234abcd \ + --event-starts-after 2022-01-01T00:00:00` +) + +// called by backup.go to map subcommands to provider-specific handling. +func addGroupsCommands(cmd *cobra.Command) *cobra.Command { + var ( + c *cobra.Command + fs *pflag.FlagSet + ) + + switch cmd.Use { + case createCommand: + c, fs = utils.AddCommand(cmd, groupsCreateCmd(), utils.HideCommand()) + fs.SortFlags = false + + c.Use = c.Use + " " + groupsServiceCommandCreateUseSuffix + c.Example = groupsServiceCommandCreateExamples + + // Flags addition ordering should follow the order we want them to appear in help and docs: + flags.AddGroupFlag(c) + flags.AddDataFlag(c, []string{dataLibraries}, false) + flags.AddCorsoPassphaseFlags(c) + flags.AddAWSCredsFlags(c) + flags.AddAzureCredsFlags(c) + flags.AddFetchParallelismFlag(c) + flags.AddFailFastFlag(c) + + case listCommand: + c, fs = utils.AddCommand(cmd, groupsListCmd(), utils.HideCommand()) + fs.SortFlags = false + + flags.AddBackupIDFlag(c, false) + flags.AddCorsoPassphaseFlags(c) + flags.AddAWSCredsFlags(c) + flags.AddAzureCredsFlags(c) + addFailedItemsFN(c) + addSkippedItemsFN(c) + addRecoveredErrorsFN(c) + + case detailsCommand: + c, fs = utils.AddCommand(cmd, groupsDetailsCmd(), utils.HideCommand()) + fs.SortFlags = false + + c.Use = c.Use + " " + groupsServiceCommandDetailsUseSuffix + c.Example = groupsServiceCommandDetailsExamples + + flags.AddSkipReduceFlag(c) + + // 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. + flags.AddBackupIDFlag(c, true) + flags.AddCorsoPassphaseFlags(c) + flags.AddAWSCredsFlags(c) + flags.AddAzureCredsFlags(c) + + case deleteCommand: + c, fs = utils.AddCommand(cmd, groupsDeleteCmd(), utils.HideCommand()) + fs.SortFlags = false + + c.Use = c.Use + " " + groupsServiceCommandDeleteUseSuffix + c.Example = groupsServiceCommandDeleteExamples + + flags.AddBackupIDFlag(c, true) + flags.AddCorsoPassphaseFlags(c) + flags.AddAWSCredsFlags(c) + flags.AddAzureCredsFlags(c) + } + + return c +} + +// ------------------------------------------------------------------------------------------------ +// backup create +// ------------------------------------------------------------------------------------------------ + +// `corso backup create groups [...]` +func groupsCreateCmd() *cobra.Command { + return &cobra.Command{ + Use: groupsServiceCommand, + Short: "Backup M365 Group service data", + RunE: createGroupsCmd, + Args: cobra.NoArgs, + } +} + +// processes a groups service backup. +func createGroupsCmd(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + + if utils.HasNoFlagsAndShownHelp(cmd) { + return nil + } + + return Only(ctx, utils.ErrNotYetImplemented) +} + +// ------------------------------------------------------------------------------------------------ +// backup list +// ------------------------------------------------------------------------------------------------ + +// `corso backup list groups [...]` +func groupsListCmd() *cobra.Command { + return &cobra.Command{ + Use: groupsServiceCommand, + Short: "List the history of M365 Groups service backups", + RunE: listGroupsCmd, + Args: cobra.NoArgs, + } +} + +// lists the history of backup operations +func listGroupsCmd(cmd *cobra.Command, args []string) error { + return genericListCommand(cmd, flags.BackupIDFV, path.GroupsService, args) +} + +// ------------------------------------------------------------------------------------------------ +// backup details +// ------------------------------------------------------------------------------------------------ + +// `corso backup details groups [...]` +func groupsDetailsCmd() *cobra.Command { + return &cobra.Command{ + Use: groupsServiceCommand, + Short: "Shows the details of a M365 Groups service backup", + RunE: detailsGroupsCmd, + Args: cobra.NoArgs, + } +} + +// processes a groups service backup. +func detailsGroupsCmd(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + + if utils.HasNoFlagsAndShownHelp(cmd) { + return nil + } + + if err := validateGroupBackupCreateFlags(flags.GroupFV); err != nil { + return Only(ctx, err) + } + + return Only(ctx, utils.ErrNotYetImplemented) +} + +// ------------------------------------------------------------------------------------------------ +// backup delete +// ------------------------------------------------------------------------------------------------ + +// `corso backup delete groups [...]` +func groupsDeleteCmd() *cobra.Command { + return &cobra.Command{ + Use: groupsServiceCommand, + Short: "Delete backed-up M365 Groups service data", + RunE: deleteGroupsCmd, + Args: cobra.NoArgs, + } +} + +// deletes an groups service backup. +func deleteGroupsCmd(cmd *cobra.Command, args []string) error { + return genericDeleteCommand(cmd, path.GroupsService, flags.BackupIDFV, "Groups", args) +} + +// --------------------------------------------------------------------------- +// helpers +// --------------------------------------------------------------------------- + +func validateGroupBackupCreateFlags(groups []string) error { + if len(groups) == 0 { + return clues.New( + "requires one or more --" + + flags.GroupFN + " ids, or the wildcard --" + + flags.GroupFN + " *", + ) + } + + // TODO(meain) + // for _, d := range cats { + // if d != dataLibraries { + // return clues.New( + // d + " is an unrecognized data type; only " + dataLibraries + " is supported" + // ) + // } + // } + + return nil +} diff --git a/src/cli/backup/groups_test.go b/src/cli/backup/groups_test.go new file mode 100644 index 000000000..04a131b59 --- /dev/null +++ b/src/cli/backup/groups_test.go @@ -0,0 +1,98 @@ +package backup + +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/cli/flags" + "github.com/alcionai/corso/src/internal/tester" +) + +type GroupsUnitSuite struct { + tester.Suite +} + +func TestGroupsUnitSuite(t *testing.T) { + suite.Run(t, &GroupsUnitSuite{Suite: tester.NewUnitSuite(t)}) +} + +func (suite *GroupsUnitSuite) TestAddGroupsCommands() { + expectUse := groupsServiceCommand + + table := []struct { + name string + use string + expectUse string + expectShort string + flags []string + expectRunE func(*cobra.Command, []string) error + }{ + { + "create groups", + createCommand, + expectUse + " " + groupsServiceCommandCreateUseSuffix, + groupsCreateCmd().Short, + []string{ + flags.CategoryDataFN, + flags.FailFastFN, + flags.FetchParallelismFN, + flags.SkipReduceFN, + flags.NoStatsFN, + }, + createGroupsCmd, + }, + { + "list groups", + listCommand, + expectUse, + groupsListCmd().Short, + []string{ + flags.BackupFN, + flags.FailedItemsFN, + flags.SkippedItemsFN, + flags.RecoveredErrorsFN, + }, + listGroupsCmd, + }, + { + "details groups", + detailsCommand, + expectUse + " " + groupsServiceCommandDetailsUseSuffix, + groupsDetailsCmd().Short, + []string{ + flags.BackupFN, + }, + detailsGroupsCmd, + }, + { + "delete groups", + deleteCommand, + expectUse + " " + groupsServiceCommandDeleteUseSuffix, + groupsDeleteCmd().Short, + []string{flags.BackupFN}, + deleteGroupsCmd, + }, + } + for _, test := range table { + suite.Run(test.name, func() { + t := suite.T() + + cmd := &cobra.Command{Use: test.use} + + c := addGroupsCommands(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/cli/backup/teams.go b/src/cli/backup/teams.go new file mode 100644 index 000000000..fcac3394d --- /dev/null +++ b/src/cli/backup/teams.go @@ -0,0 +1,230 @@ +package backup + +import ( + "github.com/alcionai/clues" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + "github.com/alcionai/corso/src/cli/flags" + . "github.com/alcionai/corso/src/cli/print" + "github.com/alcionai/corso/src/cli/utils" + "github.com/alcionai/corso/src/pkg/path" +) + +// ------------------------------------------------------------------------------------------------ +// setup and globals +// ------------------------------------------------------------------------------------------------ + +const ( + teamsServiceCommand = "teams" + teamsServiceCommandCreateUseSuffix = "--team | '" + flags.Wildcard + "'" + teamsServiceCommandDeleteUseSuffix = "--backup " + teamsServiceCommandDetailsUseSuffix = "--backup " +) + +// TODO: correct examples +const ( + teamsServiceCommandCreateExamples = `# Backup all Teams data for Alice +corso backup create teams --team alice@example.com + +# Backup only Teams contacts for Alice and Bob +corso backup create teams --team engineering,sales --data contacts + +# Backup all Teams data for all M365 users +corso backup create teams --team '*'` + + teamsServiceCommandDeleteExamples = `# Delete Teams backup with ID 1234abcd-12ab-cd34-56de-1234abcd +corso backup delete teams --backup 1234abcd-12ab-cd34-56de-1234abcd` + + teamsServiceCommandDetailsExamples = `# Explore items in Alice's latest backup (1234abcd...) +corso backup details teams --backup 1234abcd-12ab-cd34-56de-1234abcd + +# Explore calendar events occurring after start of 2022 +corso backup details teams --backup 1234abcd-12ab-cd34-56de-1234abcd \ + --event-starts-after 2022-01-01T00:00:00` +) + +// called by backup.go to map subcommands to provider-specific handling. +func addTeamsCommands(cmd *cobra.Command) *cobra.Command { + var ( + c *cobra.Command + fs *pflag.FlagSet + ) + + switch cmd.Use { + case createCommand: + c, fs = utils.AddCommand(cmd, teamsCreateCmd(), utils.HideCommand()) + fs.SortFlags = false + + c.Use = c.Use + " " + teamsServiceCommandCreateUseSuffix + c.Example = teamsServiceCommandCreateExamples + + // Flags addition ordering should follow the order we want them to appear in help and docs: + flags.AddTeamFlag(c) + flags.AddDataFlag(c, []string{dataEmail, dataContacts, dataEvents}, false) + flags.AddCorsoPassphaseFlags(c) + flags.AddAWSCredsFlags(c) + flags.AddAzureCredsFlags(c) + flags.AddFetchParallelismFlag(c) + flags.AddFailFastFlag(c) + + case listCommand: + c, fs = utils.AddCommand(cmd, teamsListCmd(), utils.HideCommand()) + fs.SortFlags = false + + flags.AddBackupIDFlag(c, false) + flags.AddCorsoPassphaseFlags(c) + flags.AddAWSCredsFlags(c) + flags.AddAzureCredsFlags(c) + addFailedItemsFN(c) + addSkippedItemsFN(c) + addRecoveredErrorsFN(c) + + case detailsCommand: + c, fs = utils.AddCommand(cmd, teamsDetailsCmd(), utils.HideCommand()) + fs.SortFlags = false + + c.Use = c.Use + " " + teamsServiceCommandDetailsUseSuffix + c.Example = teamsServiceCommandDetailsExamples + + flags.AddSkipReduceFlag(c) + + // 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. + flags.AddBackupIDFlag(c, true) + flags.AddCorsoPassphaseFlags(c) + flags.AddAWSCredsFlags(c) + flags.AddAzureCredsFlags(c) + + case deleteCommand: + c, fs = utils.AddCommand(cmd, teamsDeleteCmd(), utils.HideCommand()) + fs.SortFlags = false + + c.Use = c.Use + " " + teamsServiceCommandDeleteUseSuffix + c.Example = teamsServiceCommandDeleteExamples + + flags.AddBackupIDFlag(c, true) + flags.AddCorsoPassphaseFlags(c) + flags.AddAWSCredsFlags(c) + flags.AddAzureCredsFlags(c) + } + + return c +} + +// ------------------------------------------------------------------------------------------------ +// backup create +// ------------------------------------------------------------------------------------------------ + +// `corso backup create teams [...]` +func teamsCreateCmd() *cobra.Command { + return &cobra.Command{ + Use: teamsServiceCommand, + Short: "Backup M365 Team service data", + RunE: createTeamsCmd, + Args: cobra.NoArgs, + } +} + +// processes a teams service backup. +func createTeamsCmd(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + + if utils.HasNoFlagsAndShownHelp(cmd) { + return nil + } + + if err := validateTeamBackupCreateFlags(flags.TeamFV); err != nil { + return Only(ctx, err) + } + + return Only(ctx, utils.ErrNotYetImplemented) +} + +// ------------------------------------------------------------------------------------------------ +// backup list +// ------------------------------------------------------------------------------------------------ + +// `corso backup list teams [...]` +func teamsListCmd() *cobra.Command { + return &cobra.Command{ + Use: teamsServiceCommand, + Short: "List the history of M365 Teams service backups", + RunE: listTeamsCmd, + Args: cobra.NoArgs, + } +} + +// lists the history of backup operations +func listTeamsCmd(cmd *cobra.Command, args []string) error { + return genericListCommand(cmd, flags.BackupIDFV, path.TeamsService, args) +} + +// ------------------------------------------------------------------------------------------------ +// backup details +// ------------------------------------------------------------------------------------------------ + +// `corso backup details teams [...]` +func teamsDetailsCmd() *cobra.Command { + return &cobra.Command{ + Use: teamsServiceCommand, + Short: "Shows the details of a M365 Teams service backup", + RunE: detailsTeamsCmd, + Args: cobra.NoArgs, + } +} + +// processes a teams service backup. +func detailsTeamsCmd(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + + if utils.HasNoFlagsAndShownHelp(cmd) { + return nil + } + + return Only(ctx, utils.ErrNotYetImplemented) +} + +// ------------------------------------------------------------------------------------------------ +// backup delete +// ------------------------------------------------------------------------------------------------ + +// `corso backup delete teams [...]` +func teamsDeleteCmd() *cobra.Command { + return &cobra.Command{ + Use: teamsServiceCommand, + Short: "Delete backed-up M365 Teams service data", + RunE: deleteTeamsCmd, + Args: cobra.NoArgs, + } +} + +// deletes an teams service backup. +func deleteTeamsCmd(cmd *cobra.Command, args []string) error { + return genericDeleteCommand(cmd, path.TeamsService, flags.BackupIDFV, "Teams", args) +} + +// --------------------------------------------------------------------------- +// helpers +// --------------------------------------------------------------------------- + +func validateTeamBackupCreateFlags(teams []string) error { + if len(teams) == 0 { + return clues.New( + "requires one or more --" + + flags.TeamFN + " ids, or the wildcard --" + + flags.TeamFN + " *", + ) + } + + // TODO(meain) + // for _, d := range cats { + // if d != dataLibraries { + // return clues.New( + // d + " is an unrecognized data type; only " + dataLibraries + " is supported" + // ) + // } + // } + + return nil +} diff --git a/src/cli/backup/teams_test.go b/src/cli/backup/teams_test.go new file mode 100644 index 000000000..966830f82 --- /dev/null +++ b/src/cli/backup/teams_test.go @@ -0,0 +1,98 @@ +package backup + +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/cli/flags" + "github.com/alcionai/corso/src/internal/tester" +) + +type TeamsUnitSuite struct { + tester.Suite +} + +func TestTeamsUnitSuite(t *testing.T) { + suite.Run(t, &TeamsUnitSuite{Suite: tester.NewUnitSuite(t)}) +} + +func (suite *TeamsUnitSuite) TestAddTeamsCommands() { + expectUse := teamsServiceCommand + + table := []struct { + name string + use string + expectUse string + expectShort string + flags []string + expectRunE func(*cobra.Command, []string) error + }{ + { + "create teams", + createCommand, + expectUse + " " + teamsServiceCommandCreateUseSuffix, + teamsCreateCmd().Short, + []string{ + flags.CategoryDataFN, + flags.FailFastFN, + flags.FetchParallelismFN, + flags.SkipReduceFN, + flags.NoStatsFN, + }, + createTeamsCmd, + }, + { + "list teams", + listCommand, + expectUse, + teamsListCmd().Short, + []string{ + flags.BackupFN, + flags.FailedItemsFN, + flags.SkippedItemsFN, + flags.RecoveredErrorsFN, + }, + listTeamsCmd, + }, + { + "details teams", + detailsCommand, + expectUse + " " + teamsServiceCommandDetailsUseSuffix, + teamsDetailsCmd().Short, + []string{ + flags.BackupFN, + }, + detailsTeamsCmd, + }, + { + "delete teams", + deleteCommand, + expectUse + " " + teamsServiceCommandDeleteUseSuffix, + teamsDeleteCmd().Short, + []string{flags.BackupFN}, + deleteTeamsCmd, + }, + } + for _, test := range table { + suite.Run(test.name, func() { + t := suite.T() + + cmd := &cobra.Command{Use: test.use} + + c := addTeamsCommands(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/cli/flags/groups.go b/src/cli/flags/groups.go new file mode 100644 index 000000000..8aa6792ad --- /dev/null +++ b/src/cli/flags/groups.go @@ -0,0 +1,28 @@ +package flags + +import ( + "github.com/spf13/cobra" +) + +const ( + GroupFN = "group" +) + +var GroupFV []string + +func AddGroupDetailsAndRestoreFlags(cmd *cobra.Command) { + // TODO: implement flags +} + +// AddGroupFlag adds the --group flag, which accepts id or name values. +// TODO: need to decide what the appropriate "name" to accept here is. +// keepers thinks its either DisplayName or MailNickname or Mail +// Mail is most accurate, MailNickame is accurate and shorter, but the end user +// may not see either one visibly. +// https://learn.microsoft.com/en-us/graph/api/group-list?view=graph-rest-1.0&tabs=http +func AddGroupFlag(cmd *cobra.Command) { + cmd.Flags().StringSliceVar( + &GroupFV, + GroupFN, nil, + "Backup data by group; accepts '"+Wildcard+"' to select all groups.") +} diff --git a/src/cli/flags/teams.go b/src/cli/flags/teams.go new file mode 100644 index 000000000..a3ca73e62 --- /dev/null +++ b/src/cli/flags/teams.go @@ -0,0 +1,28 @@ +package flags + +import ( + "github.com/spf13/cobra" +) + +const ( + TeamFN = "team" +) + +var TeamFV []string + +func AddTeamDetailsAndRestoreFlags(cmd *cobra.Command) { + // TODO: implement flags +} + +// AddTeamFlag adds the --team flag, which accepts id or name values. +// TODO: need to decide what the appropriate "name" to accept here is. +// keepers thinks its either DisplayName or MailNickname or Mail +// Mail is most accurate, MailNickame is accurate and shorter, but the end user +// may not see either one visibly. +// https://learn.microsoft.com/en-us/graph/api/team-list?view=graph-rest-1.0&tabs=http +func AddTeamFlag(cmd *cobra.Command) { + cmd.Flags().StringSliceVar( + &TeamFV, + TeamFN, nil, + "Backup data by team; accepts '"+Wildcard+"' to select all teams.") +} diff --git a/src/cli/restore/groups.go b/src/cli/restore/groups.go new file mode 100644 index 000000000..a98c9d088 --- /dev/null +++ b/src/cli/restore/groups.go @@ -0,0 +1,81 @@ +package restore + +import ( + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + "github.com/alcionai/corso/src/cli/flags" + . "github.com/alcionai/corso/src/cli/print" + "github.com/alcionai/corso/src/cli/utils" +) + +// called by restore.go to map subcommands to provider-specific handling. +func addGroupsCommands(cmd *cobra.Command) *cobra.Command { + var ( + c *cobra.Command + fs *pflag.FlagSet + ) + + switch cmd.Use { + case restoreCommand: + c, fs = utils.AddCommand(cmd, groupsRestoreCmd(), utils.HideCommand()) + + c.Use = c.Use + " " + groupsServiceCommandUseSuffix + + // 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.SortFlags = false + + flags.AddBackupIDFlag(c, true) + flags.AddRestorePermissionsFlag(c) + flags.AddRestoreConfigFlags(c) + flags.AddFailFastFlag(c) + flags.AddCorsoPassphaseFlags(c) + flags.AddAWSCredsFlags(c) + flags.AddAzureCredsFlags(c) + } + + return c +} + +// TODO: correct examples +const ( + groupsServiceCommand = "groups" + groupsServiceCommandUseSuffix = "--backup " + + groupsServiceCommandRestoreExamples = `# Restore file with ID 98765abcdef in Bob's last backup (1234abcd...) +corso restore groups --backup 1234abcd-12ab-cd34-56de-1234abcd --file 98765abcdef + +# Restore the file with ID 98765abcdef along with its associated permissions +corso restore groups --backup 1234abcd-12ab-cd34-56de-1234abcd --file 98765abcdef --restore-permissions + +# Restore files named "FY2021 Planning.xlsx" in "Documents/Finance Reports" +corso restore groups --backup 1234abcd-12ab-cd34-56de-1234abcd \ + --file "FY2021 Planning.xlsx" --folder "Documents/Finance Reports" + +# Restore all files and folders in folder "Documents/Finance Reports" that were created before 2020 +corso restore groups --backup 1234abcd-12ab-cd34-56de-1234abcd + --folder "Documents/Finance Reports" --file-created-before 2020-01-01T00:00:00` +) + +// `corso restore groups [...]` +func groupsRestoreCmd() *cobra.Command { + return &cobra.Command{ + Use: groupsServiceCommand, + Short: "Restore M365 Groups service data", + RunE: restoreGroupsCmd, + Args: cobra.NoArgs, + Example: groupsServiceCommandRestoreExamples, + } +} + +// processes an groups service restore. +func restoreGroupsCmd(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + + if utils.HasNoFlagsAndShownHelp(cmd) { + return nil + } + + return Only(ctx, utils.ErrNotYetImplemented) +} diff --git a/src/cli/restore/groups_test.go b/src/cli/restore/groups_test.go new file mode 100644 index 000000000..4ea7a7d19 --- /dev/null +++ b/src/cli/restore/groups_test.go @@ -0,0 +1,108 @@ +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/flags" + "github.com/alcionai/corso/src/cli/utils" + "github.com/alcionai/corso/src/cli/utils/testdata" + "github.com/alcionai/corso/src/internal/tester" +) + +type GroupsUnitSuite struct { + tester.Suite +} + +func TestGroupsUnitSuite(t *testing.T) { + suite.Run(t, &GroupsUnitSuite{Suite: tester.NewUnitSuite(t)}) +} + +func (suite *GroupsUnitSuite) TestAddGroupsCommands() { + expectUse := groupsServiceCommand + " " + groupsServiceCommandUseSuffix + + table := []struct { + name string + use string + expectUse string + expectShort string + expectRunE func(*cobra.Command, []string) error + }{ + {"restore groups", restoreCommand, expectUse, groupsRestoreCmd().Short, restoreGroupsCmd}, + } + for _, test := range table { + suite.Run(test.name, func() { + t := suite.T() + + cmd := &cobra.Command{Use: test.use} + + // normally a persistent flag from the root. + // required to ensure a dry run. + flags.AddRunModeFlag(cmd, true) + + c := addGroupsCommands(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) + + cmd.SetArgs([]string{ + "groups", + "--" + flags.RunModeFN, flags.RunModeFlagTest, + "--" + flags.BackupFN, testdata.BackupInput, + + "--" + flags.CollisionsFN, testdata.Collisions, + "--" + flags.DestinationFN, testdata.Destination, + "--" + flags.ToResourceFN, testdata.ToResource, + + "--" + flags.AWSAccessKeyFN, testdata.AWSAccessKeyID, + "--" + flags.AWSSecretAccessKeyFN, testdata.AWSSecretAccessKey, + "--" + flags.AWSSessionTokenFN, testdata.AWSSessionToken, + + "--" + flags.AzureClientIDFN, testdata.AzureClientID, + "--" + flags.AzureClientTenantFN, testdata.AzureTenantID, + "--" + flags.AzureClientSecretFN, testdata.AzureClientSecret, + + "--" + flags.CorsoPassphraseFN, testdata.CorsoPassphrase, + + // bool flags + "--" + flags.RestorePermissionsFN, + }) + + cmd.SetOut(new(bytes.Buffer)) // drop output + cmd.SetErr(new(bytes.Buffer)) // drop output + err := cmd.Execute() + // assert.NoError(t, err, clues.ToCore(err)) + assert.ErrorIs(t, err, utils.ErrNotYetImplemented, clues.ToCore(err)) + + opts := utils.MakeGroupsOpts(cmd) + assert.Equal(t, testdata.BackupInput, flags.BackupIDFV) + + assert.Equal(t, testdata.Collisions, opts.RestoreCfg.Collisions) + assert.Equal(t, testdata.Destination, opts.RestoreCfg.Destination) + assert.Equal(t, testdata.ToResource, opts.RestoreCfg.ProtectedResource) + + assert.Equal(t, testdata.AWSAccessKeyID, flags.AWSAccessKeyFV) + assert.Equal(t, testdata.AWSSecretAccessKey, flags.AWSSecretAccessKeyFV) + assert.Equal(t, testdata.AWSSessionToken, flags.AWSSessionTokenFV) + + assert.Equal(t, testdata.AzureClientID, flags.AzureClientIDFV) + assert.Equal(t, testdata.AzureTenantID, flags.AzureClientTenantFV) + assert.Equal(t, testdata.AzureClientSecret, flags.AzureClientSecretFV) + + assert.Equal(t, testdata.CorsoPassphrase, flags.CorsoPassphraseFV) + assert.True(t, flags.RestorePermissionsFV) + }) + } +} diff --git a/src/cli/restore/teams.go b/src/cli/restore/teams.go new file mode 100644 index 000000000..59623024a --- /dev/null +++ b/src/cli/restore/teams.go @@ -0,0 +1,81 @@ +package restore + +import ( + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + "github.com/alcionai/corso/src/cli/flags" + . "github.com/alcionai/corso/src/cli/print" + "github.com/alcionai/corso/src/cli/utils" +) + +// called by restore.go to map subcommands to provider-specific handling. +func addTeamsCommands(cmd *cobra.Command) *cobra.Command { + var ( + c *cobra.Command + fs *pflag.FlagSet + ) + + switch cmd.Use { + case restoreCommand: + c, fs = utils.AddCommand(cmd, teamsRestoreCmd(), utils.HideCommand()) + + c.Use = c.Use + " " + teamsServiceCommandUseSuffix + + // 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.SortFlags = false + + flags.AddBackupIDFlag(c, true) + flags.AddRestorePermissionsFlag(c) + flags.AddRestoreConfigFlags(c) + flags.AddFailFastFlag(c) + flags.AddCorsoPassphaseFlags(c) + flags.AddAWSCredsFlags(c) + flags.AddAzureCredsFlags(c) + } + + return c +} + +// TODO: correct examples +const ( + teamsServiceCommand = "teams" + teamsServiceCommandUseSuffix = "--backup " + + teamsServiceCommandRestoreExamples = `# Restore file with ID 98765abcdef in Bob's last backup (1234abcd...) +corso restore teams --backup 1234abcd-12ab-cd34-56de-1234abcd --file 98765abcdef + +# Restore the file with ID 98765abcdef along with its associated permissions +corso restore teams --backup 1234abcd-12ab-cd34-56de-1234abcd --file 98765abcdef --restore-permissions + +# Restore files named "FY2021 Planning.xlsx" in "Documents/Finance Reports" +corso restore teams --backup 1234abcd-12ab-cd34-56de-1234abcd \ + --file "FY2021 Planning.xlsx" --folder "Documents/Finance Reports" + +# Restore all files and folders in folder "Documents/Finance Reports" that were created before 2020 +corso restore teams --backup 1234abcd-12ab-cd34-56de-1234abcd + --folder "Documents/Finance Reports" --file-created-before 2020-01-01T00:00:00` +) + +// `corso restore teams [...]` +func teamsRestoreCmd() *cobra.Command { + return &cobra.Command{ + Use: teamsServiceCommand, + Short: "Restore M365 Teams service data", + RunE: restoreTeamsCmd, + Args: cobra.NoArgs, + Example: teamsServiceCommandRestoreExamples, + } +} + +// processes an teams service restore. +func restoreTeamsCmd(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + + if utils.HasNoFlagsAndShownHelp(cmd) { + return nil + } + + return Only(ctx, utils.ErrNotYetImplemented) +} diff --git a/src/cli/restore/teams_test.go b/src/cli/restore/teams_test.go new file mode 100644 index 000000000..ac502e950 --- /dev/null +++ b/src/cli/restore/teams_test.go @@ -0,0 +1,108 @@ +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/flags" + "github.com/alcionai/corso/src/cli/utils" + "github.com/alcionai/corso/src/cli/utils/testdata" + "github.com/alcionai/corso/src/internal/tester" +) + +type TeamsUnitSuite struct { + tester.Suite +} + +func TestTeamsUnitSuite(t *testing.T) { + suite.Run(t, &TeamsUnitSuite{Suite: tester.NewUnitSuite(t)}) +} + +func (suite *TeamsUnitSuite) TestAddTeamsCommands() { + expectUse := teamsServiceCommand + " " + teamsServiceCommandUseSuffix + + table := []struct { + name string + use string + expectUse string + expectShort string + expectRunE func(*cobra.Command, []string) error + }{ + {"restore teams", restoreCommand, expectUse, teamsRestoreCmd().Short, restoreTeamsCmd}, + } + for _, test := range table { + suite.Run(test.name, func() { + t := suite.T() + + cmd := &cobra.Command{Use: test.use} + + // normally a persistent flag from the root. + // required to ensure a dry run. + flags.AddRunModeFlag(cmd, true) + + c := addTeamsCommands(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) + + cmd.SetArgs([]string{ + "teams", + "--" + flags.RunModeFN, flags.RunModeFlagTest, + "--" + flags.BackupFN, testdata.BackupInput, + + "--" + flags.CollisionsFN, testdata.Collisions, + "--" + flags.DestinationFN, testdata.Destination, + "--" + flags.ToResourceFN, testdata.ToResource, + + "--" + flags.AWSAccessKeyFN, testdata.AWSAccessKeyID, + "--" + flags.AWSSecretAccessKeyFN, testdata.AWSSecretAccessKey, + "--" + flags.AWSSessionTokenFN, testdata.AWSSessionToken, + + "--" + flags.AzureClientIDFN, testdata.AzureClientID, + "--" + flags.AzureClientTenantFN, testdata.AzureTenantID, + "--" + flags.AzureClientSecretFN, testdata.AzureClientSecret, + + "--" + flags.CorsoPassphraseFN, testdata.CorsoPassphrase, + + // bool flags + "--" + flags.RestorePermissionsFN, + }) + + cmd.SetOut(new(bytes.Buffer)) // drop output + cmd.SetErr(new(bytes.Buffer)) // drop output + err := cmd.Execute() + // assert.NoError(t, err, clues.ToCore(err)) + assert.ErrorIs(t, err, utils.ErrNotYetImplemented, clues.ToCore(err)) + + opts := utils.MakeTeamsOpts(cmd) + assert.Equal(t, testdata.BackupInput, flags.BackupIDFV) + + assert.Equal(t, testdata.Collisions, opts.RestoreCfg.Collisions) + assert.Equal(t, testdata.Destination, opts.RestoreCfg.Destination) + assert.Equal(t, testdata.ToResource, opts.RestoreCfg.ProtectedResource) + + assert.Equal(t, testdata.AWSAccessKeyID, flags.AWSAccessKeyFV) + assert.Equal(t, testdata.AWSSecretAccessKey, flags.AWSSecretAccessKeyFV) + assert.Equal(t, testdata.AWSSessionToken, flags.AWSSessionTokenFV) + + assert.Equal(t, testdata.AzureClientID, flags.AzureClientIDFV) + assert.Equal(t, testdata.AzureTenantID, flags.AzureClientTenantFV) + assert.Equal(t, testdata.AzureClientSecret, flags.AzureClientSecretFV) + + assert.Equal(t, testdata.CorsoPassphrase, flags.CorsoPassphraseFV) + assert.True(t, flags.RestorePermissionsFV) + }) + } +} diff --git a/src/cli/utils/groups.go b/src/cli/utils/groups.go new file mode 100644 index 000000000..9b0827d46 --- /dev/null +++ b/src/cli/utils/groups.go @@ -0,0 +1,30 @@ +package utils + +import ( + "github.com/spf13/cobra" + + "github.com/alcionai/corso/src/cli/flags" +) + +type GroupsOpts struct { + Groups []string + + RestoreCfg RestoreCfgOpts + ExportCfg ExportCfgOpts + + Populated flags.PopulatedFlags +} + +func MakeGroupsOpts(cmd *cobra.Command) GroupsOpts { + return GroupsOpts{ + Groups: flags.UserFV, + + RestoreCfg: makeRestoreCfgOpts(cmd), + ExportCfg: makeExportCfgOpts(cmd), + + // populated contains the list of flags that appear in the + // command, according to pflags. Use this to differentiate + // between an "empty" and a "missing" value. + Populated: flags.GetPopulatedFlags(cmd), + } +} diff --git a/src/cli/utils/teams.go b/src/cli/utils/teams.go new file mode 100644 index 000000000..365e7971e --- /dev/null +++ b/src/cli/utils/teams.go @@ -0,0 +1,30 @@ +package utils + +import ( + "github.com/spf13/cobra" + + "github.com/alcionai/corso/src/cli/flags" +) + +type TeamsOpts struct { + Teams []string + + RestoreCfg RestoreCfgOpts + ExportCfg ExportCfgOpts + + Populated flags.PopulatedFlags +} + +func MakeTeamsOpts(cmd *cobra.Command) TeamsOpts { + return TeamsOpts{ + Teams: flags.UserFV, + + RestoreCfg: makeRestoreCfgOpts(cmd), + ExportCfg: makeExportCfgOpts(cmd), + + // populated contains the list of flags that appear in the + // command, according to pflags. Use this to differentiate + // between an "empty" and a "missing" value. + Populated: flags.GetPopulatedFlags(cmd), + } +} diff --git a/src/cli/utils/utils.go b/src/cli/utils/utils.go index a542d55f3..5a639474a 100644 --- a/src/cli/utils/utils.go +++ b/src/cli/utils/utils.go @@ -19,6 +19,8 @@ import ( "github.com/alcionai/corso/src/pkg/storage" ) +var ErrNotYetImplemented = clues.New("not yet implemented") + func GetAccountAndConnect( ctx context.Context, pst path.ServiceType, diff --git a/src/pkg/path/service_type.go b/src/pkg/path/service_type.go index 0028bca4b..343117857 100644 --- a/src/pkg/path/service_type.go +++ b/src/pkg/path/service_type.go @@ -31,6 +31,8 @@ const ( SharePointMetadataService // sharepointMetadata GroupsService // groups GroupsMetadataService // groupsMetadata + TeamsService // teams + TeamsMetadataService // teamsMetadata ) func toServiceType(service string) ServiceType {