diff --git a/src/cli/backup/backup.go b/src/cli/backup/backup.go index c3233e231..673266272 100644 --- a/src/cli/backup/backup.go +++ b/src/cli/backup/backup.go @@ -230,7 +230,13 @@ func runBackups( } bIDs = append(bIDs, string(bo.Results.BackupID)) - Infof(ctx, "Done - ID: %v\n", bo.Results.BackupID) + + if !DisplayJSONFormat() { + Infof(ctx, "Done\n") + printBackupStats(ctx, r, string(bo.Results.BackupID)) + } else { + Infof(ctx, "Done - ID: %v\n", bo.Results.BackupID) + } } bups, berrs := r.Backups(ctx, bIDs) @@ -335,3 +341,13 @@ func getAccountAndConnect(ctx context.Context) (repository.Repository, *account. func ifShow(flag string) bool { return strings.ToLower(strings.TrimSpace(flag)) == "show" } + +func printBackupStats(ctx context.Context, r repository.Repository, bid string) { + b, err := r.Backup(ctx, bid) + if err != nil { + logger.CtxErr(ctx, err).Error("finding backup immediately after backup operation completion") + } + + b.ToPrintable().Stats.Print(ctx) + Info(ctx, " ") +} diff --git a/src/cli/print/print.go b/src/cli/print/print.go index 5ab61acca..91ef1e581 100644 --- a/src/cli/print/print.go +++ b/src/cli/print/print.go @@ -50,8 +50,8 @@ func AddOutputFlag(cmd *cobra.Command) { cobra.CheckErr(fs.MarkHidden("json-debug")) } -// JSONFormat returns true if the printer plans to output as json. -func JSONFormat() bool { +// DisplayJSONFormat returns true if the printer plans to output as json. +func DisplayJSONFormat() bool { return outputAsJSON || outputAsJSONDebug } diff --git a/src/pkg/backup/backup.go b/src/pkg/backup/backup.go index d9b52c9d3..8d792c1a7 100644 --- a/src/pkg/backup/backup.go +++ b/src/pkg/backup/backup.go @@ -3,9 +3,12 @@ package backup import ( "context" "fmt" + "strconv" "strings" "time" + "github.com/dustin/go-humanize" + "github.com/alcionai/corso/src/cli/print" "github.com/alcionai/corso/src/internal/common" "github.com/alcionai/corso/src/internal/model" @@ -141,6 +144,8 @@ func New( // CLI Output // -------------------------------------------------------------------------------- +// ----- print backups + // Print writes the Backup to StdOut, in the format requested by the caller. func (b Backup) Print(ctx context.Context) { print.Item(ctx, b) @@ -162,36 +167,36 @@ func PrintAll(ctx context.Context, bs []*Backup) { } type Printable struct { - ID model.StableID `json:"id"` - ErrorCount int `json:"errorCount"` - StartedAt time.Time `json:"started at"` - Status string `json:"status"` - Version string `json:"version"` - BytesRead int64 `json:"bytesRead"` - BytesUploaded int64 `json:"bytesUploaded"` - Owner string `json:"owner"` + ID model.StableID `json:"id"` + Status string `json:"status"` + Version string `json:"version"` + Owner string `json:"owner"` + Stats backupStats `json:"stats"` +} + +// ToPrintable reduces the Backup to its minimally printable details. +func (b Backup) ToPrintable() Printable { + return Printable{ + ID: b.ID, + Status: b.Status, + Version: "0", + Owner: b.Selector.DiscreteOwner, + Stats: b.toStats(), + } } // MinimumPrintable reduces the Backup to its minimally printable details. func (b Backup) MinimumPrintable() any { - return Printable{ - ID: b.ID, - ErrorCount: b.ErrorCount, - StartedAt: b.StartedAt, - Status: b.Status, - Version: "0", - BytesRead: b.BytesRead, - BytesUploaded: b.BytesUploaded, - Owner: b.Selector.DiscreteOwner, - } + return b.ToPrintable() } // Headers returns the human-readable names of properties in a Backup // for printing out to a terminal in a columnar display. func (b Backup) Headers() []string { return []string{ - "Started At", "ID", + "Started At", + "Duration", "Status", "Resource Owner", } @@ -255,10 +260,78 @@ func (b Backup) Values() []string { name = b.Selector.DiscreteOwner } + bs := b.toStats() + return []string{ - common.FormatTabularDisplayTime(b.StartedAt), string(b.ID), + common.FormatTabularDisplayTime(b.StartedAt), + bs.EndedAt.Sub(bs.StartedAt).String(), status, name, } } + +// ----- print backup stats + +func (b Backup) toStats() backupStats { + return backupStats{ + ID: string(b.ID), + BytesRead: b.BytesRead, + BytesUploaded: b.BytesUploaded, + EndedAt: b.CompletedAt, + ErrorCount: b.ErrorCount, + ItemsRead: b.ItemsRead, + ItemsSkipped: b.TotalSkippedItems, + ItemsWritten: b.ItemsWritten, + StartedAt: b.StartedAt, + } +} + +// interface compliance checks +var _ print.Printable = &backupStats{} + +type backupStats struct { + ID string `json:"id"` + BytesRead int64 `json:"bytesRead"` + BytesUploaded int64 `json:"bytesUploaded"` + EndedAt time.Time `json:"endedAt"` + ErrorCount int `json:"errorCount"` + ItemsRead int `json:"itemsRead"` + ItemsSkipped int `json:"itemsSkipped"` + ItemsWritten int `json:"itemsWritten"` + StartedAt time.Time `json:"startedAt"` +} + +// Print writes the Backup to StdOut, in the format requested by the caller. +func (bs backupStats) Print(ctx context.Context) { + print.Item(ctx, bs) +} + +// MinimumPrintable reduces the Backup to its minimally printable details. +func (bs backupStats) MinimumPrintable() any { + return bs +} + +// Headers returns the human-readable names of properties in a Backup +// for printing out to a terminal in a columnar display. +func (bs backupStats) Headers() []string { + return []string{ + "ID", + "Bytes Uploaded", + "Items Uploaded", + "Items Skipped", + "Errors", + } +} + +// Values returns the values matching the Headers list for printing +// out to a terminal in a columnar display. +func (bs backupStats) Values() []string { + return []string{ + bs.ID, + humanize.Bytes(uint64(bs.BytesUploaded)), + strconv.Itoa(bs.ItemsWritten), + strconv.Itoa(bs.ItemsSkipped), + strconv.Itoa(bs.ErrorCount), + } +} diff --git a/src/pkg/backup/backup_test.go b/src/pkg/backup/backup_test.go index 91bde1a17..67892ac98 100644 --- a/src/pkg/backup/backup_test.go +++ b/src/pkg/backup/backup_test.go @@ -1,9 +1,11 @@ package backup_test import ( + "strconv" "testing" "time" + "github.com/dustin/go-humanize" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" @@ -50,7 +52,7 @@ func stubBackup(t time.Time, ownerID, ownerName string) backup.Backup { }, StartAndEndTime: stats.StartAndEndTime{ StartedAt: t, - CompletedAt: t, + CompletedAt: t.Add(1 * time.Minute), }, SkippedCounts: stats.SkippedCounts{ TotalSkippedItems: 1, @@ -63,22 +65,27 @@ func (suite *BackupUnitSuite) TestBackup_HeadersValues() { var ( t = suite.T() now = time.Now() + later = now.Add(1 * time.Minute) b = stubBackup(now, "id", "name") expectHs = []string{ - "Started At", "ID", + "Started At", + "Duration", "Status", "Resource Owner", } nowFmt = common.FormatTabularDisplayTime(now) expectVs = []string{ - nowFmt, "id", + nowFmt, + "1m0s", "status (2 errors, 1 skipped: 1 malware)", "test", } ) + b.StartAndEndTime.CompletedAt = later + // single skipped malware hs := b.Headers() assert.Equal(t, expectHs, hs) @@ -182,7 +189,7 @@ func (suite *BackupUnitSuite) TestBackup_Values_statusVariations() { for _, test := range table { suite.Run(test.name, func() { result := test.bup.Values() - assert.Equal(suite.T(), test.expect, result[2], "status value") + assert.Equal(suite.T(), test.expect, result[3], "status value") }) } } @@ -197,10 +204,57 @@ func (suite *BackupUnitSuite) TestBackup_MinimumPrintable() { require.True(t, ok) assert.Equal(t, b.ID, result.ID, "id") - assert.Equal(t, 2, result.ErrorCount, "error count") - assert.Equal(t, now, result.StartedAt, "started at") + assert.Equal(t, 2, result.Stats.ErrorCount, "error count") + assert.Equal(t, now, result.Stats.StartedAt, "started at") assert.Equal(t, b.Status, result.Status, "status") - assert.Equal(t, b.BytesRead, result.BytesRead, "size") - assert.Equal(t, b.BytesUploaded, result.BytesUploaded, "stored size") + assert.Equal(t, b.BytesRead, result.Stats.BytesRead, "size") + assert.Equal(t, b.BytesUploaded, result.Stats.BytesUploaded, "stored size") assert.Equal(t, b.Selector.DiscreteOwner, result.Owner, "owner") } + +func (suite *BackupUnitSuite) TestStats() { + var ( + t = suite.T() + start = time.Now() + b = stubBackup(start, "owner", "ownername") + s = b.ToPrintable().Stats + ) + + assert.Equal(t, b.BytesRead, s.BytesRead, "bytes read") + assert.Equal(t, b.BytesUploaded, s.BytesUploaded, "bytes uploaded") + assert.Equal(t, b.CompletedAt, s.EndedAt, "completion time") + assert.Equal(t, b.ErrorCount, s.ErrorCount, "error count") + assert.Equal(t, b.ItemsRead, s.ItemsRead, "items read") + assert.Equal(t, b.TotalSkippedItems, s.ItemsSkipped, "items skipped") + assert.Equal(t, b.ItemsWritten, s.ItemsWritten, "items written") + assert.Equal(t, b.StartedAt, s.StartedAt, "started at") +} + +func (suite *BackupUnitSuite) TestStats_headersValues() { + var ( + t = suite.T() + start = time.Now() + b = stubBackup(start, "owner", "ownername") + s = b.ToPrintable().Stats + ) + + expectHeaders := []string{ + "ID", + "Bytes Uploaded", + "Items Uploaded", + "Items Skipped", + "Errors", + } + + assert.Equal(t, expectHeaders, s.Headers()) + + expectValues := []string{ + "id", + humanize.Bytes(uint64(b.BytesUploaded)), + strconv.Itoa(b.ItemsWritten), + strconv.Itoa(b.TotalSkippedItems), + strconv.Itoa(b.ErrorCount), + } + + assert.Equal(t, expectValues, s.Values()) +} diff --git a/src/pkg/backup/details/details.go b/src/pkg/backup/details/details.go index 32074c9c6..c0835dddf 100644 --- a/src/pkg/backup/details/details.go +++ b/src/pkg/backup/details/details.go @@ -139,7 +139,7 @@ type DetailsModel struct { // Print writes the DetailModel Entries to StdOut, in the format // requested by the caller. func (dm DetailsModel) PrintEntries(ctx context.Context) { - if print.JSONFormat() { + if print.DisplayJSONFormat() { printJSON(ctx, dm) } else { printTable(ctx, dm)