Make the cli colorful and informational (#4525)

This commit include a few things that improves the progresbars and other info messages are printed out during the CLI run.

![2023-10-20-17-17-09](https://github.com/alcionai/corso/assets/14259816/30a25832-24cb-48ef-944d-6aaced1cd62f)

---

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

- [ ]  Yes, it's included
- [x] 🕐 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.-->
- [x] 💪 Manual
- [ ]  Unit test
- [ ] 💚 E2E
This commit is contained in:
Abin Simon 2023-11-16 17:41:37 +05:30 committed by GitHub
parent f2102e55f6
commit b46f242bc4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 290 additions and 74 deletions

View File

@ -2,6 +2,7 @@ package backup
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"strings" "strings"
@ -12,8 +13,10 @@ import (
"github.com/alcionai/corso/src/cli/flags" "github.com/alcionai/corso/src/cli/flags"
. "github.com/alcionai/corso/src/cli/print" . "github.com/alcionai/corso/src/cli/print"
"github.com/alcionai/corso/src/cli/utils" "github.com/alcionai/corso/src/cli/utils"
"github.com/alcionai/corso/src/internal/common/color"
"github.com/alcionai/corso/src/internal/common/idname" "github.com/alcionai/corso/src/internal/common/idname"
"github.com/alcionai/corso/src/internal/data" "github.com/alcionai/corso/src/internal/data"
"github.com/alcionai/corso/src/internal/observe"
"github.com/alcionai/corso/src/pkg/backup" "github.com/alcionai/corso/src/pkg/backup"
"github.com/alcionai/corso/src/pkg/backup/details" "github.com/alcionai/corso/src/pkg/backup/details"
"github.com/alcionai/corso/src/pkg/control" "github.com/alcionai/corso/src/pkg/control"
@ -187,8 +190,15 @@ func genericCreateCommand(
bo, err := r.NewBackupWithLookup(ictx, discSel, ins) bo, err := r.NewBackupWithLookup(ictx, discSel, ins)
if err != nil { if err != nil {
errs = append(errs, clues.WrapWC(ictx, err, owner)) cerr := clues.WrapWC(ictx, err, owner)
Errf(ictx, "%v\n", err) errs = append(errs, cerr)
meta, err := json.Marshal(cerr.Core().Values)
if err != nil {
meta = []byte("Unable to marshal error metadata")
}
Errf(ictx, "%s\nMessage: %v\nMetadata:%s", "Unable to complete backup", err, meta)
continue continue
} }
@ -208,8 +218,15 @@ func genericCreateCommand(
continue continue
} }
errs = append(errs, clues.WrapWC(ictx, err, owner)) cerr := clues.WrapWC(ictx, err, owner)
Errf(ictx, "%v\n", err) errs = append(errs, cerr)
meta, err := json.Marshal(cerr.Core().Values)
if err != nil {
meta = []byte("Unable to marshal error metadata")
}
Errf(ictx, "%s\nMessage: %v\nMetadata:%s", "Unable to complete backup", err, meta)
continue continue
} }
@ -217,10 +234,10 @@ func genericCreateCommand(
bIDs = append(bIDs, string(bo.Results.BackupID)) bIDs = append(bIDs, string(bo.Results.BackupID))
if !DisplayJSONFormat() { if !DisplayJSONFormat() {
Infof(ctx, "Done\n") Infof(ctx, fmt.Sprintf("Backup complete %s %s", observe.Bullet, color.BlueOutput(bo.Results.BackupID)))
printBackupStats(ctx, r, string(bo.Results.BackupID)) printBackupStats(ctx, r, string(bo.Results.BackupID))
} else { } else {
Infof(ctx, "Done - ID: %v\n", bo.Results.BackupID) Infof(ctx, "Backup complete - ID: %v\n", bo.Results.BackupID)
} }
} }
@ -231,10 +248,9 @@ func genericCreateCommand(
if len(bups) > 0 { if len(bups) > 0 {
Info(ctx, "Completed Backups:") Info(ctx, "Completed Backups:")
backup.PrintAll(ctx, bups)
} }
backup.PrintAll(ctx, bups)
if len(errs) > 0 { if len(errs) > 0 {
sb := fmt.Sprintf("%d of %d backups failed:\n", len(errs), len(selectorSet)) sb := fmt.Sprintf("%d of %d backups failed:\n", len(errs), len(selectorSet))

View File

@ -73,7 +73,7 @@ func preRun(cc *cobra.Command, args []string) error {
func handleMailBoxFlag(ctx context.Context, c *cobra.Command, flagNames []string) { func handleMailBoxFlag(ctx context.Context, c *cobra.Command, flagNames []string) {
if !slices.Contains(flagNames, "user") && !slices.Contains(flagNames, "mailbox") { if !slices.Contains(flagNames, "user") && !slices.Contains(flagNames, "mailbox") {
print.Errf(ctx, "either --user or --mailbox flag is required") print.Err(ctx, "either --user or --mailbox flag is required")
os.Exit(1) os.Exit(1)
} }

View File

@ -107,7 +107,7 @@ func runExport(
// It would be better to give a progressbar than a spinner, but we // It would be better to give a progressbar than a spinner, but we
// have any way of knowing how many files are available as of now. // have any way of knowing how many files are available as of now.
diskWriteComplete := observe.MessageWithCompletion(ctx, "Writing data to disk") diskWriteComplete := observe.MessageWithCompletion(ctx, observe.ProgressCfg{}, "Writing data to disk")
err = export.ConsumeExportCollections(ctx, exportLocation, expColl, eo.Errors) err = export.ConsumeExportCollections(ctx, exportLocation, expColl, eo.Errors)

View File

@ -10,6 +10,7 @@ import (
"github.com/tidwall/pretty" "github.com/tidwall/pretty"
"github.com/tomlazar/table" "github.com/tomlazar/table"
"github.com/alcionai/corso/src/internal/common/color"
"github.com/alcionai/corso/src/internal/observe" "github.com/alcionai/corso/src/internal/observe"
) )
@ -83,16 +84,21 @@ func Only(ctx context.Context, e error) error {
// Err prints the params to cobra's error writer (stdErr by default) // Err prints the params to cobra's error writer (stdErr by default)
// if s is nil, prints nothing. // if s is nil, prints nothing.
// Prepends the message with "Error: "
func Err(ctx context.Context, s ...any) { func Err(ctx context.Context, s ...any) {
out(ctx, getRootCmd(ctx).ErrOrStderr(), s...) cw := color.NewColorableWriter(color.Red, getRootCmd(ctx).ErrOrStderr())
s = append([]any{"Error:"}, s...)
out(ctx, cw, s...)
} }
// Errf prints the params to cobra's error writer (stdErr by default) // Errf prints the params to cobra's error writer (stdErr by default)
// if s is nil, prints nothing. // if s is nil, prints nothing.
// Prepends the message with "Error: " // You should ideally be using SimpleError or OperationError.
func Errf(ctx context.Context, tmpl string, s ...any) { func Errf(ctx context.Context, tmpl string, s ...any) {
outf(ctx, getRootCmd(ctx).ErrOrStderr(), "\nError: \n\t"+tmpl+"\n", s...) cw := color.NewColorableWriter(color.Red, getRootCmd(ctx).ErrOrStderr())
tmpl = "Error: " + tmpl
outf(ctx, cw, tmpl, s...)
} }
// Out prints the params to cobra's output writer (stdOut by default) // Out prints the params to cobra's output writer (stdOut by default)

View File

@ -10,6 +10,7 @@ require (
github.com/armon/go-metrics v0.4.1 github.com/armon/go-metrics v0.4.1
github.com/aws/aws-xray-sdk-go v1.8.3 github.com/aws/aws-xray-sdk-go v1.8.3
github.com/cenkalti/backoff/v4 v4.2.1 github.com/cenkalti/backoff/v4 v4.2.1
github.com/fatih/color v1.15.0
github.com/golang-jwt/jwt/v5 v5.1.0 github.com/golang-jwt/jwt/v5 v5.1.0
github.com/google/uuid v1.4.0 github.com/google/uuid v1.4.0
github.com/h2non/gock v1.2.0 github.com/h2non/gock v1.2.0
@ -51,7 +52,6 @@ require (
github.com/aws/aws-sdk-go v1.47.9 // indirect github.com/aws/aws-sdk-go v1.47.9 // indirect
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a // indirect github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/go-test/deep v1.1.0 // indirect
github.com/gofrs/flock v0.8.1 // indirect github.com/gofrs/flock v0.8.1 // indirect
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f // indirect github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f // indirect
github.com/google/go-cmp v0.5.9 // indirect github.com/google/go-cmp v0.5.9 // indirect

View File

@ -119,6 +119,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=

View File

@ -0,0 +1,36 @@
package color
import (
"io"
"github.com/fatih/color"
)
var (
Red = color.FgRed
Blue = color.FgBlue
Magenta = color.FgMagenta
Cyan = color.FgCyan
Green = color.FgGreen
White = color.FgWhite
RedOutput = color.New(Red).SprintFunc()
BlueOutput = color.New(Blue).SprintFunc()
MagentaOutput = color.New(Magenta).SprintFunc()
CyanOutput = color.New(Cyan).SprintFunc()
GreenOutput = color.New(Green).SprintFunc()
GreyOutput = color.New(White).SprintFunc()
)
type colorableWriter struct {
color color.Attribute
writer io.Writer
}
func NewColorableWriter(clr color.Attribute, writer io.Writer) io.Writer {
return &colorableWriter{clr, writer}
}
func (cw *colorableWriter) Write(p []byte) (n int, err error) {
return color.New(cw.color).Fprint(cw.writer, string(p))
}

View File

@ -651,6 +651,7 @@ func (w Wrapper) RepoMaintenance(
if len(params.Owner) == 0 || (params.Owner != currentOwner && opts.Force) { if len(params.Owner) == 0 || (params.Owner != currentOwner && opts.Force) {
observe.Message( observe.Message(
ctx, ctx,
observe.ProgressCfg{},
"updating maintenance user@host to ", "updating maintenance user@host to ",
clues.Hide(currentOwner)) clues.Hide(currentOwner))

View File

@ -3,7 +3,6 @@ package drive
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"fmt"
"io" "io"
"strings" "strings"
@ -18,7 +17,6 @@ import (
"github.com/alcionai/corso/src/internal/m365/collection/drive/metadata" "github.com/alcionai/corso/src/internal/m365/collection/drive/metadata"
odConsts "github.com/alcionai/corso/src/internal/m365/service/onedrive/consts" odConsts "github.com/alcionai/corso/src/internal/m365/service/onedrive/consts"
"github.com/alcionai/corso/src/internal/m365/support" "github.com/alcionai/corso/src/internal/m365/support"
"github.com/alcionai/corso/src/internal/observe"
bupMD "github.com/alcionai/corso/src/pkg/backup/metadata" bupMD "github.com/alcionai/corso/src/pkg/backup/metadata"
"github.com/alcionai/corso/src/pkg/control" "github.com/alcionai/corso/src/pkg/control"
"github.com/alcionai/corso/src/pkg/fault" "github.com/alcionai/corso/src/pkg/fault"
@ -283,11 +281,6 @@ func (c *Collections) Get(
driveTombstones[driveID] = struct{}{} driveTombstones[driveID] = struct{}{}
} }
progressBar := observe.MessageWithCompletion(
ctx,
observe.Bulletf(path.FilesCategory.HumanString()))
defer close(progressBar)
// Enumerate drives for the specified resourceOwner // Enumerate drives for the specified resourceOwner
pager := c.handler.NewDrivePager(c.protectedResource.ID(), nil) pager := c.handler.NewDrivePager(c.protectedResource.ID(), nil)
@ -456,8 +449,6 @@ func (c *Collections) Get(
} }
} }
observe.Message(ctx, fmt.Sprintf("Discovered %d items to backup", c.NumItems))
collections := []data.BackupCollection{} collections := []data.BackupCollection{}
// add all the drives we found // add all the drives we found

View File

@ -2,6 +2,7 @@ package exchange
import ( import (
"context" "context"
"fmt"
"github.com/alcionai/clues" "github.com/alcionai/clues"
@ -42,6 +43,8 @@ func CreateCollections(
ProtectedResource: bpc.ProtectedResource, ProtectedResource: bpc.ProtectedResource,
TenantID: tenantID, TenantID: tenantID,
} }
collections map[string]data.BackupCollection
err error
) )
handler, ok := handlers[category] handler, ok := handlers[category]
@ -49,9 +52,15 @@ func CreateCollections(
return nil, clues.NewWC(ctx, "unsupported backup category type") return nil, clues.NewWC(ctx, "unsupported backup category type")
} }
pcfg := observe.ProgressCfg{
Indent: 1,
CompletionMessage: func() string { return fmt.Sprintf("(found %d folders)", len(collections)) },
}
foldersComplete := observe.MessageWithCompletion( foldersComplete := observe.MessageWithCompletion(
ctx, ctx,
observe.Bulletf("%s", qp.Category.HumanString())) pcfg,
qp.Category.HumanString())
defer close(foldersComplete) defer close(foldersComplete)
rootFolder, cc := handler.NewContainerCache(bpc.ProtectedResource.ID()) rootFolder, cc := handler.NewContainerCache(bpc.ProtectedResource.ID())
@ -60,7 +69,7 @@ func CreateCollections(
return nil, clues.Wrap(err, "populating container cache") return nil, clues.Wrap(err, "populating container cache")
} }
collections, err := populateCollections( collections, err = populateCollections(
ctx, ctx,
qp, qp,
handler, handler,

View File

@ -2,6 +2,8 @@ package site
import ( import (
"context" "context"
"fmt"
stdpath "path"
"github.com/alcionai/clues" "github.com/alcionai/clues"
@ -10,6 +12,7 @@ import (
"github.com/alcionai/corso/src/internal/m365/collection/drive" "github.com/alcionai/corso/src/internal/m365/collection/drive"
betaAPI "github.com/alcionai/corso/src/internal/m365/service/sharepoint/api" betaAPI "github.com/alcionai/corso/src/internal/m365/service/sharepoint/api"
"github.com/alcionai/corso/src/internal/m365/support" "github.com/alcionai/corso/src/internal/m365/support"
"github.com/alcionai/corso/src/internal/observe"
"github.com/alcionai/corso/src/internal/operations/inject" "github.com/alcionai/corso/src/internal/operations/inject"
"github.com/alcionai/corso/src/pkg/account" "github.com/alcionai/corso/src/pkg/account"
"github.com/alcionai/corso/src/pkg/count" "github.com/alcionai/corso/src/pkg/count"
@ -44,6 +47,18 @@ func CollectLibraries(
bpc.Options) bpc.Options)
) )
msg := fmt.Sprintf(
"%s (%s)",
path.LibrariesCategory.HumanString(),
stdpath.Base(bpc.ProtectedResource.Name()))
pcfg := observe.ProgressCfg{
Indent: 1,
CompletionMessage: func() string { return fmt.Sprintf("(found %d items)", colls.NumItems) },
}
progressBar := observe.MessageWithCompletion(ctx, pcfg, msg)
close(progressBar)
odcs, canUsePreviousBackup, err := colls.Get(ctx, bpc.MetadataCollections, ssmb, errs) odcs, canUsePreviousBackup, err := colls.Get(ctx, bpc.MetadataCollections, ssmb, errs)
if err != nil { if err != nil {
return nil, false, graph.Wrap(ctx, err, "getting library") return nil, false, graph.Wrap(ctx, err, "getting library")

View File

@ -2,6 +2,7 @@ package groups
import ( import (
"context" "context"
"fmt"
"github.com/alcionai/clues" "github.com/alcionai/clues"
"github.com/kopia/kopia/repo/manifest" "github.com/kopia/kopia/repo/manifest"
@ -70,11 +71,6 @@ func ProduceBackupCollections(
break break
} }
progressBar := observe.MessageWithCompletion(
ctx,
observe.Bulletf("%s", scope.Category().PathType().HumanString()))
defer close(progressBar)
var dbcs []data.BackupCollection var dbcs []data.BackupCollection
switch scope.Category().PathType() { switch scope.Category().PathType() {
@ -134,15 +130,27 @@ func ProduceBackupCollections(
dbcs = append(dbcs, cs...) dbcs = append(dbcs, cs...)
} }
case path.ChannelMessagesCategory: case path.ChannelMessagesCategory:
var (
cs []data.BackupCollection
canUsePreviousBackup bool
err error
)
pcfg := observe.ProgressCfg{
Indent: 1,
// TODO(meain): Use number of messages and not channels
CompletionMessage: func() string { return fmt.Sprintf("(found %d channels)", len(cs)) },
}
progressBar := observe.MessageWithCompletion(ctx, pcfg, scope.Category().PathType().HumanString())
if !isTeam { if !isTeam {
continue continue
} }
bh := groups.NewChannelBackupHandler(bpc.ProtectedResource.ID(), ac.Channels()) bh := groups.NewChannelBackupHandler(bpc.ProtectedResource.ID(), ac.Channels())
cs, canUsePreviousBackup, err := groups.CreateCollections( cs, canUsePreviousBackup, err = groups.CreateCollections(
ctx, ctx,
bpc, bpc,
bh, bh,
@ -165,6 +173,8 @@ func ProduceBackupCollections(
} }
dbcs = append(dbcs, cs...) dbcs = append(dbcs, cs...)
close(progressBar)
} }
collections = append(collections, dbcs...) collections = append(collections, dbcs...)

View File

@ -2,6 +2,7 @@ package onedrive
import ( import (
"context" "context"
"fmt"
"github.com/alcionai/clues" "github.com/alcionai/clues"
@ -9,6 +10,7 @@ import (
"github.com/alcionai/corso/src/internal/data" "github.com/alcionai/corso/src/internal/data"
"github.com/alcionai/corso/src/internal/m365/collection/drive" "github.com/alcionai/corso/src/internal/m365/collection/drive"
"github.com/alcionai/corso/src/internal/m365/support" "github.com/alcionai/corso/src/internal/m365/support"
"github.com/alcionai/corso/src/internal/observe"
"github.com/alcionai/corso/src/internal/operations/inject" "github.com/alcionai/corso/src/internal/operations/inject"
"github.com/alcionai/corso/src/internal/version" "github.com/alcionai/corso/src/internal/version"
"github.com/alcionai/corso/src/pkg/fault" "github.com/alcionai/corso/src/pkg/fault"
@ -55,6 +57,14 @@ func ProduceBackupCollections(
su, su,
bpc.Options) bpc.Options)
pcfg := observe.ProgressCfg{
Indent: 1,
CompletionMessage: func() string { return fmt.Sprintf("(found %d files)", nc.NumFiles) },
}
progressBar := observe.MessageWithCompletion(ctx, pcfg, path.FilesCategory.HumanString())
defer close(progressBar)
odcs, canUsePreviousBackup, err = nc.Get(ctx, bpc.MetadataCollections, ssmb, errs) odcs, canUsePreviousBackup, err = nc.Get(ctx, bpc.MetadataCollections, ssmb, errs)
if err != nil { if err != nil {
el.AddRecoverable(ctx, clues.Stack(err).Label(fault.LabelForceNoBackupCreation)) el.AddRecoverable(ctx, clues.Stack(err).Label(fault.LabelForceNoBackupCreation))

View File

@ -10,7 +10,6 @@ import (
"github.com/alcionai/corso/src/internal/m365/collection/drive" "github.com/alcionai/corso/src/internal/m365/collection/drive"
"github.com/alcionai/corso/src/internal/m365/collection/site" "github.com/alcionai/corso/src/internal/m365/collection/site"
"github.com/alcionai/corso/src/internal/m365/support" "github.com/alcionai/corso/src/internal/m365/support"
"github.com/alcionai/corso/src/internal/observe"
"github.com/alcionai/corso/src/internal/operations/inject" "github.com/alcionai/corso/src/internal/operations/inject"
"github.com/alcionai/corso/src/pkg/account" "github.com/alcionai/corso/src/pkg/account"
"github.com/alcionai/corso/src/pkg/count" "github.com/alcionai/corso/src/pkg/count"
@ -52,11 +51,6 @@ func ProduceBackupCollections(
break break
} }
progressBar := observe.MessageWithCompletion(
ctx,
observe.Bulletf("%s", scope.Category().PathType().HumanString()))
defer close(progressBar)
var spcs []data.BackupCollection var spcs []data.BackupCollection
switch scope.Category().PathType() { switch scope.Category().PathType() {

View File

@ -15,13 +15,14 @@ import (
"github.com/vbauerster/mpb/v8" "github.com/vbauerster/mpb/v8"
"github.com/vbauerster/mpb/v8/decor" "github.com/vbauerster/mpb/v8/decor"
"github.com/alcionai/corso/src/internal/common/color"
"github.com/alcionai/corso/src/pkg/logger" "github.com/alcionai/corso/src/pkg/logger"
) )
const ( const (
hideProgressBarsFN = "hide-progress" hideProgressBarsFN = "hide-progress"
retainProgressBarsFN = "retain-progress" retainProgressBarsFN = "retain-progress"
progressBarWidth = 32 progressBarWidth = 40
) )
func init() { func init() {
@ -170,8 +171,15 @@ const (
// Progress Updates // Progress Updates
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
type ProgressCfg struct {
NewSection bool
SectionIdentifier any
Indent int
CompletionMessage func() string
}
// Message is used to display a progress message // Message is used to display a progress message
func Message(ctx context.Context, msgs ...any) { func Message(ctx context.Context, cfg ProgressCfg, msgs ...any) {
var ( var (
obs = getObserver(ctx) obs = getObserver(ctx)
plainSl = make([]string, 0, len(msgs)) plainSl = make([]string, 0, len(msgs))
@ -186,23 +194,42 @@ func Message(ctx context.Context, msgs ...any) {
plain := strings.Join(plainSl, " ") plain := strings.Join(plainSl, " ")
loggable := strings.Join(loggableSl, " ") loggable := strings.Join(loggableSl, " ")
if cfg.SectionIdentifier != nil {
plain = fmt.Sprintf(
"%s %s %s",
plain,
Bullet,
color.MagentaOutput(plainString(cfg.SectionIdentifier)))
loggable = fmt.Sprintf("%s - %v", loggable, cfg.SectionIdentifier)
}
logger.Ctx(ctx).Info(loggable) logger.Ctx(ctx).Info(loggable)
if obs.hidden() { if obs.hidden() {
return return
} }
if cfg.NewSection {
// Empty bar to separate out section
obs.wg.Add(1)
empty := obs.mp.New(-1, mpb.NopStyle())
empty.SetTotal(-1, true)
waitAndCloseBar(ctx, empty, obs.wg, func() {})()
}
obs.wg.Add(1) obs.wg.Add(1)
bar := obs.mp.New( bar := obs.mp.New(
-1, -1,
mpb.NopStyle(), mpb.NopStyle(),
mpb.PrependDecorators(decor.Name( mpb.PrependDecorators(
plain, decor.Name("", decor.WC{W: cfg.Indent * 2}),
decor.WC{ decor.Name(
W: len(plain) + 1, plain,
C: decor.DidentRight, decor.WC{
}))) W: len(plain) + 1,
C: decor.DidentRight,
})))
// Complete the bar immediately // Complete the bar immediately
bar.SetTotal(-1, true) bar.SetTotal(-1, true)
@ -213,16 +240,38 @@ func Message(ctx context.Context, msgs ...any) {
// that switches to "done" when the completion channel is signalled // that switches to "done" when the completion channel is signalled
func MessageWithCompletion( func MessageWithCompletion(
ctx context.Context, ctx context.Context,
msg any, cfg ProgressCfg,
msgs ...any,
) chan<- struct{} { ) chan<- struct{} {
var ( var (
obs = getObserver(ctx) obs = getObserver(ctx)
plain = plainString(msg) log = logger.Ctx(ctx)
loggable = fmt.Sprintf("%v", msg) plainSl = make([]string, 0, len(msgs))
log = logger.Ctx(ctx) loggableSl = make([]string, 0, len(msgs))
ch = make(chan struct{}, 1) ch = make(chan struct{}, 1)
) )
for _, m := range msgs {
plainSl = append(plainSl, plainString(m))
loggableSl = append(loggableSl, fmt.Sprintf("%v", m))
}
plain := strings.Join(plainSl, " ")
loggable := strings.Join(loggableSl, " ")
if cfg.SectionIdentifier != nil {
plain = fmt.Sprintf(
"%s %s %s",
plain,
Bullet,
color.MagentaOutput(plainString(cfg.SectionIdentifier)))
loggable = fmt.Sprintf("%s - %v", loggable, cfg.SectionIdentifier)
}
if cfg.Indent > 0 {
plain = color.CyanOutput(plain)
}
log.Info(loggable) log.Info(loggable)
if obs.hidden() { if obs.hidden() {
@ -230,17 +279,45 @@ func MessageWithCompletion(
return ch return ch
} }
if cfg.NewSection {
// Empty bar to separate out section
obs.wg.Add(1)
empty := obs.mp.New(-1, mpb.NopStyle())
empty.SetTotal(-1, true)
waitAndCloseBar(ctx, empty, obs.wg, func() {})()
}
obs.wg.Add(1) obs.wg.Add(1)
frames := []string{"∙∙∙", "●∙∙", "∙●∙", "∙∙●", "∙∙∙"} frames := []string{"∙∙∙", "●∙∙", "∙●∙", "∙∙●", "∙∙∙"}
// https://github.com/vbauerster/mpb/issues/71
bfoc := mpb.BarFillerOnComplete(color.GreenOutput("done"))
if cfg.CompletionMessage != nil {
bfoc = mpb.BarFillerMiddleware(func(base mpb.BarFiller) mpb.BarFiller {
filler := func(w io.Writer, st decor.Statistics) error {
if st.Completed {
msg := fmt.Sprintf("%s %s", color.GreenOutput("done"), color.GreyOutput(cfg.CompletionMessage()))
_, err := io.WriteString(w, msg)
return err
}
return base.Fill(w, st)
}
return mpb.BarFillerFunc(filler)
})
}
bar := obs.mp.New( bar := obs.mp.New(
-1, -1,
mpb.SpinnerStyle(frames...).PositionLeft(), mpb.SpinnerStyle(frames...).PositionLeft(),
mpb.PrependDecorators( mpb.PrependDecorators(
decor.Name(plain+":"), decor.Name("", decor.WC{W: cfg.Indent * 2}),
decor.Name(plain, decor.WC{W: progressBarWidth - cfg.Indent*2, C: decor.DidentRight}),
decor.Elapsed(decor.ET_STYLE_GO, decor.WC{W: 8})), decor.Elapsed(decor.ET_STYLE_GO, decor.WC{W: 8})),
mpb.BarFillerOnComplete("done")) bfoc)
go listen( go listen(
ctx, ctx,
@ -520,7 +597,9 @@ func CollectionProgress(
obs.wg.Add(1) obs.wg.Add(1)
barOpts := []mpb.BarOption{ barOpts := []mpb.BarOption{
mpb.PrependDecorators(decor.Name(string(category))), mpb.PrependDecorators(
decor.Name("", decor.WC{W: 2}),
decor.Name(color.CyanOutput(string(category)))),
mpb.AppendDecorators( mpb.AppendDecorators(
decor.CurrentNoUnit("%d - ", decor.WCSyncSpace), decor.CurrentNoUnit("%d - ", decor.WCSyncSpace),
decor.Name(plain)), decor.Name(plain)),

View File

@ -127,6 +127,30 @@ func (suite *ObserveProgressUnitSuite) TestCollectionProgress_unblockOnChannelCl
}() }()
} }
func (suite *ObserveProgressUnitSuite) TestObserve_section() {
t := suite.T()
ctx, flush := tester.NewContext(t)
defer flush()
recorder := strings.Builder{}
ctx = SeedObserver(ctx, &recorder, config{})
process := uuid.NewString()[:8]
target := uuid.NewString()[:8]
pcfg := ProgressCfg{
NewSection: true,
SectionIdentifier: target,
}
Message(ctx, pcfg, process)
Flush(ctx)
assert.NotEmpty(t, recorder)
assert.Contains(t, recorder.String(), process)
assert.Contains(t, recorder.String(), target)
}
func (suite *ObserveProgressUnitSuite) TestObserve_message() { func (suite *ObserveProgressUnitSuite) TestObserve_message() {
t := suite.T() t := suite.T()
@ -138,7 +162,7 @@ func (suite *ObserveProgressUnitSuite) TestObserve_message() {
message := uuid.NewString()[:8] message := uuid.NewString()[:8]
Message(ctx, message) Message(ctx, ProgressCfg{}, message)
Flush(ctx) Flush(ctx)
assert.NotEmpty(t, recorder) assert.NotEmpty(t, recorder)
assert.Contains(t, recorder.String(), message) assert.Contains(t, recorder.String(), message)
@ -155,7 +179,7 @@ func (suite *ObserveProgressUnitSuite) TestObserve_progressWithChannelClosed() {
message := uuid.NewString()[:8] message := uuid.NewString()[:8]
ch := MessageWithCompletion(ctx, message) ch := MessageWithCompletion(ctx, ProgressCfg{}, message)
// Close channel without completing // Close channel without completing
close(ch) close(ch)
@ -180,7 +204,7 @@ func (suite *ObserveProgressUnitSuite) TestObserve_progressWithContextCancelled(
message := uuid.NewString()[:8] message := uuid.NewString()[:8]
_ = MessageWithCompletion(ctx, message) _ = MessageWithCompletion(ctx, ProgressCfg{}, message)
// cancel context // cancel context
cancel() cancel()

View File

@ -279,7 +279,11 @@ func (op *BackupOperation) Run(ctx context.Context) (err error) {
// Execution // Execution
// ----- // -----
observe.Message(ctx, "Backing Up", observe.Bullet, clues.Hide(op.ResourceOwner.Name())) pcfg := observe.ProgressCfg{
NewSection: true,
SectionIdentifier: clues.Hide(op.ResourceOwner.Name()),
}
observe.Message(ctx, pcfg, "Backing Up")
deets, err := op.do( deets, err := op.do(
ctx, ctx,
@ -528,7 +532,7 @@ func produceBackupDataCollections(
counter *count.Bus, counter *count.Bus,
errs *fault.Bus, errs *fault.Bus,
) ([]data.BackupCollection, prefixmatcher.StringSetReader, bool, error) { ) ([]data.BackupCollection, prefixmatcher.StringSetReader, bool, error) {
progressBar := observe.MessageWithCompletion(ctx, "Discovering items to backup") progressBar := observe.MessageWithCompletion(ctx, observe.ProgressCfg{}, "Discovering items to backup")
defer close(progressBar) defer close(progressBar)
bpc := inject.BackupProducerConfig{ bpc := inject.BackupProducerConfig{
@ -565,7 +569,7 @@ func consumeBackupCollections(
"collection_source", "operations", "collection_source", "operations",
"snapshot_type", "item data") "snapshot_type", "item data")
progressBar := observe.MessageWithCompletion(ctx, "Backing up data") progressBar := observe.MessageWithCompletion(ctx, observe.ProgressCfg{}, "Backing up data")
defer close(progressBar) defer close(progressBar)
tags := map[string]string{ tags := map[string]string{

View File

@ -223,7 +223,11 @@ func (op *ExportOperation) do(
return nil, clues.Wrap(err, "getting backup and details") return nil, clues.Wrap(err, "getting backup and details")
} }
observe.Message(ctx, "Exporting", observe.Bullet, clues.Hide(bup.Selector.DiscreteOwner)) pcfg := observe.ProgressCfg{
NewSection: true,
SectionIdentifier: clues.Hide(bup.Selector.DiscreteOwner),
}
observe.Message(ctx, pcfg, "Exporting")
paths, err := formatDetailsForRestoration(ctx, bup.Version, op.Selectors, deets, op.ec, op.Errors) paths, err := formatDetailsForRestoration(ctx, bup.Version, op.Selectors, deets, op.ec, op.Errors)
if err != nil { if err != nil {
@ -239,9 +243,12 @@ func (op *ExportOperation) do(
"backup_snapshot_id", bup.SnapshotID, "backup_snapshot_id", bup.SnapshotID,
"backup_version", bup.Version) "backup_version", bup.Version)
observe.Message(ctx, fmt.Sprintf("Discovered %d items in backup %s to export", len(paths), op.BackupID)) observe.Message(
ctx,
observe.ProgressCfg{},
fmt.Sprintf("Discovered %d items in backup %s to export", len(paths), op.BackupID))
kopiaComplete := observe.MessageWithCompletion(ctx, "Enumerating items in repository") kopiaComplete := observe.MessageWithCompletion(ctx, observe.ProgressCfg{}, "Enumerating items in repository")
defer close(kopiaComplete) defer close(kopiaComplete)
dcs, err := op.kopia.ProduceRestoreCollections(ctx, bup.SnapshotID, paths, opStats.bytesRead, op.Errors) dcs, err := op.kopia.ProduceRestoreCollections(ctx, bup.SnapshotID, paths, opStats.bytesRead, op.Errors)
@ -332,7 +339,7 @@ func produceExportCollections(
exportStats *data.ExportStats, exportStats *data.ExportStats,
errs *fault.Bus, errs *fault.Bus,
) ([]export.Collectioner, error) { ) ([]export.Collectioner, error) {
complete := observe.MessageWithCompletion(ctx, "Preparing export") complete := observe.MessageWithCompletion(ctx, observe.ProgressCfg{}, "Preparing export")
defer func() { defer func() {
complete <- struct{}{} complete <- struct{}{}
close(complete) close(complete)

View File

@ -250,7 +250,11 @@ func (op *RestoreOperation) do(
return nil, clues.WrapWC(ctx, graph.ErrServiceNotEnabled, "service not enabled for restore") return nil, clues.WrapWC(ctx, graph.ErrServiceNotEnabled, "service not enabled for restore")
} }
observe.Message(ctx, "Restoring", observe.Bullet, clues.Hide(restoreToProtectedResource.Name())) pcfg := observe.ProgressCfg{
NewSection: true,
SectionIdentifier: clues.Hide(restoreToProtectedResource.Name()),
}
observe.Message(ctx, pcfg, "Restoring")
paths, err := formatDetailsForRestoration( paths, err := formatDetailsForRestoration(
ctx, ctx,
@ -270,9 +274,16 @@ func (op *RestoreOperation) do(
"backup_snapshot_id", bup.SnapshotID, "backup_snapshot_id", bup.SnapshotID,
"backup_version", bup.Version) "backup_version", bup.Version)
observe.Message(ctx, fmt.Sprintf("Discovered %d items in backup %s to restore", len(paths), op.BackupID)) if len(paths) == 0 {
return nil, clues.New("no items match the provided filters")
}
progressBar := observe.MessageWithCompletion(ctx, "Enumerating items in repository") observe.Message(
ctx,
observe.ProgressCfg{},
fmt.Sprintf("Discovered %d items in backup %s to restore", len(paths), op.BackupID))
progressBar := observe.MessageWithCompletion(ctx, observe.ProgressCfg{}, "Enumerating items in repository")
defer close(progressBar) defer close(progressBar)
dcs, err := op.kopia.ProduceRestoreCollections( dcs, err := op.kopia.ProduceRestoreCollections(
@ -380,7 +391,7 @@ func consumeRestoreCollections(
errs *fault.Bus, errs *fault.Bus,
ctr *count.Bus, ctr *count.Bus,
) (*details.Details, error) { ) (*details.Details, error) {
progressBar := observe.MessageWithCompletion(ctx, "Restoring data") progressBar := observe.MessageWithCompletion(ctx, observe.ProgressCfg{}, "Restoring data")
defer close(progressBar) defer close(progressBar)
rcc := inject.RestoreConsumerConfig{ rcc := inject.RestoreConsumerConfig{

View File

@ -89,7 +89,7 @@ func connectToM365(
return ctrl, nil return ctrl, nil
} }
progressBar := observe.MessageWithCompletion(ctx, "Connecting to M365") progressBar := observe.MessageWithCompletion(ctx, observe.ProgressCfg{}, "Connecting to M365")
defer close(progressBar) defer close(progressBar)
ctrl, err := m365.NewController( ctrl, err := m365.NewController(

View File

@ -153,7 +153,7 @@ func (r *repository) Initialize(ctx context.Context, cfg InitConfig) (err error)
return clues.Stack(err) return clues.Stack(err)
} }
observe.Message(ctx, "Initializing repository") observe.Message(ctx, observe.ProgressCfg{}, "Initializing repository")
if err := r.setupKopia(ctx, cfg.RetentionOpts, true); err != nil { if err := r.setupKopia(ctx, cfg.RetentionOpts, true); err != nil {
return err return err
@ -187,7 +187,8 @@ func (r *repository) Connect(ctx context.Context, cfg ConnConfig) (err error) {
return clues.Stack(err) return clues.Stack(err)
} }
observe.Message(ctx, "Connecting to repository") progressBar := observe.MessageWithCompletion(ctx, observe.ProgressCfg{}, "Connecting to repository")
defer close(progressBar)
if err := r.setupKopia(ctx, ctrlRepo.Retention{}, false); err != nil { if err := r.setupKopia(ctx, ctrlRepo.Retention{}, false); err != nil {
return clues.Stack(err) return clues.Stack(err)
@ -209,7 +210,7 @@ func (r *repository) UpdatePassword(ctx context.Context, password string) (err e
} }
}() }()
progressBar := observe.MessageWithCompletion(ctx, "Connecting to repository") progressBar := observe.MessageWithCompletion(ctx, observe.ProgressCfg{}, "Connecting to repository")
defer close(progressBar) defer close(progressBar)
repoNameHash, err := r.GenerateHashForRepositoryConfigFileName() repoNameHash, err := r.GenerateHashForRepositoryConfigFileName()