corso/src/pkg/backup/backup.go
ashmrtn 66eb789d39
Helper function to get backup type (#4591)
Allow the backup model to tell us what type it was tagged with. This
will make logic for base selection easier since it won't have to worry
about extracting the type anymore.

---

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

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

#### Type of change

- [x] 🌻 Feature
- [ ] 🐛 Bugfix
- [ ] 🗺️ Documentation
- [ ] 🤖 Supportability/Tests
- [ ] 💻 CI/Deployment
- [ ] 🧹 Tech Debt/Cleanup

#### Test Plan

- [ ] 💪 Manual
- [x]  Unit test
- [ ] 💚 E2E
2023-11-01 01:06:50 +00:00

357 lines
9.0 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)
}
// 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() []string {
return []string{
"ID",
"Started At",
"Duration",
"Status",
"Resource Owner",
}
}
// Values returns the values matching the Headers list for printing
// out to a terminal in a columnar display.
func (b Backup) Values() []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()
return []string{
string(b.ID),
dttm.FormatToTabularDisplay(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.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)
}
// 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),
}
}