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
357 lines
9.0 KiB
Go
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),
|
|
}
|
|
}
|