diff --git a/src/cli/backup/exchange.go b/src/cli/backup/exchange.go index 1d84d2991..dc4af7f48 100644 --- a/src/cli/backup/exchange.go +++ b/src/cli/backup/exchange.go @@ -119,7 +119,11 @@ func addExchangeCommands(parent *cobra.Command) *cobra.Command { options.AddOperationFlags(c) case listCommand: - c, _ = utils.AddCommand(parent, exchangeListCmd()) + c, fs = utils.AddCommand(parent, exchangeListCmd()) + + fs.StringVar(&backupID, + "backup", "", + "ID of the backup to retrieve.") case detailsCommand: c, fs = utils.AddCommand(parent, exchangeDetailsCmd()) @@ -350,6 +354,21 @@ func listExchangeCmd(cmd *cobra.Command, args []string) error { defer utils.CloseRepo(ctx, r) + if len(backupID) > 0 { + b, err := r.Backup(ctx, model.StableID(backupID)) + if err != nil { + if errors.Is(err, kopia.ErrNotFound) { + return Only(ctx, errors.Errorf("No backup exists with the id %s", backupID)) + } + + return Only(ctx, errors.Wrap(err, "Failed to find backup "+backupID)) + } + + b.Print(ctx) + + return nil + } + bs, err := r.Backups(ctx) if err != nil { return Only(ctx, errors.Wrap(err, "Failed to list backups in the repository")) @@ -443,7 +462,7 @@ func runDetailsExchangeCmd( d, _, err := r.BackupDetails(ctx, backupID) if err != nil { if errors.Is(err, kopia.ErrNotFound) { - return nil, errors.Errorf("no backup exists with the id %s", backupID) + return nil, errors.Errorf("No backup exists with the id %s", backupID) } return nil, errors.Wrap(err, "Failed to get backup details in the repository") diff --git a/src/cli/backup/exchange_integration_test.go b/src/cli/backup/exchange_integration_test.go index 2a101ca15..4593d2175 100644 --- a/src/cli/backup/exchange_integration_test.go +++ b/src/cli/backup/exchange_integration_test.go @@ -140,7 +140,8 @@ type PreparedBackupExchangeIntegrationSuite struct { cfgFP string repo repository.Repository m365UserID string - backupOps map[path.CategoryType]operations.BackupOperation + backupOps map[path.CategoryType]string + recorder strings.Builder } func TestPreparedBackupExchangeIntegrationSuite(t *testing.T) { @@ -165,6 +166,7 @@ func (suite *PreparedBackupExchangeIntegrationSuite) SetupSuite() { // prepare common details suite.acct = tester.NewM365Account(t) suite.st = tester.NewPrefixedS3Storage(t) + suite.recorder = strings.Builder{} cfg, err := suite.st.S3Config() require.NoError(t, err) @@ -188,7 +190,7 @@ func (suite *PreparedBackupExchangeIntegrationSuite) SetupSuite() { suite.repo, err = repository.Initialize(ctx, suite.acct, suite.st, control.Options{}) require.NoError(t, err) - suite.backupOps = make(map[path.CategoryType]operations.BackupOperation) + suite.backupOps = make(map[path.CategoryType]string) for _, set := range backupDataSets { var ( @@ -213,21 +215,23 @@ func (suite *PreparedBackupExchangeIntegrationSuite) SetupSuite() { require.NoError(t, bop.Run(ctx)) require.NoError(t, err) - suite.backupOps[set] = bop + bIDs := string(bop.Results.BackupID) // sanity check, ensure we can find the backup and its details immediately - _, err = suite.repo.Backup(ctx, bop.Results.BackupID) + b, err := suite.repo.Backup(ctx, bop.Results.BackupID) require.NoError(t, err, "retrieving recent backup by ID") - _, _, err = suite.repo.BackupDetails(ctx, string(bop.Results.BackupID)) + require.Equal(t, bIDs, string(b.ID), "repo backup matches results id") + _, b, err = suite.repo.BackupDetails(ctx, bIDs) require.NoError(t, err, "retrieving recent backup details by ID") + require.Equal(t, bIDs, string(b.ID), "repo details matches results id") + + suite.backupOps[set] = string(b.ID) } } func (suite *PreparedBackupExchangeIntegrationSuite) TestExchangeListCmd() { - recorder := strings.Builder{} - for _, set := range backupDataSets { - recorder.Reset() + suite.recorder.Reset() suite.T().Run(set.String(), func(t *testing.T) { ctx, flush := tester.NewContext() @@ -239,7 +243,7 @@ func (suite *PreparedBackupExchangeIntegrationSuite) TestExchangeListCmd() { "--config-file", suite.cfgFP) cli.BuildCommandTree(cmd) - cmd.SetOut(&recorder) + cmd.SetOut(&suite.recorder) ctx = print.SetRootCmd(ctx, cmd) @@ -247,24 +251,74 @@ func (suite *PreparedBackupExchangeIntegrationSuite) TestExchangeListCmd() { require.NoError(t, cmd.ExecuteContext(ctx)) // compare the output - result := recorder.String() - assert.Contains(t, result, suite.backupOps[set].Results.BackupID) + result := suite.recorder.String() + assert.Contains(t, result, suite.backupOps[set]) }) } } -func (suite *PreparedBackupExchangeIntegrationSuite) TestExchangeDetailsCmd() { - recorder := strings.Builder{} - +func (suite *PreparedBackupExchangeIntegrationSuite) TestExchangeListCmd_singleID() { for _, set := range backupDataSets { - recorder.Reset() + suite.recorder.Reset() suite.T().Run(set.String(), func(t *testing.T) { ctx, flush := tester.NewContext() ctx = config.SetViper(ctx, suite.vpr) defer flush() - bID := suite.backupOps[set].Results.BackupID + bID := suite.backupOps[set] + + cmd := tester.StubRootCmd( + "backup", "list", "exchange", + "--config-file", suite.cfgFP, + "--backup", string(bID)) + cli.BuildCommandTree(cmd) + + cmd.SetOut(&suite.recorder) + + ctx = print.SetRootCmd(ctx, cmd) + + // run the command + require.NoError(t, cmd.ExecuteContext(ctx)) + + // compare the output + result := suite.recorder.String() + assert.Contains(t, result, bID) + }) + } +} + +func (suite *PreparedBackupExchangeIntegrationSuite) TestExchangeListCmd_badID() { + for _, set := range backupDataSets { + suite.T().Run(set.String(), func(t *testing.T) { + ctx, flush := tester.NewContext() + ctx = config.SetViper(ctx, suite.vpr) + defer flush() + + cmd := tester.StubRootCmd( + "backup", "list", "exchange", + "--config-file", suite.cfgFP, + "--backup", "smarfs") + cli.BuildCommandTree(cmd) + + ctx = print.SetRootCmd(ctx, cmd) + + // run the command + require.Error(t, cmd.ExecuteContext(ctx)) + }) + } +} + +func (suite *PreparedBackupExchangeIntegrationSuite) TestExchangeDetailsCmd() { + for _, set := range backupDataSets { + suite.recorder.Reset() + + suite.T().Run(set.String(), func(t *testing.T) { + ctx, flush := tester.NewContext() + ctx = config.SetViper(ctx, suite.vpr) + defer flush() + + bID := suite.backupOps[set] // fetch the details from the repo first deets, _, err := suite.repo.BackupDetails(ctx, string(bID)) @@ -276,7 +330,7 @@ func (suite *PreparedBackupExchangeIntegrationSuite) TestExchangeDetailsCmd() { "--backup", string(bID)) cli.BuildCommandTree(cmd) - cmd.SetOut(&recorder) + cmd.SetOut(&suite.recorder) ctx = print.SetRootCmd(ctx, cmd) @@ -284,7 +338,7 @@ func (suite *PreparedBackupExchangeIntegrationSuite) TestExchangeDetailsCmd() { require.NoError(t, cmd.ExecuteContext(ctx)) // compare the output - result := recorder.String() + result := suite.recorder.String() i := 0 foundFolders := 0 diff --git a/src/cli/backup/onedrive.go b/src/cli/backup/onedrive.go index b55284a02..08a4252a8 100644 --- a/src/cli/backup/onedrive.go +++ b/src/cli/backup/onedrive.go @@ -84,7 +84,11 @@ func addOneDriveCommands(parent *cobra.Command) *cobra.Command { options.AddOperationFlags(c) case listCommand: - c, _ = utils.AddCommand(parent, oneDriveListCmd()) + c, fs = utils.AddCommand(parent, oneDriveListCmd()) + + fs.StringVar(&backupID, + "backup", "", + "ID of the backup to retrieve.") case detailsCommand: c, fs = utils.AddCommand(parent, oneDriveDetailsCmd()) @@ -246,6 +250,21 @@ func listOneDriveCmd(cmd *cobra.Command, args []string) error { defer utils.CloseRepo(ctx, r) + if len(backupID) > 0 { + b, err := r.Backup(ctx, model.StableID(backupID)) + if err != nil { + if errors.Is(err, kopia.ErrNotFound) { + return Only(ctx, errors.Errorf("No backup exists with the id %s", backupID)) + } + + return Only(ctx, errors.Wrap(err, "Failed to find backup "+backupID)) + } + + b.Print(ctx) + + return nil + } + bs, err := r.Backups(ctx) if err != nil { return Only(ctx, errors.Wrap(err, "Failed to list backups in the repository"))