corso/src/pkg/backup/backup.go
Abin Simon 68256cf59f
Change printing of backup stats to single line (#4526)
Previously we were printing a table for a single item which, for one
was unnecessary, but this also make the output of a backup more
compact.

Before:

```
Logging to file: /home/meain/.cache/corso/logs/2023-10-20T10-21-23Z.log
Connecting to M365                               done
Connecting to repository                      1s done

Backing Up ∙ Teams Testing (meain)
Discovering items to backup                  27s done
    Libraries (TeamsTestingmeain)                  done (found 0 items)
    Libraries (TeamsTestingmeain-Shared0)          done (found 0 items)
    Libraries (TeamsTestingmeain-Private2)         done (found 0 items)
    Messages                                    7s done (found 6 channels)
Backing up data                               7s done
Backup complete ∙ 7b40dd40-f808-4d57-8e39-b4553e48dc5d
    ID                                    Bytes Uploaded  Items Uploaded  Items Skipped  Errors
    7b40dd40-f808-4d57-8e39-b4553e48dc5d  0 B             0               0              0

Completed Backups:
    ID                                    Started At            Duration       Status     Resource Owner
    7b40dd40-f808-4d57-8e39-b4553e48dc5d  2023-10-20T10:21:32Z  36.632747912s  Completed  Teams Testing (meain)
```

After:
```
Connecting to M365                               done
Connecting to repository                      1s done

Backing Up ∙ Teams Testing (meain)
Discovering items to backup                  31s done
    Libraries (TeamsTestingmeain)                  done (found 0 items)
    Libraries (TeamsTestingmeain-Shared0)          done (found 0 items)
    Libraries (TeamsTestingmeain-Private2)         done (found 0 items)
    Messages                                    9s done (found 6 channels)
Backing up data                               7s done
Backup complete ∙ ffb2f619-1cb7-4a11-b3e2-7300aa513c6a
Bytes Uploaded: 0 B | Items Uploaded: 0 | Items Skipped: 0 | Errors: 0

Completed Backups:
    ID                                    ID                    Started At     Duration   Status                 Resource Owner
    ffb2f619-1cb7-4a11-b3e2-7300aa513c6a  2023-10-20T10:23:35Z  40.096203016s  Completed  Teams Testing (meain)
```


---

#### Does this PR need a docs update or release note?

- [x]  Yes, it's included
- [ ] 🕐 Yes, but in a later PR
- [ ]  No

#### Type of change

<!--- Please check the type of change your PR introduces: --->
- [x] 🌻 Feature
- [ ] 🐛 Bugfix
- [ ] 🗺️ Documentation
- [ ] 🤖 Supportability/Tests
- [ ] 💻 CI/Deployment
- [ ] 🧹 Tech Debt/Cleanup

#### Issue(s)

<!-- Can reference multiple issues. Use one of the following "magic words" - "closes, fixes" to auto-close the Github issue. -->
* #<issue>

#### Test Plan

<!-- How will this be tested prior to merging.-->
- [ ] 💪 Manual
- [x]  Unit test
- [ ] 💚 E2E
2023-11-16 12:43:27 +00:00

389 lines
9.8 KiB
Go

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/dttm"
"github.com/alcionai/corso/src/internal/common/str"
"github.com/alcionai/corso/src/internal/model"
"github.com/alcionai/corso/src/internal/stats"
"github.com/alcionai/corso/src/internal/version"
"github.com/alcionai/corso/src/pkg/fault"
"github.com/alcionai/corso/src/pkg/selectors"
)
// Backup represents the result of a backup operation
type Backup struct {
model.BaseModel
CreationTime time.Time `json:"creationTime"`
// SnapshotID is the kopia snapshot ID
SnapshotID string `json:"snapshotID"`
// Reference to the details and fault errors storage location.
// Used to read backup.Details and fault.Errors from the streamstore.
StreamStoreID string `json:"streamStoreID"`
// Status of the operation, eg: completed, failed, etc
Status string `json:"status"`
// Selector used in this operation
Selector selectors.Selector `json:"selectors"`
// TODO: in process of gaining support, most cases will still use
// ResourceOwner and ResourceOwnerName.
ProtectedResourceID string `json:"protectedResourceID,omitempty"`
ProtectedResourceName string `json:"protectedResourceName,omitempty"`
// Version represents the version of the backup format
Version int `json:"version"`
FailFast bool `json:"failFast"`
// the quantity of errors, both hard failure and recoverable.
ErrorCount int `json:"errorCount"`
// the non-recoverable failure message, only populated if one occurred.
Failure string `json:"failure"`
// stats are embedded so that the values appear as top-level properties
stats.ReadWrites
stats.StartAndEndTime
stats.SkippedCounts
// **Deprecated**
// Reference to the backup details storage location.
// Used to read backup.Details from the streamstore.
DetailsID string `json:"detailsID"`
// prefer protectedResource
ResourceOwnerID string `json:"resourceOwnerID,omitempty"`
ResourceOwnerName string `json:"resourceOwnerName,omitempty"`
}
// interface compliance checks
var _ print.Printable = &Backup{}
func New(
snapshotID, streamStoreID, status string,
backupVersion int,
id model.StableID,
selector selectors.Selector,
ownerID, ownerName string,
rw stats.ReadWrites,
se stats.StartAndEndTime,
fe *fault.Errors,
tags map[string]string,
) *Backup {
if fe == nil {
fe = &fault.Errors{}
}
var (
errCount = len(fe.Items)
skipCount = len(fe.Skipped)
failMsg string
malware, invalidONFile, otherSkips int
)
if fe.Failure != nil {
failMsg = fe.Failure.Msg
errCount++
}
for _, s := range fe.Skipped {
switch true {
case s.HasCause(fault.SkipMalware):
malware++
case s.HasCause(fault.SkipOneNote):
invalidONFile++
default:
otherSkips++
}
}
return &Backup{
BaseModel: model.BaseModel{
ID: id,
Tags: tags,
},
ResourceOwnerID: ownerID,
ResourceOwnerName: ownerName,
Version: backupVersion,
SnapshotID: snapshotID,
StreamStoreID: streamStoreID,
CreationTime: time.Now(),
Status: status,
Selector: selector,
FailFast: fe.FailFast,
ErrorCount: errCount,
Failure: failMsg,
ReadWrites: rw,
StartAndEndTime: se,
SkippedCounts: stats.SkippedCounts{
TotalSkippedItems: skipCount,
SkippedMalware: malware,
SkippedInvalidOneNoteFile: invalidONFile,
},
}
}
// Type returns the type of the backup according to the value stored in the
// Backup's tags. Backup type is used during base finding to determine if a
// given backup is eligible to be used for the upcoming incremental backup.
func (b Backup) Type() string {
t, ok := b.Tags[model.BackupTypeTag]
// Older backups didn't set the backup type tag because we only persisted
// backup models for the MergeBackup type. Corso started adding the backup
// type tag when it was producing v8 backups. Any backup newer than that that
// doesn't have a backup type should just return an empty type and let the
// caller figure out what to do.
if !ok &&
b.Version != version.NoBackup &&
b.Version <= version.All8MigrateUserPNToID {
t = model.MergeBackup
}
return t
}
// --------------------------------------------------------------------------------
// 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)
}
// PrintProperties writes the Backup to StdOut, in the format requested by the caller.
// Unlike Print, it skips the ID of the Backup
func (b Backup) PrintProperties(ctx context.Context) {
print.ItemProperties(ctx, b)
}
// PrintAll writes the slice of Backups to StdOut, in the format requested by the caller.
func PrintAll(ctx context.Context, bs []*Backup) {
if len(bs) == 0 {
print.Info(ctx, "No backups available")
return
}
ps := []print.Printable{}
for _, b := range bs {
ps = append(ps, print.Printable(b))
}
print.All(ctx, ps...)
}
type Printable struct {
ID model.StableID `json:"id"`
Status string `json:"status"`
Version string `json:"version"`
ProtectedResourceID string `json:"protectedResourceID,omitempty"`
ProtectedResourceName string `json:"protectedResourceName,omitempty"`
Owner string `json:"owner,omitempty"`
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",
ProtectedResourceID: b.Selector.DiscreteOwner,
ProtectedResourceName: b.Selector.DiscreteOwnerName,
Owner: b.Selector.DiscreteOwner,
Stats: b.toStats(),
}
}
// MinimumPrintable reduces the Backup to its minimally printable details.
func (b Backup) MinimumPrintable() any {
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(skipID bool) []string {
headers := []string{
"Started At",
"Duration",
"Status",
"Resource Owner",
}
if skipID {
return headers
}
return append([]string{"ID"}, headers...)
}
// Values returns the values matching the Headers list for printing
// out to a terminal in a columnar display.
func (b Backup) Values(skipID bool) []string {
var (
status = b.Status
errCount = b.ErrorCount
)
if errCount+b.TotalSkippedItems > 0 {
status += (" (")
}
if errCount > 0 {
status += fmt.Sprintf("%d errors", errCount)
}
if errCount > 0 && b.TotalSkippedItems > 0 {
status += ", "
}
if b.TotalSkippedItems > 0 {
status += fmt.Sprintf("%d skipped", b.TotalSkippedItems)
if b.SkippedMalware+b.SkippedInvalidOneNoteFile > 0 {
status += ": "
}
}
skipped := []string{}
if b.SkippedMalware > 0 {
skipped = append(skipped, fmt.Sprintf("%d malware", b.SkippedMalware))
}
if b.SkippedInvalidOneNoteFile > 0 {
skipped = append(skipped, fmt.Sprintf("%d invalid OneNote file", b.SkippedInvalidOneNoteFile))
}
status += strings.Join(skipped, ", ")
if errCount+b.TotalSkippedItems > 0 {
status += (")")
}
name := str.First(
b.ProtectedResourceName,
b.ResourceOwnerName,
b.ProtectedResourceID,
b.ResourceOwnerID,
b.Selector.Name())
bs := b.toStats()
values := []string{
dttm.FormatToTabularDisplay(b.StartedAt),
bs.EndedAt.Sub(bs.StartedAt).String(),
status,
name,
}
if skipID {
return values
}
return append([]string{string(b.ID)}, values...)
}
// ----- print backup stats
func (b Backup) toStats() backupStats {
return backupStats{
ID: string(b.ID),
BytesRead: b.BytesRead,
BytesUploaded: b.NonMetaBytesUploaded,
EndedAt: b.CompletedAt,
ErrorCount: b.ErrorCount,
ItemsRead: b.ItemsRead,
ItemsSkipped: b.TotalSkippedItems,
ItemsWritten: b.NonMetaItemsWritten,
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)
}
// PrintProperties writes the Backup to StdOut, in the format requested by the caller.
// Unlike Print, it skips the ID of backupStats
func (bs backupStats) PrintProperties(ctx context.Context) {
print.ItemProperties(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(skipID bool) []string {
headers := []string{
"Bytes Uploaded",
"Items Uploaded",
"Items Skipped",
"Errors",
}
if skipID {
return headers
}
return append([]string{"ID"}, headers...)
}
// Values returns the values matching the Headers list for printing
// out to a terminal in a columnar display.
func (bs backupStats) Values(skipID bool) []string {
values := []string{
humanize.Bytes(uint64(bs.BytesUploaded)),
strconv.Itoa(bs.ItemsWritten),
strconv.Itoa(bs.ItemsSkipped),
strconv.Itoa(bs.ErrorCount),
}
if skipID {
return values
}
return append([]string{bs.ID}, values...)
}