diff --git a/cmd/restic/cleanup.go b/cmd/restic/cleanup.go index 45dc43b5c..b01029c0c 100644 --- a/cmd/restic/cleanup.go +++ b/cmd/restic/cleanup.go @@ -2,6 +2,7 @@ package main import ( "context" + "fmt" "os" "os/signal" "syscall" @@ -23,7 +24,8 @@ func createGlobalContext() context.Context { func cleanupHandler(c <-chan os.Signal, cancel context.CancelFunc) { s := <-c debug.Log("signal %v received, cleaning up", s) - Warnf("\rsignal %v received, cleaning up \n", s) + // ignore error as there's no good way to handle it + _, _ = fmt.Fprintf(os.Stderr, "\rsignal %v received, cleaning up \n", s) if val, _ := os.LookupEnv("RESTIC_DEBUG_STACKTRACE_SIGINT"); val != "" { _, _ = os.Stderr.WriteString("\n--- STACKTRACE START ---\n\n") diff --git a/cmd/restic/cmd_backup.go b/cmd/restic/cmd_backup.go index acb5500fd..2c7e209c5 100644 --- a/cmd/restic/cmd_backup.go +++ b/cmd/restic/cmd_backup.go @@ -28,7 +28,6 @@ import ( "github.com/restic/restic/internal/textfile" "github.com/restic/restic/internal/ui" "github.com/restic/restic/internal/ui/backup" - "github.com/restic/restic/internal/ui/termstatus" ) func newBackupCommand() *cobra.Command { @@ -161,11 +160,11 @@ var ErrInvalidSourceData = errors.New("at least one source file could not be rea // filterExisting returns a slice of all existing items, or an error if no // items exist at all. -func filterExisting(items []string) (result []string, err error) { +func filterExisting(items []string, warnf func(msg string, args ...interface{})) (result []string, err error) { for _, item := range items { _, err := fs.Lstat(item) if errors.Is(err, os.ErrNotExist) { - Warnf("%v does not exist, skipping\n", item) + warnf("%v does not exist, skipping\n", item) continue } @@ -306,7 +305,7 @@ func (opts BackupOptions) Check(gopts GlobalOptions, args []string) error { // collectRejectByNameFuncs returns a list of all functions which may reject data // from being saved in a snapshot based on path only -func collectRejectByNameFuncs(opts BackupOptions, repo *repository.Repository) (fs []archiver.RejectByNameFunc, err error) { +func collectRejectByNameFuncs(opts BackupOptions, repo *repository.Repository, warnf func(msg string, args ...interface{})) (fs []archiver.RejectByNameFunc, err error) { // exclude restic cache if repo.Cache() != nil { f, err := rejectResticCache(repo) @@ -317,7 +316,7 @@ func collectRejectByNameFuncs(opts BackupOptions, repo *repository.Repository) ( fs = append(fs, f) } - fsPatterns, err := opts.ExcludePatternOptions.CollectPatterns(Warnf) + fsPatterns, err := opts.ExcludePatternOptions.CollectPatterns(warnf) if err != nil { return nil, err } @@ -330,7 +329,7 @@ func collectRejectByNameFuncs(opts BackupOptions, repo *repository.Repository) ( // collectRejectFuncs returns a list of all functions which may reject data // from being saved in a snapshot based on path and file info -func collectRejectFuncs(opts BackupOptions, targets []string, fs fs.FS) (funcs []archiver.RejectFunc, err error) { +func collectRejectFuncs(opts BackupOptions, targets []string, fs fs.FS, warnf func(msg string, args ...interface{})) (funcs []archiver.RejectFunc, err error) { // allowed devices if opts.ExcludeOtherFS && !opts.Stdin && !opts.StdinCommand { f, err := archiver.RejectByDevice(targets, fs) @@ -357,7 +356,7 @@ func collectRejectFuncs(opts BackupOptions, targets []string, fs fs.FS) (funcs [ if runtime.GOOS != "windows" { return nil, errors.Fatalf("exclude-cloud-files is only supported on Windows") } - f, err := archiver.RejectCloudFiles(Warnf) + f, err := archiver.RejectCloudFiles(warnf) if err != nil { return nil, err } @@ -369,7 +368,7 @@ func collectRejectFuncs(opts BackupOptions, targets []string, fs fs.FS) (funcs [ } for _, spec := range opts.ExcludeIfPresent { - f, err := archiver.RejectIfPresent(spec, Warnf) + f, err := archiver.RejectIfPresent(spec, warnf) if err != nil { return nil, err } @@ -381,7 +380,7 @@ func collectRejectFuncs(opts BackupOptions, targets []string, fs fs.FS) (funcs [ } // collectTargets returns a list of target files/dirs from several sources. -func collectTargets(opts BackupOptions, args []string) (targets []string, err error) { +func collectTargets(opts BackupOptions, args []string, warnf func(msg string, args ...interface{})) (targets []string, err error) { if opts.Stdin || opts.StdinCommand { return nil, nil } @@ -405,7 +404,7 @@ func collectTargets(opts BackupOptions, args []string) (targets []string, err er return nil, fmt.Errorf("pattern: %s: %w", line, err) } if len(expanded) == 0 { - Warnf("pattern %q does not match any files, skipping\n", line) + warnf("pattern %q does not match any files, skipping\n", line) } targets = append(targets, expanded...) } @@ -439,7 +438,7 @@ func collectTargets(opts BackupOptions, args []string) (targets []string, err er return nil, errors.Fatal("nothing to backup, please specify source files/dirs") } - targets, err = filterExisting(targets) + targets, err = filterExisting(targets, warnf) if err != nil { return nil, err } @@ -477,10 +476,11 @@ func findParentSnapshot(ctx context.Context, repo restic.ListerLoaderUnpacked, o return sn, err } -func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, term *termstatus.Terminal, args []string) error { +func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, term ui.Terminal, args []string) error { var vsscfg fs.VSSConfig var err error + msg := newTerminalProgressPrinter(gopts.JSON, gopts.verbosity, term) if runtime.GOOS == "windows" { if vsscfg, err = fs.ParseVSSConfig(gopts.extended); err != nil { return err @@ -492,7 +492,7 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, ter return err } - targets, err := collectTargets(opts, args) + targets, err := collectTargets(opts, args, msg.E) if err != nil { return err } @@ -507,10 +507,10 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, ter } if gopts.verbosity >= 2 && !gopts.JSON { - Verbosef("open repository\n") + msg.P("open repository") } - ctx, repo, unlock, err := openWithAppendLock(ctx, gopts, opts.DryRun) + ctx, repo, unlock, err := openWithAppendLock(ctx, gopts, opts.DryRun, msg) if err != nil { return err } @@ -527,7 +527,7 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, ter defer progressReporter.Done() // rejectByNameFuncs collect functions that can reject items from the backup based on path only - rejectByNameFuncs, err := collectRejectByNameFuncs(opts, repo) + rejectByNameFuncs, err := collectRejectByNameFuncs(opts, repo, msg.E) if err != nil { return err } @@ -552,7 +552,7 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, ter progressPrinter.V("load index files") } - bar := newIndexTerminalProgress(gopts.Quiet, gopts.JSON, term) + bar := newIndexTerminalProgress(msg) err = repo.LoadIndex(ctx, bar) if err != nil { return err @@ -606,7 +606,7 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, ter } // rejectFuncs collect functions that can reject items from the backup based on path and file info - rejectFuncs, err := collectRejectFuncs(opts, targets, targetFS) + rejectFuncs, err := collectRejectFuncs(opts, targets, targetFS, msg.E) if err != nil { return err } diff --git a/cmd/restic/cmd_backup_integration_test.go b/cmd/restic/cmd_backup_integration_test.go index 0002b207f..ba9ea2e62 100644 --- a/cmd/restic/cmd_backup_integration_test.go +++ b/cmd/restic/cmd_backup_integration_test.go @@ -13,11 +13,11 @@ import ( "github.com/restic/restic/internal/fs" "github.com/restic/restic/internal/restic" rtest "github.com/restic/restic/internal/test" - "github.com/restic/restic/internal/ui/termstatus" + "github.com/restic/restic/internal/ui" ) func testRunBackupAssumeFailure(t testing.TB, dir string, target []string, opts BackupOptions, gopts GlobalOptions) error { - return withTermStatus(gopts, func(ctx context.Context, term *termstatus.Terminal) error { + return withTermStatus(gopts, func(ctx context.Context, term ui.Terminal) error { t.Logf("backing up %v in %v", target, dir) if dir != "" { cleanup := rtest.Chdir(t, dir) @@ -218,41 +218,41 @@ func TestDryRunBackup(t *testing.T) { // dry run before first backup testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, dryOpts, env.gopts) snapshotIDs := testListSnapshots(t, env.gopts, 0) - packIDs := testRunList(t, "packs", env.gopts) + packIDs := testRunList(t, env.gopts, "packs") rtest.Assert(t, len(packIDs) == 0, "expected no data, got %v", snapshotIDs) - indexIDs := testRunList(t, "index", env.gopts) + indexIDs := testRunList(t, env.gopts, "index") rtest.Assert(t, len(indexIDs) == 0, "expected no index, got %v", snapshotIDs) // first backup testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, opts, env.gopts) snapshotIDs = testListSnapshots(t, env.gopts, 1) - packIDs = testRunList(t, "packs", env.gopts) - indexIDs = testRunList(t, "index", env.gopts) + packIDs = testRunList(t, env.gopts, "packs") + indexIDs = testRunList(t, env.gopts, "index") // dry run between backups testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, dryOpts, env.gopts) snapshotIDsAfter := testListSnapshots(t, env.gopts, 1) rtest.Equals(t, snapshotIDs, snapshotIDsAfter) - dataIDsAfter := testRunList(t, "packs", env.gopts) + dataIDsAfter := testRunList(t, env.gopts, "packs") rtest.Equals(t, packIDs, dataIDsAfter) - indexIDsAfter := testRunList(t, "index", env.gopts) + indexIDsAfter := testRunList(t, env.gopts, "index") rtest.Equals(t, indexIDs, indexIDsAfter) // second backup, implicit incremental testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, opts, env.gopts) snapshotIDs = testListSnapshots(t, env.gopts, 2) - packIDs = testRunList(t, "packs", env.gopts) - indexIDs = testRunList(t, "index", env.gopts) + packIDs = testRunList(t, env.gopts, "packs") + indexIDs = testRunList(t, env.gopts, "index") // another dry run testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, dryOpts, env.gopts) snapshotIDsAfter = testListSnapshots(t, env.gopts, 2) rtest.Equals(t, snapshotIDs, snapshotIDsAfter) - dataIDsAfter = testRunList(t, "packs", env.gopts) + dataIDsAfter = testRunList(t, env.gopts, "packs") rtest.Equals(t, packIDs, dataIDsAfter) - indexIDsAfter = testRunList(t, "index", env.gopts) + indexIDsAfter = testRunList(t, env.gopts, "index") rtest.Equals(t, indexIDs, indexIDsAfter) } diff --git a/cmd/restic/cmd_backup_test.go b/cmd/restic/cmd_backup_test.go index 44e08ff96..ef5f02825 100644 --- a/cmd/restic/cmd_backup_test.go +++ b/cmd/restic/cmd_backup_test.go @@ -67,7 +67,7 @@ func TestCollectTargets(t *testing.T) { FilesFromRaw: []string{f3.Name()}, } - targets, err := collectTargets(opts, []string{filepath.Join(dir, "cmdline arg")}) + targets, err := collectTargets(opts, []string{filepath.Join(dir, "cmdline arg")}, t.Logf) rtest.OK(t, err) sort.Strings(targets) rtest.Equals(t, expect, targets) diff --git a/cmd/restic/cmd_cache.go b/cmd/restic/cmd_cache.go index 284c94cad..f94c23747 100644 --- a/cmd/restic/cmd_cache.go +++ b/cmd/restic/cmd_cache.go @@ -34,7 +34,9 @@ Exit status is 1 if there was any error. GroupID: cmdGroupDefault, DisableAutoGenTag: true, RunE: func(_ *cobra.Command, args []string) error { - return runCache(opts, globalOptions, args) + term, cancel := setupTermstatus() + defer cancel() + return runCache(opts, globalOptions, args, term) }, } @@ -55,7 +57,9 @@ func (opts *CacheOptions) AddFlags(f *pflag.FlagSet) { f.BoolVar(&opts.NoSize, "no-size", false, "do not output the size of the cache directories") } -func runCache(opts CacheOptions, gopts GlobalOptions, args []string) error { +func runCache(opts CacheOptions, gopts GlobalOptions, args []string, term ui.Terminal) error { + printer := newTerminalProgressPrinter(false, gopts.verbosity, term) + if len(args) > 0 { return errors.Fatal("the cache command expects no arguments, only options - please see `restic help cache` for usage and flags") } @@ -83,17 +87,17 @@ func runCache(opts CacheOptions, gopts GlobalOptions, args []string) error { } if len(oldDirs) == 0 { - Verbosef("no old cache dirs found\n") + printer.P("no old cache dirs found") return nil } - Verbosef("remove %d old cache directories\n", len(oldDirs)) + printer.P("remove %d old cache directories", len(oldDirs)) for _, item := range oldDirs { dir := filepath.Join(cachedir, item.Name()) err = os.RemoveAll(dir) if err != nil { - Warnf("unable to remove %v: %v\n", dir, err) + printer.E("unable to remove %v: %v", dir, err) } } @@ -123,7 +127,7 @@ func runCache(opts CacheOptions, gopts GlobalOptions, args []string) error { } if len(dirs) == 0 { - Printf("no cache dirs found, basedir is %v\n", cachedir) + printer.S("no cache dirs found, basedir is %v", cachedir) return nil } @@ -160,7 +164,7 @@ func runCache(opts CacheOptions, gopts GlobalOptions, args []string) error { } _ = tab.Write(globalOptions.stdout) - Printf("%d cache dirs in %s\n", len(dirs), cachedir) + printer.S("%d cache dirs in %s", len(dirs), cachedir) return nil } diff --git a/cmd/restic/cmd_cat.go b/cmd/restic/cmd_cat.go index 983e309dd..41ea9f5e0 100644 --- a/cmd/restic/cmd_cat.go +++ b/cmd/restic/cmd_cat.go @@ -10,6 +10,7 @@ import ( "github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/repository" "github.com/restic/restic/internal/restic" + "github.com/restic/restic/internal/ui" ) var catAllowedCmds = []string{"config", "index", "snapshot", "key", "masterkey", "lock", "pack", "blob", "tree"} @@ -33,7 +34,9 @@ Exit status is 12 if the password is incorrect. GroupID: cmdGroupDefault, DisableAutoGenTag: true, RunE: func(cmd *cobra.Command, args []string) error { - return runCat(cmd.Context(), globalOptions, args) + term, cancel := setupTermstatus() + defer cancel() + return runCat(cmd.Context(), globalOptions, args, term) }, ValidArgs: catAllowedCmds, } @@ -63,12 +66,14 @@ func validateCatArgs(args []string) error { return nil } -func runCat(ctx context.Context, gopts GlobalOptions, args []string) error { +func runCat(ctx context.Context, gopts GlobalOptions, args []string, term ui.Terminal) error { + printer := newTerminalProgressPrinter(gopts.JSON, gopts.verbosity, term) + if err := validateCatArgs(args); err != nil { return err } - ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock) + ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock, printer) if err != nil { return err } @@ -80,7 +85,7 @@ func runCat(ctx context.Context, gopts GlobalOptions, args []string) error { if tpe != "masterkey" && tpe != "config" && tpe != "snapshot" && tpe != "tree" { id, err = restic.ParseID(args[1]) if err != nil { - return errors.Fatalf("unable to parse ID: %v\n", err) + return errors.Fatalf("unable to parse ID: %v", err) } } @@ -91,7 +96,7 @@ func runCat(ctx context.Context, gopts GlobalOptions, args []string) error { return err } - Println(string(buf)) + printer.S(string(buf)) return nil case "index": buf, err := repo.LoadUnpacked(ctx, restic.IndexFile, id) @@ -99,7 +104,7 @@ func runCat(ctx context.Context, gopts GlobalOptions, args []string) error { return err } - Println(string(buf)) + printer.S(string(buf)) return nil case "snapshot": sn, _, err := restic.FindSnapshot(ctx, repo, repo, args[1]) @@ -112,7 +117,7 @@ func runCat(ctx context.Context, gopts GlobalOptions, args []string) error { return err } - Println(string(buf)) + printer.S(string(buf)) return nil case "key": key, err := repository.LoadKey(ctx, repo, id) @@ -125,7 +130,7 @@ func runCat(ctx context.Context, gopts GlobalOptions, args []string) error { return err } - Println(string(buf)) + printer.S(string(buf)) return nil case "masterkey": buf, err := json.MarshalIndent(repo.Key(), "", " ") @@ -133,7 +138,7 @@ func runCat(ctx context.Context, gopts GlobalOptions, args []string) error { return err } - Println(string(buf)) + printer.S(string(buf)) return nil case "lock": lock, err := restic.LoadLock(ctx, repo, id) @@ -146,7 +151,7 @@ func runCat(ctx context.Context, gopts GlobalOptions, args []string) error { return err } - Println(string(buf)) + printer.S(string(buf)) return nil case "pack": @@ -158,14 +163,14 @@ func runCat(ctx context.Context, gopts GlobalOptions, args []string) error { hash := restic.Hash(buf) if !hash.Equal(id) { - Warnf("Warning: hash of data does not match ID, want\n %v\ngot:\n %v\n", id.String(), hash.String()) + printer.E("Warning: hash of data does not match ID, want\n %v\ngot:\n %v", id.String(), hash.String()) } - _, err = globalOptions.stdout.Write(buf) + _, err = term.OutputRaw().Write(buf) return err case "blob": - bar := newIndexProgress(gopts.Quiet, gopts.JSON) + bar := newIndexTerminalProgress(printer) err = repo.LoadIndex(ctx, bar) if err != nil { return err @@ -181,7 +186,7 @@ func runCat(ctx context.Context, gopts GlobalOptions, args []string) error { return err } - _, err = globalOptions.stdout.Write(buf) + _, err = term.OutputRaw().Write(buf) return err } @@ -193,7 +198,7 @@ func runCat(ctx context.Context, gopts GlobalOptions, args []string) error { return errors.Fatalf("could not find snapshot: %v\n", err) } - bar := newIndexProgress(gopts.Quiet, gopts.JSON) + bar := newIndexTerminalProgress(printer) err = repo.LoadIndex(ctx, bar) if err != nil { return err @@ -208,7 +213,7 @@ func runCat(ctx context.Context, gopts GlobalOptions, args []string) error { if err != nil { return err } - _, err = globalOptions.stdout.Write(buf) + _, err = term.OutputRaw().Write(buf) return err default: diff --git a/cmd/restic/cmd_check.go b/cmd/restic/cmd_check.go index 6bbaa2747..8b9937b69 100644 --- a/cmd/restic/cmd_check.go +++ b/cmd/restic/cmd_check.go @@ -20,7 +20,6 @@ import ( "github.com/restic/restic/internal/restic" "github.com/restic/restic/internal/ui" "github.com/restic/restic/internal/ui/progress" - "github.com/restic/restic/internal/ui/termstatus" ) func newCheckCommand() *cobra.Command { @@ -194,7 +193,7 @@ func prepareCheckCache(opts CheckOptions, gopts *GlobalOptions, printer progress // use a cache in a temporary directory err := os.MkdirAll(cachedir, 0755) if err != nil { - Warnf("unable to create cache directory %s, disabling cache: %v\n", cachedir, err) + printer.E("unable to create cache directory %s, disabling cache: %v", cachedir, err) gopts.NoCache = true return cleanup } @@ -220,7 +219,7 @@ func prepareCheckCache(opts CheckOptions, gopts *GlobalOptions, printer progress return cleanup } -func runCheck(ctx context.Context, opts CheckOptions, gopts GlobalOptions, args []string, term *termstatus.Terminal) (checkSummary, error) { +func runCheck(ctx context.Context, opts CheckOptions, gopts GlobalOptions, args []string, term ui.Terminal) (checkSummary, error) { summary := checkSummary{MessageType: "summary"} if len(args) != 0 { return summary, errors.Fatal("the check command expects no arguments, only options - please see `restic help check` for usage and flags") @@ -228,7 +227,7 @@ func runCheck(ctx context.Context, opts CheckOptions, gopts GlobalOptions, args var printer progress.Printer if !gopts.JSON { - printer = newTerminalProgressPrinter(gopts.verbosity, term) + printer = newTerminalProgressPrinter(gopts.JSON, gopts.verbosity, term) } else { printer = newJSONErrorPrinter(term) } @@ -239,7 +238,7 @@ func runCheck(ctx context.Context, opts CheckOptions, gopts GlobalOptions, args if !gopts.NoLock { printer.P("create exclusive lock for repository\n") } - ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, gopts.NoLock) + ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, gopts.NoLock, printer) if err != nil { return summary, err } @@ -252,7 +251,7 @@ func runCheck(ctx context.Context, opts CheckOptions, gopts GlobalOptions, args } printer.P("load indexes\n") - bar := newIndexTerminalProgress(gopts.Quiet, gopts.JSON, term) + bar := newIndexTerminalProgress(printer) hints, errs := chkr.LoadIndex(ctx, bar) if ctx.Err() != nil { return summary, ctx.Err() @@ -528,6 +527,10 @@ func (*jsonErrorPrinter) NewCounter(_ string) *progress.Counter { return nil } +func (*jsonErrorPrinter) NewCounterTerminalOnly(_ string) *progress.Counter { + return nil +} + func (p *jsonErrorPrinter) E(msg string, args ...interface{}) { status := checkError{ MessageType: "error", diff --git a/cmd/restic/cmd_check_integration_test.go b/cmd/restic/cmd_check_integration_test.go index f5a3dc395..59a0f7498 100644 --- a/cmd/restic/cmd_check_integration_test.go +++ b/cmd/restic/cmd_check_integration_test.go @@ -6,7 +6,7 @@ import ( "testing" rtest "github.com/restic/restic/internal/test" - "github.com/restic/restic/internal/ui/termstatus" + "github.com/restic/restic/internal/ui" ) func testRunCheck(t testing.TB, gopts GlobalOptions) { @@ -27,7 +27,7 @@ func testRunCheckMustFail(t testing.TB, gopts GlobalOptions) { func testRunCheckOutput(gopts GlobalOptions, checkUnused bool) (string, error) { buf := bytes.NewBuffer(nil) gopts.stdout = buf - err := withTermStatus(gopts, func(ctx context.Context, term *termstatus.Terminal) error { + err := withTermStatus(gopts, func(ctx context.Context, term ui.Terminal) error { opts := CheckOptions{ ReadData: true, CheckUnused: checkUnused, diff --git a/cmd/restic/cmd_copy.go b/cmd/restic/cmd_copy.go index 2ad5a464c..341537067 100644 --- a/cmd/restic/cmd_copy.go +++ b/cmd/restic/cmd_copy.go @@ -8,6 +8,8 @@ import ( "github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/repository" "github.com/restic/restic/internal/restic" + "github.com/restic/restic/internal/ui" + "github.com/restic/restic/internal/ui/progress" "golang.org/x/sync/errgroup" "github.com/spf13/cobra" @@ -46,7 +48,9 @@ Exit status is 12 if the password is incorrect. GroupID: cmdGroupDefault, DisableAutoGenTag: true, RunE: func(cmd *cobra.Command, args []string) error { - return runCopy(cmd.Context(), opts, globalOptions, args) + term, cancel := setupTermstatus() + defer cancel() + return runCopy(cmd.Context(), opts, globalOptions, args, term) }, } @@ -65,8 +69,9 @@ func (opts *CopyOptions) AddFlags(f *pflag.FlagSet) { initMultiSnapshotFilter(f, &opts.SnapshotFilter, true) } -func runCopy(ctx context.Context, opts CopyOptions, gopts GlobalOptions, args []string) error { - secondaryGopts, isFromRepo, err := fillSecondaryGlobalOpts(ctx, opts.secondaryRepoOptions, gopts, "destination") +func runCopy(ctx context.Context, opts CopyOptions, gopts GlobalOptions, args []string, term ui.Terminal) error { + printer := newTerminalProgressPrinter(false, gopts.verbosity, term) + secondaryGopts, isFromRepo, err := fillSecondaryGlobalOpts(ctx, opts.secondaryRepoOptions, gopts, "destination", printer) if err != nil { return err } @@ -75,13 +80,13 @@ func runCopy(ctx context.Context, opts CopyOptions, gopts GlobalOptions, args [] gopts, secondaryGopts = secondaryGopts, gopts } - ctx, srcRepo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock) + ctx, srcRepo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock, printer) if err != nil { return err } defer unlock() - ctx, dstRepo, unlock, err := openWithAppendLock(ctx, secondaryGopts, false) + ctx, dstRepo, unlock, err := openWithAppendLock(ctx, secondaryGopts, false, printer) if err != nil { return err } @@ -98,18 +103,18 @@ func runCopy(ctx context.Context, opts CopyOptions, gopts GlobalOptions, args [] } debug.Log("Loading source index") - bar := newIndexProgress(gopts.Quiet, gopts.JSON) + bar := newIndexTerminalProgress(printer) if err := srcRepo.LoadIndex(ctx, bar); err != nil { return err } - bar = newIndexProgress(gopts.Quiet, gopts.JSON) + bar = newIndexTerminalProgress(printer) debug.Log("Loading destination index") if err := dstRepo.LoadIndex(ctx, bar); err != nil { return err } dstSnapshotByOriginal := make(map[restic.ID][]*restic.Snapshot) - for sn := range FindFilteredSnapshots(ctx, dstSnapshotLister, dstRepo, &opts.SnapshotFilter, nil) { + for sn := range FindFilteredSnapshots(ctx, dstSnapshotLister, dstRepo, &opts.SnapshotFilter, nil, printer) { if sn.Original != nil && !sn.Original.IsNull() { dstSnapshotByOriginal[*sn.Original] = append(dstSnapshotByOriginal[*sn.Original], sn) } @@ -123,7 +128,7 @@ func runCopy(ctx context.Context, opts CopyOptions, gopts GlobalOptions, args [] // remember already processed trees across all snapshots visitedTrees := restic.NewIDSet() - for sn := range FindFilteredSnapshots(ctx, srcSnapshotLister, srcRepo, &opts.SnapshotFilter, args) { + for sn := range FindFilteredSnapshots(ctx, srcSnapshotLister, srcRepo, &opts.SnapshotFilter, args, printer) { // check whether the destination has a snapshot with the same persistent ID which has similar snapshot fields srcOriginal := *sn.ID() if sn.Original != nil { @@ -134,8 +139,8 @@ func runCopy(ctx context.Context, opts CopyOptions, gopts GlobalOptions, args [] isCopy := false for _, originalSn := range originalSns { if similarSnapshots(originalSn, sn) { - Verboseff("\n%v\n", sn) - Verboseff("skipping source snapshot %s, was already copied to snapshot %s\n", sn.ID().Str(), originalSn.ID().Str()) + printer.V("\n%v", sn) + printer.V("skipping source snapshot %s, was already copied to snapshot %s", sn.ID().Str(), originalSn.ID().Str()) isCopy = true break } @@ -144,9 +149,9 @@ func runCopy(ctx context.Context, opts CopyOptions, gopts GlobalOptions, args [] continue } } - Verbosef("\n%v\n", sn) - Verbosef(" copy started, this may take a while...\n") - if err := copyTree(ctx, srcRepo, dstRepo, visitedTrees, *sn.Tree, gopts.Quiet); err != nil { + printer.P("\n%v", sn) + printer.P(" copy started, this may take a while...") + if err := copyTree(ctx, srcRepo, dstRepo, visitedTrees, *sn.Tree, printer); err != nil { return err } debug.Log("tree copied") @@ -161,7 +166,7 @@ func runCopy(ctx context.Context, opts CopyOptions, gopts GlobalOptions, args [] if err != nil { return err } - Verbosef("snapshot %s saved\n", newID.Str()) + printer.P("snapshot %s saved", newID.Str()) } return ctx.Err() } @@ -186,7 +191,7 @@ func similarSnapshots(sna *restic.Snapshot, snb *restic.Snapshot) bool { } func copyTree(ctx context.Context, srcRepo restic.Repository, dstRepo restic.Repository, - visitedTrees restic.IDSet, rootTreeID restic.ID, quiet bool) error { + visitedTrees restic.IDSet, rootTreeID restic.ID, printer progress.Printer) error { wg, wgCtx := errgroup.WithContext(ctx) @@ -238,16 +243,9 @@ func copyTree(ctx context.Context, srcRepo restic.Repository, dstRepo restic.Rep return err } - bar := newProgressMax(!quiet, uint64(len(packList)), "packs copied") - _, err = repository.Repack( - ctx, - srcRepo, - dstRepo, - packList, - copyBlobs, - bar, - func(msg string, args ...interface{}) { fmt.Printf(msg+"\n", args...) }, - ) + bar := printer.NewCounter("packs copied") + bar.SetMax(uint64(len(packList))) + _, err = repository.Repack(ctx, srcRepo, dstRepo, packList, copyBlobs, bar, printer.P) bar.Done() if err != nil { return errors.Fatal(err.Error()) diff --git a/cmd/restic/cmd_copy_integration_test.go b/cmd/restic/cmd_copy_integration_test.go index 9ae78ba50..2dc27050f 100644 --- a/cmd/restic/cmd_copy_integration_test.go +++ b/cmd/restic/cmd_copy_integration_test.go @@ -7,6 +7,7 @@ import ( "testing" rtest "github.com/restic/restic/internal/test" + "github.com/restic/restic/internal/ui" ) func testRunCopy(t testing.TB, srcGopts GlobalOptions, dstGopts GlobalOptions) { @@ -22,7 +23,9 @@ func testRunCopy(t testing.TB, srcGopts GlobalOptions, dstGopts GlobalOptions) { }, } - rtest.OK(t, runCopy(context.TODO(), copyOpts, gopts, nil)) + rtest.OK(t, withTermStatus(gopts, func(ctx context.Context, term ui.Terminal) error { + return runCopy(context.TODO(), copyOpts, gopts, nil, term) + })) } func TestCopy(t *testing.T) { diff --git a/cmd/restic/cmd_debug.go b/cmd/restic/cmd_debug.go index 63ca86da3..8dcc3cc8d 100644 --- a/cmd/restic/cmd_debug.go +++ b/cmd/restic/cmd_debug.go @@ -27,6 +27,8 @@ import ( "github.com/restic/restic/internal/repository/index" "github.com/restic/restic/internal/repository/pack" "github.com/restic/restic/internal/restic" + "github.com/restic/restic/internal/ui" + "github.com/restic/restic/internal/ui/progress" ) func registerDebugCommand(cmd *cobra.Command) { @@ -66,7 +68,9 @@ Exit status is 12 if the password is incorrect. `, DisableAutoGenTag: true, RunE: func(cmd *cobra.Command, args []string) error { - return runDebugDump(cmd.Context(), globalOptions, args) + term, cancel := setupTermstatus() + defer cancel() + return runDebugDump(cmd.Context(), globalOptions, args, term) }, } return cmd @@ -80,7 +84,9 @@ func newDebugExamineCommand() *cobra.Command { Short: "Examine a pack file", DisableAutoGenTag: true, RunE: func(cmd *cobra.Command, args []string) error { - return runDebugExamine(cmd.Context(), globalOptions, opts, args) + term, cancel := setupTermstatus() + defer cancel() + return runDebugExamine(cmd.Context(), globalOptions, opts, args, term) }, } @@ -141,13 +147,13 @@ type Blob struct { Offset uint `json:"offset"` } -func printPacks(ctx context.Context, repo *repository.Repository, wr io.Writer) error { +func printPacks(ctx context.Context, repo *repository.Repository, wr io.Writer, printer progress.Printer) error { var m sync.Mutex return restic.ParallelList(ctx, repo, restic.PackFile, repo.Connections(), func(ctx context.Context, id restic.ID, size int64) error { blobs, _, err := repo.ListPack(ctx, id, size) if err != nil { - Warnf("error for pack %v: %v\n", id.Str(), err) + printer.E("error for pack %v: %v", id.Str(), err) return nil } @@ -170,9 +176,9 @@ func printPacks(ctx context.Context, repo *repository.Repository, wr io.Writer) }) } -func dumpIndexes(ctx context.Context, repo restic.ListerLoaderUnpacked, wr io.Writer) error { +func dumpIndexes(ctx context.Context, repo restic.ListerLoaderUnpacked, wr io.Writer, printer progress.Printer) error { return index.ForAllIndexes(ctx, repo, repo, func(id restic.ID, idx *index.Index, err error) error { - Printf("index_id: %v\n", id) + printer.S("index_id: %v", id) if err != nil { return err } @@ -181,12 +187,14 @@ func dumpIndexes(ctx context.Context, repo restic.ListerLoaderUnpacked, wr io.Wr }) } -func runDebugDump(ctx context.Context, gopts GlobalOptions, args []string) error { +func runDebugDump(ctx context.Context, gopts GlobalOptions, args []string, term ui.Terminal) error { + printer := newTerminalProgressPrinter(false, gopts.verbosity, term) + if len(args) != 1 { return errors.Fatal("type not specified") } - ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock) + ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock, printer) if err != nil { return err } @@ -196,20 +204,20 @@ func runDebugDump(ctx context.Context, gopts GlobalOptions, args []string) error switch tpe { case "indexes": - return dumpIndexes(ctx, repo, globalOptions.stdout) + return dumpIndexes(ctx, repo, globalOptions.stdout, printer) case "snapshots": return debugPrintSnapshots(ctx, repo, globalOptions.stdout) case "packs": - return printPacks(ctx, repo, globalOptions.stdout) + return printPacks(ctx, repo, globalOptions.stdout, printer) case "all": - Printf("snapshots:\n") + printer.S("snapshots:") err := debugPrintSnapshots(ctx, repo, globalOptions.stdout) if err != nil { return err } - Printf("\nindexes:\n") - err = dumpIndexes(ctx, repo, globalOptions.stdout) + printer.S("indexes:") + err = dumpIndexes(ctx, repo, globalOptions.stdout, printer) if err != nil { return err } @@ -220,11 +228,11 @@ func runDebugDump(ctx context.Context, gopts GlobalOptions, args []string) error } } -func tryRepairWithBitflip(key *crypto.Key, input []byte, bytewise bool) []byte { +func tryRepairWithBitflip(key *crypto.Key, input []byte, bytewise bool, printer progress.Printer) []byte { if bytewise { - Printf(" trying to repair blob by finding a broken byte\n") + printer.S(" trying to repair blob by finding a broken byte") } else { - Printf(" trying to repair blob with single bit flip\n") + printer.S(" trying to repair blob with single bit flip") } ch := make(chan int) @@ -234,7 +242,7 @@ func tryRepairWithBitflip(key *crypto.Key, input []byte, bytewise bool) []byte { var found bool workers := runtime.GOMAXPROCS(0) - Printf(" spinning up %d worker functions\n", runtime.GOMAXPROCS(0)) + printer.S(" spinning up %d worker functions", runtime.GOMAXPROCS(0)) for i := 0; i < workers; i++ { wg.Go(func() error { // make a local copy of the buffer @@ -248,9 +256,9 @@ func tryRepairWithBitflip(key *crypto.Key, input []byte, bytewise bool) []byte { nonce, plaintext := buf[:key.NonceSize()], buf[key.NonceSize():] plaintext, err := key.Open(plaintext[:0], nonce, plaintext, nil) if err == nil { - Printf("\n") - Printf(" blob could be repaired by XORing byte %v with 0x%02x\n", idx, pattern) - Printf(" hash is %v\n", restic.Hash(plaintext)) + printer.S("") + printer.S(" blob could be repaired by XORing byte %v with 0x%02x", idx, pattern) + printer.S(" hash is %v", restic.Hash(plaintext)) close(done) found = true fixed = plaintext @@ -291,7 +299,7 @@ func tryRepairWithBitflip(key *crypto.Key, input []byte, bytewise bool) []byte { select { case ch <- i: case <-done: - Printf(" done after %v\n", time.Since(start)) + printer.S(" done after %v", time.Since(start)) return nil } @@ -301,7 +309,7 @@ func tryRepairWithBitflip(key *crypto.Key, input []byte, bytewise bool) []byte { remaining := len(input) - i eta := time.Duration(float64(remaining)/gps) * time.Second - Printf("\r%d byte of %d done (%.2f%%), %.0f byte per second, ETA %v", + printer.S("\r%d byte of %d done (%.2f%%), %.0f byte per second, ETA %v", i, len(input), float32(i)/float32(len(input))*100, gps, eta) info = time.Now() } @@ -314,7 +322,7 @@ func tryRepairWithBitflip(key *crypto.Key, input []byte, bytewise bool) []byte { } if !found { - Printf("\n blob could not be repaired\n") + printer.S("\n blob could not be repaired") } return fixed } @@ -335,7 +343,7 @@ func decryptUnsigned(k *crypto.Key, buf []byte) []byte { return out } -func loadBlobs(ctx context.Context, opts DebugExamineOptions, repo restic.Repository, packID restic.ID, list []restic.Blob) error { +func loadBlobs(ctx context.Context, opts DebugExamineOptions, repo restic.Repository, packID restic.ID, list []restic.Blob, printer progress.Printer) error { dec, err := zstd.NewReader(nil) if err != nil { panic(err) @@ -355,9 +363,9 @@ func loadBlobs(ctx context.Context, opts DebugExamineOptions, repo restic.Reposi wg.Go(func() error { for _, blob := range list { - Printf(" loading blob %v at %v (length %v)\n", blob.ID, blob.Offset, blob.Length) + printer.S(" loading blob %v at %v (length %v)", blob.ID, blob.Offset, blob.Length) if int(blob.Offset+blob.Length) > len(pack) { - Warnf("skipping truncated blob\n") + printer.E("skipping truncated blob") continue } buf := pack[blob.Offset : blob.Offset+blob.Length] @@ -368,16 +376,16 @@ func loadBlobs(ctx context.Context, opts DebugExamineOptions, repo restic.Reposi outputPrefix := "" filePrefix := "" if err != nil { - Warnf("error decrypting blob: %v\n", err) + printer.E("error decrypting blob: %v", err) if opts.TryRepair || opts.RepairByte { - plaintext = tryRepairWithBitflip(key, buf, opts.RepairByte) + plaintext = tryRepairWithBitflip(key, buf, opts.RepairByte, printer) } if plaintext != nil { outputPrefix = "repaired " filePrefix = "repaired-" } else { plaintext = decryptUnsigned(key, buf) - err = storePlainBlob(blob.ID, "damaged-", plaintext) + err = storePlainBlob(blob.ID, "damaged-", plaintext, printer) if err != nil { return err } @@ -388,7 +396,7 @@ func loadBlobs(ctx context.Context, opts DebugExamineOptions, repo restic.Reposi if blob.IsCompressed() { decompressed, err := dec.DecodeAll(plaintext, nil) if err != nil { - Printf(" failed to decompress blob %v\n", blob.ID) + printer.S(" failed to decompress blob %v", blob.ID) } if decompressed != nil { plaintext = decompressed @@ -398,14 +406,14 @@ func loadBlobs(ctx context.Context, opts DebugExamineOptions, repo restic.Reposi id := restic.Hash(plaintext) var prefix string if !id.Equal(blob.ID) { - Printf(" successfully %vdecrypted blob (length %v), hash is %v, ID does not match, wanted %v\n", outputPrefix, len(plaintext), id, blob.ID) + printer.S(" successfully %vdecrypted blob (length %v), hash is %v, ID does not match, wanted %v", outputPrefix, len(plaintext), id, blob.ID) prefix = "wrong-hash-" } else { - Printf(" successfully %vdecrypted blob (length %v), hash is %v, ID matches\n", outputPrefix, len(plaintext), id) + printer.S(" successfully %vdecrypted blob (length %v), hash is %v, ID matches", outputPrefix, len(plaintext), id) prefix = "correct-" } if opts.ExtractPack { - err = storePlainBlob(id, filePrefix+prefix, plaintext) + err = storePlainBlob(id, filePrefix+prefix, plaintext, printer) if err != nil { return err } @@ -415,7 +423,7 @@ func loadBlobs(ctx context.Context, opts DebugExamineOptions, repo restic.Reposi if err != nil { return err } - Printf(" uploaded %v %v\n", blob.Type, id) + printer.S(" uploaded %v %v", blob.Type, id) } } @@ -428,7 +436,7 @@ func loadBlobs(ctx context.Context, opts DebugExamineOptions, repo restic.Reposi return wg.Wait() } -func storePlainBlob(id restic.ID, prefix string, plain []byte) error { +func storePlainBlob(id restic.ID, prefix string, plain []byte, printer progress.Printer) error { filename := fmt.Sprintf("%s%s.bin", prefix, id) f, err := os.Create(filename) if err != nil { @@ -446,16 +454,18 @@ func storePlainBlob(id restic.ID, prefix string, plain []byte) error { return err } - Printf("decrypt of blob %v stored at %v\n", id, filename) + printer.S("decrypt of blob %v stored at %v", id, filename) return nil } -func runDebugExamine(ctx context.Context, gopts GlobalOptions, opts DebugExamineOptions, args []string) error { +func runDebugExamine(ctx context.Context, gopts GlobalOptions, opts DebugExamineOptions, args []string, term ui.Terminal) error { + printer := newTerminalProgressPrinter(false, gopts.verbosity, term) + if opts.ExtractPack && gopts.NoLock { return fmt.Errorf("--extract-pack and --no-lock are mutually exclusive") } - ctx, repo, unlock, err := openWithAppendLock(ctx, gopts, gopts.NoLock) + ctx, repo, unlock, err := openWithAppendLock(ctx, gopts, gopts.NoLock, printer) if err != nil { return err } @@ -467,7 +477,7 @@ func runDebugExamine(ctx context.Context, gopts GlobalOptions, opts DebugExamine if err != nil { id, err = restic.Find(ctx, repo, restic.PackFile, name) if err != nil { - Warnf("error: %v\n", err) + printer.E("error: %v", err) continue } } @@ -478,16 +488,16 @@ func runDebugExamine(ctx context.Context, gopts GlobalOptions, opts DebugExamine return errors.Fatal("no pack files to examine") } - bar := newIndexProgress(gopts.Quiet, gopts.JSON) + bar := newIndexTerminalProgress(printer) err = repo.LoadIndex(ctx, bar) if err != nil { return err } for _, id := range ids { - err := examinePack(ctx, opts, repo, id) + err := examinePack(ctx, opts, repo, id, printer) if err != nil { - Warnf("error: %v\n", err) + printer.E("error: %v", err) } if err == context.Canceled { break @@ -496,24 +506,24 @@ func runDebugExamine(ctx context.Context, gopts GlobalOptions, opts DebugExamine return nil } -func examinePack(ctx context.Context, opts DebugExamineOptions, repo restic.Repository, id restic.ID) error { - Printf("examine %v\n", id) +func examinePack(ctx context.Context, opts DebugExamineOptions, repo restic.Repository, id restic.ID, printer progress.Printer) error { + printer.S("examine %v", id) buf, err := repo.LoadRaw(ctx, restic.PackFile, id) // also process damaged pack files if buf == nil { return err } - Printf(" file size is %v\n", len(buf)) + printer.S(" file size is %v", len(buf)) gotID := restic.Hash(buf) if !id.Equal(gotID) { - Printf(" wanted hash %v, got %v\n", id, gotID) + printer.S(" wanted hash %v, got %v", id, gotID) } else { - Printf(" hash for file content matches\n") + printer.S(" hash for file content matches") } - Printf(" ========================================\n") - Printf(" looking for info in the indexes\n") + printer.S(" ========================================") + printer.S(" looking for info in the indexes") blobsLoaded := false // examine all data the indexes have for the pack file @@ -523,32 +533,32 @@ func examinePack(ctx context.Context, opts DebugExamineOptions, repo restic.Repo continue } - checkPackSize(blobs, len(buf)) + checkPackSize(blobs, len(buf), printer) - err = loadBlobs(ctx, opts, repo, id, blobs) + err = loadBlobs(ctx, opts, repo, id, blobs, printer) if err != nil { - Warnf("error: %v\n", err) + printer.E("error: %v", err) } else { blobsLoaded = true } } - Printf(" ========================================\n") - Printf(" inspect the pack itself\n") + printer.S(" ========================================") + printer.S(" inspect the pack itself") blobs, _, err := repo.ListPack(ctx, id, int64(len(buf))) if err != nil { return fmt.Errorf("pack %v: %v", id.Str(), err) } - checkPackSize(blobs, len(buf)) + checkPackSize(blobs, len(buf), printer) if !blobsLoaded { - return loadBlobs(ctx, opts, repo, id, blobs) + return loadBlobs(ctx, opts, repo, id, blobs, printer) } return nil } -func checkPackSize(blobs []restic.Blob, fileSize int) { +func checkPackSize(blobs []restic.Blob, fileSize int, printer progress.Printer) { // track current size and offset var size, offset uint64 @@ -557,9 +567,9 @@ func checkPackSize(blobs []restic.Blob, fileSize int) { }) for _, pb := range blobs { - Printf(" %v blob %v, offset %-6d, raw length %-6d\n", pb.Type, pb.ID, pb.Offset, pb.Length) + printer.S(" %v blob %v, offset %-6d, raw length %-6d", pb.Type, pb.ID, pb.Offset, pb.Length) if offset != uint64(pb.Offset) { - Printf(" hole in file, want offset %v, got %v\n", offset, pb.Offset) + printer.S(" hole in file, want offset %v, got %v", offset, pb.Offset) } offset = uint64(pb.Offset + pb.Length) size += uint64(pb.Length) @@ -567,8 +577,8 @@ func checkPackSize(blobs []restic.Blob, fileSize int) { size += uint64(pack.CalculateHeaderSize(blobs)) if uint64(fileSize) != size { - Printf(" file sizes do not match: computed %v, file size is %v\n", size, fileSize) + printer.S(" file sizes do not match: computed %v, file size is %v", size, fileSize) } else { - Printf(" file sizes match\n") + printer.S(" file sizes match") } } diff --git a/cmd/restic/cmd_diff.go b/cmd/restic/cmd_diff.go index e065ba4b6..8560a35c8 100644 --- a/cmd/restic/cmd_diff.go +++ b/cmd/restic/cmd_diff.go @@ -52,7 +52,9 @@ Exit status is 12 if the password is incorrect. GroupID: cmdGroupDefault, DisableAutoGenTag: true, RunE: func(cmd *cobra.Command, args []string) error { - return runDiff(cmd.Context(), opts, globalOptions, args) + term, cancel := setupTermstatus() + defer cancel() + return runDiff(cmd.Context(), opts, globalOptions, args, term) }, } @@ -82,6 +84,7 @@ type Comparer struct { repo restic.BlobLoader opts DiffOptions printChange func(change *Change) + printError func(string, ...interface{}) } type Change struct { @@ -155,7 +158,7 @@ type DiffStatsContainer struct { } // updateBlobs updates the blob counters in the stats struct. -func updateBlobs(repo restic.Loader, blobs restic.BlobSet, stats *DiffStat) { +func updateBlobs(repo restic.Loader, blobs restic.BlobSet, stats *DiffStat, printError func(string, ...interface{})) { for h := range blobs { switch h.Type { case restic.DataBlob: @@ -166,7 +169,7 @@ func updateBlobs(repo restic.Loader, blobs restic.BlobSet, stats *DiffStat) { size, found := repo.LookupBlobSize(h.Type, h.ID) if !found { - Warnf("unable to find blob size for %v\n", h) + printError("unable to find blob size for %v", h) continue } @@ -197,7 +200,7 @@ func (c *Comparer) printDir(ctx context.Context, mode string, stats *DiffStat, b if node.Type == restic.NodeTypeDir { err := c.printDir(ctx, mode, stats, blobs, name, *node.Subtree) if err != nil && err != context.Canceled { - Warnf("error: %v\n", err) + c.printError("error: %v", err) } } } @@ -222,7 +225,7 @@ func (c *Comparer) collectDir(ctx context.Context, blobs restic.BlobSet, id rest if node.Type == restic.NodeTypeDir { err := c.collectDir(ctx, blobs, *node.Subtree) if err != nil && err != context.Canceled { - Warnf("error: %v\n", err) + c.printError("error: %v", err) } } } @@ -322,7 +325,7 @@ func (c *Comparer) diffTree(ctx context.Context, stats *DiffStatsContainer, pref err = c.diffTree(ctx, stats, name, *node1.Subtree, *node2.Subtree) } if err != nil && err != context.Canceled { - Warnf("error: %v\n", err) + c.printError("error: %v", err) } } case t1 && !t2: @@ -336,7 +339,7 @@ func (c *Comparer) diffTree(ctx context.Context, stats *DiffStatsContainer, pref if node1.Type == restic.NodeTypeDir { err := c.printDir(ctx, "-", &stats.Removed, stats.BlobsBefore, prefix, *node1.Subtree) if err != nil && err != context.Canceled { - Warnf("error: %v\n", err) + c.printError("error: %v", err) } } case !t1 && t2: @@ -350,7 +353,7 @@ func (c *Comparer) diffTree(ctx context.Context, stats *DiffStatsContainer, pref if node2.Type == restic.NodeTypeDir { err := c.printDir(ctx, "+", &stats.Added, stats.BlobsAfter, prefix, *node2.Subtree) if err != nil && err != context.Canceled { - Warnf("error: %v\n", err) + c.printError("error: %v", err) } } } @@ -359,12 +362,14 @@ func (c *Comparer) diffTree(ctx context.Context, stats *DiffStatsContainer, pref return ctx.Err() } -func runDiff(ctx context.Context, opts DiffOptions, gopts GlobalOptions, args []string) error { +func runDiff(ctx context.Context, opts DiffOptions, gopts GlobalOptions, args []string, term ui.Terminal) error { if len(args) != 2 { return errors.Fatalf("specify two snapshot IDs") } - ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock) + printer := newTerminalProgressPrinter(gopts.JSON, gopts.verbosity, term) + + ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock, printer) if err != nil { return err } @@ -386,9 +391,9 @@ func runDiff(ctx context.Context, opts DiffOptions, gopts GlobalOptions, args [] } if !gopts.JSON { - Verbosef("comparing snapshot %v to %v:\n\n", sn1.ID().Str(), sn2.ID().Str()) + printer.P("comparing snapshot %v to %v:\n\n", sn1.ID().Str(), sn2.ID().Str()) } - bar := newIndexProgress(gopts.Quiet, gopts.JSON) + bar := newIndexTerminalProgress(printer) if err = repo.LoadIndex(ctx, bar); err != nil { return err } @@ -412,10 +417,11 @@ func runDiff(ctx context.Context, opts DiffOptions, gopts GlobalOptions, args [] } c := &Comparer{ - repo: repo, - opts: opts, + repo: repo, + opts: opts, + printError: printer.E, printChange: func(change *Change) { - Printf("%-5s%v\n", change.Modifier, change.Path) + printer.S("%-5s%v", change.Modifier, change.Path) }, } @@ -424,7 +430,7 @@ func runDiff(ctx context.Context, opts DiffOptions, gopts GlobalOptions, args [] c.printChange = func(change *Change) { err := enc.Encode(change) if err != nil { - Warnf("JSON encode failed: %v\n", err) + printer.E("JSON encode failed: %v", err) } } } @@ -450,23 +456,23 @@ func runDiff(ctx context.Context, opts DiffOptions, gopts GlobalOptions, args [] } both := stats.BlobsBefore.Intersect(stats.BlobsAfter) - updateBlobs(repo, stats.BlobsBefore.Sub(both).Sub(stats.BlobsCommon), &stats.Removed) - updateBlobs(repo, stats.BlobsAfter.Sub(both).Sub(stats.BlobsCommon), &stats.Added) + updateBlobs(repo, stats.BlobsBefore.Sub(both).Sub(stats.BlobsCommon), &stats.Removed, printer.E) + updateBlobs(repo, stats.BlobsAfter.Sub(both).Sub(stats.BlobsCommon), &stats.Added, printer.E) if gopts.JSON { err := json.NewEncoder(globalOptions.stdout).Encode(stats) if err != nil { - Warnf("JSON encode failed: %v\n", err) + printer.E("JSON encode failed: %v", err) } } else { - Printf("\n") - Printf("Files: %5d new, %5d removed, %5d changed\n", stats.Added.Files, stats.Removed.Files, stats.ChangedFiles) - Printf("Dirs: %5d new, %5d removed\n", stats.Added.Dirs, stats.Removed.Dirs) - Printf("Others: %5d new, %5d removed\n", stats.Added.Others, stats.Removed.Others) - Printf("Data Blobs: %5d new, %5d removed\n", stats.Added.DataBlobs, stats.Removed.DataBlobs) - Printf("Tree Blobs: %5d new, %5d removed\n", stats.Added.TreeBlobs, stats.Removed.TreeBlobs) - Printf(" Added: %-5s\n", ui.FormatBytes(stats.Added.Bytes)) - Printf(" Removed: %-5s\n", ui.FormatBytes(stats.Removed.Bytes)) + printer.S("") + printer.S("Files: %5d new, %5d removed, %5d changed", stats.Added.Files, stats.Removed.Files, stats.ChangedFiles) + printer.S("Dirs: %5d new, %5d removed", stats.Added.Dirs, stats.Removed.Dirs) + printer.S("Others: %5d new, %5d removed", stats.Added.Others, stats.Removed.Others) + printer.S("Data Blobs: %5d new, %5d removed", stats.Added.DataBlobs, stats.Removed.DataBlobs) + printer.S("Tree Blobs: %5d new, %5d removed", stats.Added.TreeBlobs, stats.Removed.TreeBlobs) + printer.S(" Added: %-5s", ui.FormatBytes(stats.Added.Bytes)) + printer.S(" Removed: %-5s", ui.FormatBytes(stats.Removed.Bytes)) } return nil diff --git a/cmd/restic/cmd_diff_integration_test.go b/cmd/restic/cmd_diff_integration_test.go index 8782053ed..ee20671c4 100644 --- a/cmd/restic/cmd_diff_integration_test.go +++ b/cmd/restic/cmd_diff_integration_test.go @@ -12,14 +12,17 @@ import ( "testing" rtest "github.com/restic/restic/internal/test" + "github.com/restic/restic/internal/ui" ) func testRunDiffOutput(gopts GlobalOptions, firstSnapshotID string, secondSnapshotID string) (string, error) { - buf, err := withCaptureStdout(func() error { + buf, err := withCaptureStdout(gopts, func(gopts GlobalOptions) error { opts := DiffOptions{ ShowMetadata: false, } - return runDiff(context.TODO(), opts, gopts, []string{firstSnapshotID, secondSnapshotID}) + return withTermStatus(gopts, func(ctx context.Context, term ui.Terminal) error { + return runDiff(ctx, opts, gopts, []string{firstSnapshotID, secondSnapshotID}, term) + }) }) return buf.String(), err } diff --git a/cmd/restic/cmd_dump.go b/cmd/restic/cmd_dump.go index 921309101..6a2a2cce8 100644 --- a/cmd/restic/cmd_dump.go +++ b/cmd/restic/cmd_dump.go @@ -12,6 +12,7 @@ import ( "github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/restic" "github.com/restic/restic/internal/terminal" + "github.com/restic/restic/internal/ui" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -47,7 +48,9 @@ Exit status is 12 if the password is incorrect. GroupID: cmdGroupDefault, DisableAutoGenTag: true, RunE: func(cmd *cobra.Command, args []string) error { - return runDump(cmd.Context(), opts, globalOptions, args) + term, cancel := setupTermstatus() + defer cancel() + return runDump(cmd.Context(), opts, globalOptions, args, term) }, } @@ -125,11 +128,13 @@ func printFromTree(ctx context.Context, tree *restic.Tree, repo restic.BlobLoade return fmt.Errorf("path %q not found in snapshot", item) } -func runDump(ctx context.Context, opts DumpOptions, gopts GlobalOptions, args []string) error { +func runDump(ctx context.Context, opts DumpOptions, gopts GlobalOptions, args []string, term ui.Terminal) error { if len(args) != 2 { return errors.Fatal("no file and no snapshot ID specified") } + printer := newTerminalProgressPrinter(gopts.JSON, gopts.verbosity, term) + switch opts.Archive { case "tar", "zip": default: @@ -143,7 +148,7 @@ func runDump(ctx context.Context, opts DumpOptions, gopts GlobalOptions, args [] splittedPath := splitPath(path.Clean(pathToPrint)) - ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock) + ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock, printer) if err != nil { return err } @@ -158,7 +163,7 @@ func runDump(ctx context.Context, opts DumpOptions, gopts GlobalOptions, args [] return errors.Fatalf("failed to find snapshot: %v", err) } - bar := newIndexProgress(gopts.Quiet, gopts.JSON) + bar := newIndexTerminalProgress(printer) err = repo.LoadIndex(ctx, bar) if err != nil { return err @@ -174,7 +179,7 @@ func runDump(ctx context.Context, opts DumpOptions, gopts GlobalOptions, args [] return errors.Fatalf("loading tree for snapshot %q failed: %v", snapshotIDString, err) } - outputFileWriter := os.Stdout + outputFileWriter := term.OutputRaw() canWriteArchiveFunc := checkStdoutArchive if opts.Target != "" { diff --git a/cmd/restic/cmd_find.go b/cmd/restic/cmd_find.go index d2fa3619a..705d12648 100644 --- a/cmd/restic/cmd_find.go +++ b/cmd/restic/cmd_find.go @@ -3,6 +3,8 @@ package main import ( "context" "encoding/json" + "fmt" + "io" "sort" "strings" "time" @@ -14,6 +16,7 @@ import ( "github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/filter" "github.com/restic/restic/internal/restic" + "github.com/restic/restic/internal/ui" "github.com/restic/restic/internal/walker" ) @@ -48,7 +51,9 @@ Exit status is 12 if the password is incorrect. GroupID: cmdGroupDefault, DisableAutoGenTag: true, RunE: func(cmd *cobra.Command, args []string) error { - return runFind(cmd.Context(), opts, globalOptions, args) + term, cancel := setupTermstatus() + defer cancel() + return runFind(cmd.Context(), opts, globalOptions, args, term) }, } @@ -124,6 +129,12 @@ type statefulOutput struct { newsn *restic.Snapshot oldsn *restic.Snapshot hits int + printer interface { + S(string, ...interface{}) + P(string, ...interface{}) + E(string, ...interface{}) + } + stdout io.Writer } func (s *statefulOutput) PrintPatternJSON(path string, node *restic.Node) { @@ -148,37 +159,37 @@ func (s *statefulOutput) PrintPatternJSON(path string, node *restic.Node) { findNode: (*findNode)(node), }) if err != nil { - Warnf("Marshall failed: %v\n", err) + s.printer.E("Marshall failed: %v", err) return } if !s.inuse { - Printf("[") + _, _ = s.stdout.Write([]byte("[")) s.inuse = true } if s.newsn != s.oldsn { if s.oldsn != nil { - Printf("],\"hits\":%d,\"snapshot\":%q},", s.hits, s.oldsn.ID()) + _, _ = s.stdout.Write([]byte(fmt.Sprintf("],\"hits\":%d,\"snapshot\":%q},", s.hits, s.oldsn.ID()))) } - Printf(`{"matches":[`) + _, _ = s.stdout.Write([]byte(`{"matches":[`)) s.oldsn = s.newsn s.hits = 0 } if s.hits > 0 { - Printf(",") + _, _ = s.stdout.Write([]byte(",")) } - Print(string(b)) + _, _ = s.stdout.Write(b) s.hits++ } func (s *statefulOutput) PrintPatternNormal(path string, node *restic.Node) { if s.newsn != s.oldsn { if s.oldsn != nil { - Verbosef("\n") + s.printer.P("") } s.oldsn = s.newsn - Verbosef("Found matching entries in snapshot %s from %s\n", s.oldsn.ID().Str(), s.oldsn.Time.Local().Format(TimeFormat)) + s.printer.P("Found matching entries in snapshot %s from %s", s.oldsn.ID().Str(), s.oldsn.Time.Local().Format(TimeFormat)) } - Println(formatNode(path, node, s.ListLong, s.HumanReadable)) + s.printer.S(formatNode(path, node, s.ListLong, s.HumanReadable)) } func (s *statefulOutput) PrintPattern(path string, node *restic.Node) { @@ -207,29 +218,29 @@ func (s *statefulOutput) PrintObjectJSON(kind, id, nodepath, treeID string, sn * Time: sn.Time, }) if err != nil { - Warnf("Marshall failed: %v\n", err) + s.printer.E("Marshall failed: %v", err) return } if !s.inuse { - Printf("[") + _, _ = s.stdout.Write([]byte("[")) s.inuse = true } if s.hits > 0 { - Printf(",") + _, _ = s.stdout.Write([]byte(",")) } - Print(string(b)) + _, _ = s.stdout.Write(b) s.hits++ } func (s *statefulOutput) PrintObjectNormal(kind, id, nodepath, treeID string, sn *restic.Snapshot) { - Printf("Found %s %s\n", kind, id) + s.printer.S("Found %s %s", kind, id) if kind == "blob" { - Printf(" ... in file %s\n", nodepath) - Printf(" (tree %s)\n", treeID) + s.printer.S(" ... in file %s", nodepath) + s.printer.S(" (tree %s)", treeID) } else { - Printf(" ... path %s\n", nodepath) + s.printer.S(" ... path %s", nodepath) } - Printf(" ... in snapshot %s (%s)\n", sn.ID().Str(), sn.Time.Local().Format(TimeFormat)) + s.printer.S(" ... in snapshot %s (%s)", sn.ID().Str(), sn.Time.Local().Format(TimeFormat)) } func (s *statefulOutput) PrintObject(kind, id, nodepath, treeID string, sn *restic.Snapshot) { @@ -244,12 +255,12 @@ func (s *statefulOutput) Finish() { if s.JSON { // do some finishing up if s.oldsn != nil { - Printf("],\"hits\":%d,\"snapshot\":%q}", s.hits, s.oldsn.ID()) + _, _ = s.stdout.Write([]byte(fmt.Sprintf("],\"hits\":%d,\"snapshot\":%q}", s.hits, s.oldsn.ID()))) } if s.inuse { - Printf("]\n") + _, _ = s.stdout.Write([]byte("]\n")) } else { - Printf("[]\n") + _, _ = s.stdout.Write([]byte("[]\n")) } return } @@ -263,6 +274,11 @@ type Finder struct { blobIDs map[string]struct{} treeIDs map[string]struct{} itemsFound int + printer interface { + S(string, ...interface{}) + P(string, ...interface{}) + E(string, ...interface{}) + } } func (f *Finder) findInSnapshot(ctx context.Context, sn *restic.Snapshot) error { @@ -277,7 +293,8 @@ func (f *Finder) findInSnapshot(ctx context.Context, sn *restic.Snapshot) error if err != nil { debug.Log("Error loading tree %v: %v", parentTreeID, err) - Printf("Unable to load tree %s\n ... which belongs to snapshot %s\n", parentTreeID, sn.ID()) + f.printer.S("Unable to load tree %s", parentTreeID) + f.printer.S(" ... which belongs to snapshot %s", sn.ID()) return walker.ErrSkipNode } @@ -375,7 +392,8 @@ func (f *Finder) findIDs(ctx context.Context, sn *restic.Snapshot) error { if err != nil { debug.Log("Error loading tree %v: %v", parentTreeID, err) - Printf("Unable to load tree %s\n ... which belongs to snapshot %s\n", parentTreeID, sn.ID()) + f.printer.S("Unable to load tree %s", parentTreeID) + f.printer.S(" ... which belongs to snapshot %s", sn.ID()) return walker.ErrSkipNode } @@ -524,7 +542,7 @@ func (f *Finder) indexPacksToBlobs(ctx context.Context, packIDs map[string]struc for h := range indexPackIDs { list = append(list, h) } - Warnf("some pack files are missing from the repository, getting their blobs from the repository index: %v\n\n", list) + f.printer.E("some pack files are missing from the repository, getting their blobs from the repository index: %v\n\n", list) } return packIDs, nil } @@ -532,19 +550,20 @@ func (f *Finder) indexPacksToBlobs(ctx context.Context, packIDs map[string]struc func (f *Finder) findObjectPack(id string, t restic.BlobType) { rid, err := restic.ParseID(id) if err != nil { - Printf("Note: cannot find pack for object '%s', unable to parse ID: %v\n", id, err) + f.printer.S("Note: cannot find pack for object '%s', unable to parse ID: %v", id, err) return } blobs := f.repo.LookupBlob(t, rid) if len(blobs) == 0 { - Printf("Object %s not found in the index\n", rid.Str()) + f.printer.S("Object %s not found in the index", rid.Str()) return } for _, b := range blobs { if b.ID.Equal(rid) { - Printf("Object belongs to pack %s\n ... Pack %s: %s\n", b.PackID, b.PackID.Str(), b.String()) + f.printer.S("Object belongs to pack %s", b.PackID) + f.printer.S(" ... Pack %s: %s", b.PackID.Str(), b.String()) break } } @@ -560,11 +579,13 @@ func (f *Finder) findObjectsPacks() { } } -func runFind(ctx context.Context, opts FindOptions, gopts GlobalOptions, args []string) error { +func runFind(ctx context.Context, opts FindOptions, gopts GlobalOptions, args []string, term ui.Terminal) error { if len(args) == 0 { return errors.Fatal("wrong number of arguments") } + printer := newTerminalProgressPrinter(gopts.JSON, gopts.verbosity, term) + var err error pat := findPattern{pattern: args} if opts.CaseInsensitive { @@ -594,7 +615,7 @@ func runFind(ctx context.Context, opts FindOptions, gopts GlobalOptions, args [] return errors.Fatal("cannot have several ID types") } - ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock) + ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock, printer) if err != nil { return err } @@ -604,15 +625,16 @@ func runFind(ctx context.Context, opts FindOptions, gopts GlobalOptions, args [] if err != nil { return err } - bar := newIndexProgress(gopts.Quiet, gopts.JSON) + bar := newIndexTerminalProgress(printer) if err = repo.LoadIndex(ctx, bar); err != nil { return err } f := &Finder{ - repo: repo, - pat: pat, - out: statefulOutput{ListLong: opts.ListLong, HumanReadable: opts.HumanReadable, JSON: gopts.JSON}, + repo: repo, + pat: pat, + out: statefulOutput{ListLong: opts.ListLong, HumanReadable: opts.HumanReadable, JSON: gopts.JSON, printer: printer, stdout: term.OutputRaw()}, + printer: printer, } if opts.BlobID { @@ -636,7 +658,7 @@ func runFind(ctx context.Context, opts FindOptions, gopts GlobalOptions, args [] } var filteredSnapshots []*restic.Snapshot - for sn := range FindFilteredSnapshots(ctx, snapshotLister, repo, &opts.SnapshotFilter, opts.Snapshots) { + for sn := range FindFilteredSnapshots(ctx, snapshotLister, repo, &opts.SnapshotFilter, opts.Snapshots, printer) { filteredSnapshots = append(filteredSnapshots, sn) } if ctx.Err() != nil { diff --git a/cmd/restic/cmd_find_integration_test.go b/cmd/restic/cmd_find_integration_test.go index 95799749a..ad34923ed 100644 --- a/cmd/restic/cmd_find_integration_test.go +++ b/cmd/restic/cmd_find_integration_test.go @@ -8,13 +8,16 @@ import ( "time" rtest "github.com/restic/restic/internal/test" + "github.com/restic/restic/internal/ui" ) func testRunFind(t testing.TB, wantJSON bool, opts FindOptions, gopts GlobalOptions, pattern string) []byte { - buf, err := withCaptureStdout(func() error { + buf, err := withCaptureStdout(gopts, func(gopts GlobalOptions) error { gopts.JSON = wantJSON - return runFind(context.TODO(), opts, gopts, []string{pattern}) + return withTermStatus(gopts, func(ctx context.Context, term ui.Terminal) error { + return runFind(ctx, opts, gopts, []string{pattern}, term) + }) }) rtest.OK(t, err) return buf.Bytes() @@ -95,7 +98,7 @@ func TestFindSorting(t *testing.T) { env, cleanup := withTestEnvironment(t) defer cleanup() - datafile := testSetupBackupData(t, env) + testSetupBackupData(t, env) opts := BackupOptions{} // first backup @@ -114,14 +117,14 @@ func TestFindSorting(t *testing.T) { // first restic find - with default FindOptions{} results := testRunFind(t, true, FindOptions{}, env.gopts, "testfile") lines := strings.Split(string(results), "\n") - rtest.Assert(t, len(lines) == 2, "expected two files found in repo (%v), found %d", datafile, len(lines)) + rtest.Assert(t, len(lines) == 2, "expected two lines of output, found %d", len(lines)) matches := []testMatches{} rtest.OK(t, json.Unmarshal(results, &matches)) // run second restic find with --reverse, sort oldest to newest resultsReverse := testRunFind(t, true, FindOptions{Reverse: true}, env.gopts, "testfile") lines = strings.Split(string(resultsReverse), "\n") - rtest.Assert(t, len(lines) == 2, "expected two files found in repo (%v), found %d", datafile, len(lines)) + rtest.Assert(t, len(lines) == 2, "expected two lines of output, found %d", len(lines)) matchesReverse := []testMatches{} rtest.OK(t, json.Unmarshal(resultsReverse, &matchesReverse)) diff --git a/cmd/restic/cmd_forget.go b/cmd/restic/cmd_forget.go index bef00ffa1..8523b3b2d 100644 --- a/cmd/restic/cmd_forget.go +++ b/cmd/restic/cmd_forget.go @@ -9,7 +9,7 @@ import ( "github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/restic" - "github.com/restic/restic/internal/ui/termstatus" + "github.com/restic/restic/internal/ui" "github.com/spf13/cobra" "github.com/spf13/pflag" ) @@ -173,7 +173,7 @@ func verifyForgetOptions(opts *ForgetOptions) error { return nil } -func runForget(ctx context.Context, opts ForgetOptions, pruneOptions PruneOptions, gopts GlobalOptions, term *termstatus.Terminal, args []string) error { +func runForget(ctx context.Context, opts ForgetOptions, pruneOptions PruneOptions, gopts GlobalOptions, term ui.Terminal, args []string) error { err := verifyForgetOptions(&opts) if err != nil { return err @@ -188,22 +188,17 @@ func runForget(ctx context.Context, opts ForgetOptions, pruneOptions PruneOption return errors.Fatal("--no-lock is only applicable in combination with --dry-run for forget command") } - ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, opts.DryRun && gopts.NoLock) + printer := newTerminalProgressPrinter(gopts.JSON, gopts.verbosity, term) + ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, opts.DryRun && gopts.NoLock, printer) if err != nil { return err } defer unlock() - verbosity := gopts.verbosity - if gopts.JSON { - verbosity = 0 - } - printer := newTerminalProgressPrinter(verbosity, term) - var snapshots restic.Snapshots removeSnIDs := restic.NewIDSet() - for sn := range FindFilteredSnapshots(ctx, repo, repo, &opts.SnapshotFilter, args) { + for sn := range FindFilteredSnapshots(ctx, repo, repo, &opts.SnapshotFilter, args, printer) { snapshots = append(snapshots, sn) } if ctx.Err() != nil { @@ -281,14 +276,18 @@ func runForget(ctx context.Context, opts ForgetOptions, pruneOptions PruneOption } if len(keep) != 0 && !gopts.Quiet && !gopts.JSON { printer.P("keep %d snapshots:\n", len(keep)) - PrintSnapshots(globalOptions.stdout, keep, reasons, opts.Compact) + if err := PrintSnapshots(globalOptions.stdout, keep, reasons, opts.Compact); err != nil { + return err + } printer.P("\n") } fg.Keep = asJSONSnapshots(keep) if len(remove) != 0 && !gopts.Quiet && !gopts.JSON { printer.P("remove %d snapshots:\n", len(remove)) - PrintSnapshots(globalOptions.stdout, remove, nil, opts.Compact) + if err := PrintSnapshots(globalOptions.stdout, remove, nil, opts.Compact); err != nil { + return err + } printer.P("\n") } fg.Remove = asJSONSnapshots(remove) @@ -348,7 +347,7 @@ func runForget(ctx context.Context, opts ForgetOptions, pruneOptions PruneOption printer.P("%d snapshots have been removed, running prune\n", len(removeSnIDs)) } pruneOptions.DryRun = opts.DryRun - return runPruneWithRepo(ctx, pruneOptions, gopts, repo, removeSnIDs, term) + return runPruneWithRepo(ctx, pruneOptions, repo, removeSnIDs, printer) } return nil diff --git a/cmd/restic/cmd_forget_integration_test.go b/cmd/restic/cmd_forget_integration_test.go index 96dd7c63e..d3be8a60d 100644 --- a/cmd/restic/cmd_forget_integration_test.go +++ b/cmd/restic/cmd_forget_integration_test.go @@ -8,14 +8,14 @@ import ( "github.com/restic/restic/internal/restic" rtest "github.com/restic/restic/internal/test" - "github.com/restic/restic/internal/ui/termstatus" + "github.com/restic/restic/internal/ui" ) func testRunForgetMayFail(gopts GlobalOptions, opts ForgetOptions, args ...string) error { pruneOpts := PruneOptions{ MaxUnused: "5%", } - return withTermStatus(gopts, func(ctx context.Context, term *termstatus.Terminal) error { + return withTermStatus(gopts, func(ctx context.Context, term ui.Terminal) error { return runForget(context.TODO(), opts, pruneOpts, gopts, term, args) }) } diff --git a/cmd/restic/cmd_generate.go b/cmd/restic/cmd_generate.go index 3c5ddffd5..cf7b3c1ea 100644 --- a/cmd/restic/cmd_generate.go +++ b/cmd/restic/cmd_generate.go @@ -7,6 +7,8 @@ import ( "github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/terminal" + "github.com/restic/restic/internal/ui" + "github.com/restic/restic/internal/ui/progress" "github.com/spf13/cobra" "github.com/spf13/cobra/doc" "github.com/spf13/pflag" @@ -30,7 +32,9 @@ Exit status is 1 if there was any error. `, DisableAutoGenTag: true, RunE: func(_ *cobra.Command, args []string) error { - return runGenerate(opts, args) + term, cancel := setupTermstatus() + defer cancel() + return runGenerate(opts, globalOptions, args, term) }, } opts.AddFlags(cmd.Flags()) @@ -53,7 +57,7 @@ func (opts *generateOptions) AddFlags(f *pflag.FlagSet) { f.StringVar(&opts.PowerShellCompletionFile, "powershell-completion", "", "write powershell completion `file` (`-` for stdout)") } -func writeManpages(root *cobra.Command, dir string) error { +func writeManpages(root *cobra.Command, dir string, printer progress.Printer) error { // use a fixed date for the man pages so that generating them is deterministic date, err := time.Parse("Jan 2006", "Jan 2017") if err != nil { @@ -67,13 +71,13 @@ func writeManpages(root *cobra.Command, dir string) error { Date: &date, } - Verbosef("writing man pages to directory %v\n", dir) + printer.P("writing man pages to directory %v", dir) return doc.GenManTree(root, header, dir) } -func writeCompletion(filename string, shell string, generate func(w io.Writer) error) (err error) { +func writeCompletion(filename string, shell string, generate func(w io.Writer) error, printer progress.Printer) (err error) { if terminal.StdoutIsTerminal() { - Verbosef("writing %s completion file to %v\n", shell, filename) + printer.P("writing %s completion file to %v", shell, filename) } var outWriter io.Writer if filename != "-" { @@ -111,15 +115,16 @@ func checkStdoutForSingleShell(opts generateOptions) error { return nil } -func runGenerate(opts generateOptions, args []string) error { +func runGenerate(opts generateOptions, gopts GlobalOptions, args []string, term ui.Terminal) error { if len(args) > 0 { return errors.Fatal("the generate command expects no arguments, only options - please see `restic help generate` for usage and flags") } + printer := newTerminalProgressPrinter(gopts.JSON, gopts.verbosity, term) cmdRoot := newRootCommand() if opts.ManDir != "" { - err := writeManpages(cmdRoot, opts.ManDir) + err := writeManpages(cmdRoot, opts.ManDir, printer) if err != nil { return err } @@ -131,28 +136,28 @@ func runGenerate(opts generateOptions, args []string) error { } if opts.BashCompletionFile != "" { - err := writeCompletion(opts.BashCompletionFile, "bash", cmdRoot.GenBashCompletion) + err := writeCompletion(opts.BashCompletionFile, "bash", cmdRoot.GenBashCompletion, printer) if err != nil { return err } } if opts.FishCompletionFile != "" { - err := writeCompletion(opts.FishCompletionFile, "fish", func(w io.Writer) error { return cmdRoot.GenFishCompletion(w, true) }) + err := writeCompletion(opts.FishCompletionFile, "fish", func(w io.Writer) error { return cmdRoot.GenFishCompletion(w, true) }, printer) if err != nil { return err } } if opts.ZSHCompletionFile != "" { - err := writeCompletion(opts.ZSHCompletionFile, "zsh", cmdRoot.GenZshCompletion) + err := writeCompletion(opts.ZSHCompletionFile, "zsh", cmdRoot.GenZshCompletion, printer) if err != nil { return err } } if opts.PowerShellCompletionFile != "" { - err := writeCompletion(opts.PowerShellCompletionFile, "powershell", cmdRoot.GenPowerShellCompletion) + err := writeCompletion(opts.PowerShellCompletionFile, "powershell", cmdRoot.GenPowerShellCompletion, printer) if err != nil { return err } diff --git a/cmd/restic/cmd_generate_integration_test.go b/cmd/restic/cmd_generate_integration_test.go index 0480abc04..858e72453 100644 --- a/cmd/restic/cmd_generate_integration_test.go +++ b/cmd/restic/cmd_generate_integration_test.go @@ -1,13 +1,23 @@ package main import ( - "bytes" + "context" "strings" "testing" rtest "github.com/restic/restic/internal/test" + "github.com/restic/restic/internal/ui" ) +func testRunGenerate(gopts GlobalOptions, opts generateOptions) ([]byte, error) { + buf, err := withCaptureStdout(gopts, func(gopts GlobalOptions) error { + return withTermStatus(gopts, func(ctx context.Context, term ui.Terminal) error { + return runGenerate(opts, gopts, []string{}, term) + }) + }) + return buf.Bytes(), err +} + func TestGenerateStdout(t *testing.T) { testCases := []struct { name string @@ -21,20 +31,14 @@ func TestGenerateStdout(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - buf := bytes.NewBuffer(nil) - globalOptions.stdout = buf - err := runGenerate(tc.opts, []string{}) + output, err := testRunGenerate(globalOptions, tc.opts) rtest.OK(t, err) - completionString := buf.String() - rtest.Assert(t, strings.Contains(completionString, "# "+tc.name+" completion for restic"), "has no expected completion header") + rtest.Assert(t, strings.Contains(string(output), "# "+tc.name+" completion for restic"), "has no expected completion header") }) } t.Run("Generate shell completions to stdout for two shells", func(t *testing.T) { - buf := bytes.NewBuffer(nil) - globalOptions.stdout = buf - opts := generateOptions{BashCompletionFile: "-", FishCompletionFile: "-"} - err := runGenerate(opts, []string{}) + _, err := testRunGenerate(globalOptions, generateOptions{BashCompletionFile: "-", FishCompletionFile: "-"}) rtest.Assert(t, err != nil, "generate shell completions to stdout for two shells fails") }) } diff --git a/cmd/restic/cmd_init.go b/cmd/restic/cmd_init.go index d66163af1..c11015feb 100644 --- a/cmd/restic/cmd_init.go +++ b/cmd/restic/cmd_init.go @@ -10,6 +10,8 @@ import ( "github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/repository" "github.com/restic/restic/internal/restic" + "github.com/restic/restic/internal/ui" + "github.com/restic/restic/internal/ui/progress" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -33,7 +35,9 @@ Exit status is 1 if there was any error. GroupID: cmdGroupDefault, DisableAutoGenTag: true, RunE: func(cmd *cobra.Command, args []string) error { - return runInit(cmd.Context(), opts, globalOptions, args) + term, cancel := setupTermstatus() + defer cancel() + return runInit(cmd.Context(), opts, globalOptions, args, term) }, } opts.AddFlags(cmd.Flags()) @@ -53,11 +57,13 @@ func (opts *InitOptions) AddFlags(f *pflag.FlagSet) { f.StringVar(&opts.RepositoryVersion, "repository-version", "stable", "repository format version to use, allowed values are a format version, 'latest' and 'stable'") } -func runInit(ctx context.Context, opts InitOptions, gopts GlobalOptions, args []string) error { +func runInit(ctx context.Context, opts InitOptions, gopts GlobalOptions, args []string, term ui.Terminal) error { if len(args) > 0 { return errors.Fatal("the init command expects no arguments, only options - please see `restic help init` for usage and flags") } + printer := newTerminalProgressPrinter(gopts.JSON, gopts.verbosity, term) + var version uint if opts.RepositoryVersion == "latest" || opts.RepositoryVersion == "" { version = restic.MaxRepoVersion @@ -74,7 +80,7 @@ func runInit(ctx context.Context, opts InitOptions, gopts GlobalOptions, args [] return errors.Fatalf("only repository versions between %v and %v are allowed", restic.MinRepoVersion, restic.MaxRepoVersion) } - chunkerPolynomial, err := maybeReadChunkerPolynomial(ctx, opts, gopts) + chunkerPolynomial, err := maybeReadChunkerPolynomial(ctx, opts, gopts, printer) if err != nil { return err } @@ -86,12 +92,13 @@ func runInit(ctx context.Context, opts InitOptions, gopts GlobalOptions, args [] gopts.password, err = ReadPasswordTwice(ctx, gopts, "enter password for new repository: ", - "enter password again: ") + "enter password again: ", + printer) if err != nil { return err } - be, err := create(ctx, gopts.Repo, gopts, gopts.extended) + be, err := create(ctx, gopts.Repo, gopts, gopts.extended, printer) if err != nil { return errors.Fatalf("create repository at %s failed: %v\n", location.StripPassword(gopts.backends, gopts.Repo), err) } @@ -110,16 +117,14 @@ func runInit(ctx context.Context, opts InitOptions, gopts GlobalOptions, args [] } if !gopts.JSON { - Verbosef("created restic repository %v at %s", s.Config().ID[:10], location.StripPassword(gopts.backends, gopts.Repo)) + printer.P("created restic repository %v at %s", s.Config().ID[:10], location.StripPassword(gopts.backends, gopts.Repo)) if opts.CopyChunkerParameters && chunkerPolynomial != nil { - Verbosef(" with chunker parameters copied from secondary repository\n") - } else { - Verbosef("\n") + printer.P(" with chunker parameters copied from secondary repository") } - Verbosef("\n") - Verbosef("Please note that knowledge of your password is required to access\n") - Verbosef("the repository. Losing your password means that your data is\n") - Verbosef("irrecoverably lost.\n") + printer.P("") + printer.P("Please note that knowledge of your password is required to access") + printer.P("the repository. Losing your password means that your data is") + printer.P("irrecoverably lost.") } else { status := initSuccess{ @@ -133,14 +138,14 @@ func runInit(ctx context.Context, opts InitOptions, gopts GlobalOptions, args [] return nil } -func maybeReadChunkerPolynomial(ctx context.Context, opts InitOptions, gopts GlobalOptions) (*chunker.Pol, error) { +func maybeReadChunkerPolynomial(ctx context.Context, opts InitOptions, gopts GlobalOptions, printer progress.Printer) (*chunker.Pol, error) { if opts.CopyChunkerParameters { - otherGopts, _, err := fillSecondaryGlobalOpts(ctx, opts.secondaryRepoOptions, gopts, "secondary") + otherGopts, _, err := fillSecondaryGlobalOpts(ctx, opts.secondaryRepoOptions, gopts, "secondary", printer) if err != nil { return nil, err } - otherRepo, err := OpenRepository(ctx, otherGopts) + otherRepo, err := OpenRepository(ctx, otherGopts, printer) if err != nil { return nil, err } diff --git a/cmd/restic/cmd_init_integration_test.go b/cmd/restic/cmd_init_integration_test.go index 4795d5510..8ce14a23a 100644 --- a/cmd/restic/cmd_init_integration_test.go +++ b/cmd/restic/cmd_init_integration_test.go @@ -9,6 +9,8 @@ import ( "github.com/restic/restic/internal/repository" "github.com/restic/restic/internal/restic" rtest "github.com/restic/restic/internal/test" + "github.com/restic/restic/internal/ui" + "github.com/restic/restic/internal/ui/progress" ) func testRunInit(t testing.TB, opts GlobalOptions) { @@ -16,7 +18,10 @@ func testRunInit(t testing.TB, opts GlobalOptions) { restic.TestDisableCheckPolynomial(t) restic.TestSetLockTimeout(t, 0) - rtest.OK(t, runInit(context.TODO(), InitOptions{}, opts, nil)) + err := withTermStatus(opts, func(ctx context.Context, term ui.Terminal) error { + return runInit(ctx, InitOptions{}, opts, nil, term) + }) + rtest.OK(t, err) t.Logf("repository initialized at %v", opts.Repo) // create temporary junk files to verify that restic does not trip over them @@ -39,15 +44,21 @@ func TestInitCopyChunkerParams(t *testing.T) { password: env2.gopts.password, }, } - rtest.Assert(t, runInit(context.TODO(), initOpts, env.gopts, nil) != nil, "expected invalid init options to fail") + err := withTermStatus(env.gopts, func(ctx context.Context, term ui.Terminal) error { + return runInit(ctx, initOpts, env.gopts, nil, term) + }) + rtest.Assert(t, err != nil, "expected invalid init options to fail") initOpts.CopyChunkerParameters = true - rtest.OK(t, runInit(context.TODO(), initOpts, env.gopts, nil)) - - repo, err := OpenRepository(context.TODO(), env.gopts) + err = withTermStatus(env.gopts, func(ctx context.Context, term ui.Terminal) error { + return runInit(ctx, initOpts, env.gopts, nil, term) + }) rtest.OK(t, err) - otherRepo, err := OpenRepository(context.TODO(), env2.gopts) + repo, err := OpenRepository(context.TODO(), env.gopts, &progress.NoopPrinter{}) + rtest.OK(t, err) + + otherRepo, err := OpenRepository(context.TODO(), env2.gopts, &progress.NoopPrinter{}) rtest.OK(t, err) rtest.Assert(t, repo.Config().ChunkerPolynomial == otherRepo.Config().ChunkerPolynomial, diff --git a/cmd/restic/cmd_key_add.go b/cmd/restic/cmd_key_add.go index a7670a842..13785d43b 100644 --- a/cmd/restic/cmd_key_add.go +++ b/cmd/restic/cmd_key_add.go @@ -6,6 +6,8 @@ import ( "github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/repository" + "github.com/restic/restic/internal/ui" + "github.com/restic/restic/internal/ui/progress" "github.com/spf13/cobra" "github.com/spf13/pflag" ) @@ -30,7 +32,9 @@ Exit status is 12 if the password is incorrect. `, DisableAutoGenTag: true, RunE: func(cmd *cobra.Command, args []string) error { - return runKeyAdd(cmd.Context(), globalOptions, opts, args) + term, cancel := setupTermstatus() + defer cancel() + return runKeyAdd(cmd.Context(), globalOptions, opts, args, term) }, } @@ -52,22 +56,23 @@ func (opts *KeyAddOptions) Add(flags *pflag.FlagSet) { flags.StringVarP(&opts.Hostname, "host", "", "", "the hostname for new key") } -func runKeyAdd(ctx context.Context, gopts GlobalOptions, opts KeyAddOptions, args []string) error { +func runKeyAdd(ctx context.Context, gopts GlobalOptions, opts KeyAddOptions, args []string, term ui.Terminal) error { if len(args) > 0 { return fmt.Errorf("the key add command expects no arguments, only options - please see `restic help key add` for usage and flags") } - ctx, repo, unlock, err := openWithAppendLock(ctx, gopts, false) + printer := newTerminalProgressPrinter(false, gopts.verbosity, term) + ctx, repo, unlock, err := openWithAppendLock(ctx, gopts, false, printer) if err != nil { return err } defer unlock() - return addKey(ctx, repo, gopts, opts) + return addKey(ctx, repo, gopts, opts, printer) } -func addKey(ctx context.Context, repo *repository.Repository, gopts GlobalOptions, opts KeyAddOptions) error { - pw, err := getNewPassword(ctx, gopts, opts.NewPasswordFile, opts.InsecureNoPassword) +func addKey(ctx context.Context, repo *repository.Repository, gopts GlobalOptions, opts KeyAddOptions, printer progress.Printer) error { + pw, err := getNewPassword(ctx, gopts, opts.NewPasswordFile, opts.InsecureNoPassword, printer) if err != nil { return err } @@ -82,7 +87,7 @@ func addKey(ctx context.Context, repo *repository.Repository, gopts GlobalOption return err } - Verbosef("saved new key with ID %s\n", id.ID()) + printer.P("saved new key with ID %s", id.ID()) return nil } @@ -90,7 +95,7 @@ func addKey(ctx context.Context, repo *repository.Repository, gopts GlobalOption // testKeyNewPassword is used to set a new password during integration testing. var testKeyNewPassword string -func getNewPassword(ctx context.Context, gopts GlobalOptions, newPasswordFile string, insecureNoPassword bool) (string, error) { +func getNewPassword(ctx context.Context, gopts GlobalOptions, newPasswordFile string, insecureNoPassword bool, printer progress.Printer) (string, error) { if testKeyNewPassword != "" { return testKeyNewPassword, nil } @@ -122,7 +127,8 @@ func getNewPassword(ctx context.Context, gopts GlobalOptions, newPasswordFile st return ReadPasswordTwice(ctx, newopts, "enter new password: ", - "enter password again: ") + "enter password again: ", + printer) } func switchToNewKeyAndRemoveIfBroken(ctx context.Context, repo *repository.Repository, key *repository.Key, pw string) error { diff --git a/cmd/restic/cmd_key_integration_test.go b/cmd/restic/cmd_key_integration_test.go index 0b4533887..d24e71b09 100644 --- a/cmd/restic/cmd_key_integration_test.go +++ b/cmd/restic/cmd_key_integration_test.go @@ -12,11 +12,15 @@ import ( "github.com/restic/restic/internal/backend" "github.com/restic/restic/internal/repository" rtest "github.com/restic/restic/internal/test" + "github.com/restic/restic/internal/ui" + "github.com/restic/restic/internal/ui/progress" ) func testRunKeyListOtherIDs(t testing.TB, gopts GlobalOptions) []string { - buf, err := withCaptureStdout(func() error { - return runKeyList(context.TODO(), gopts, []string{}) + buf, err := withCaptureStdout(gopts, func(gopts GlobalOptions) error { + return withTermStatus(gopts, func(ctx context.Context, term ui.Terminal) error { + return runKeyList(ctx, gopts, []string{}, term) + }) }) rtest.OK(t, err) @@ -39,7 +43,10 @@ func testRunKeyAddNewKey(t testing.TB, newPassword string, gopts GlobalOptions) testKeyNewPassword = "" }() - rtest.OK(t, runKeyAdd(context.TODO(), gopts, KeyAddOptions{}, []string{})) + err := withTermStatus(gopts, func(ctx context.Context, term ui.Terminal) error { + return runKeyAdd(ctx, gopts, KeyAddOptions{}, []string{}, term) + }) + rtest.OK(t, err) } func testRunKeyAddNewKeyUserHost(t testing.TB, gopts GlobalOptions) { @@ -49,12 +56,15 @@ func testRunKeyAddNewKeyUserHost(t testing.TB, gopts GlobalOptions) { }() t.Log("adding key for john@example.com") - rtest.OK(t, runKeyAdd(context.TODO(), gopts, KeyAddOptions{ - Username: "john", - Hostname: "example.com", - }, []string{})) + err := withTermStatus(gopts, func(ctx context.Context, term ui.Terminal) error { + return runKeyAdd(ctx, gopts, KeyAddOptions{ + Username: "john", + Hostname: "example.com", + }, []string{}, term) + }) + rtest.OK(t, err) - repo, err := OpenRepository(context.TODO(), gopts) + repo, err := OpenRepository(context.TODO(), gopts, &progress.NoopPrinter{}) rtest.OK(t, err) key, err := repository.SearchKey(context.TODO(), repo, testKeyNewPassword, 2, "") rtest.OK(t, err) @@ -69,13 +79,19 @@ func testRunKeyPasswd(t testing.TB, newPassword string, gopts GlobalOptions) { testKeyNewPassword = "" }() - rtest.OK(t, runKeyPasswd(context.TODO(), gopts, KeyPasswdOptions{}, []string{})) + err := withTermStatus(gopts, func(ctx context.Context, term ui.Terminal) error { + return runKeyPasswd(ctx, gopts, KeyPasswdOptions{}, []string{}, term) + }) + rtest.OK(t, err) } func testRunKeyRemove(t testing.TB, gopts GlobalOptions, IDs []string) { t.Logf("remove %d keys: %q\n", len(IDs), IDs) for _, id := range IDs { - rtest.OK(t, runKeyRemove(context.TODO(), gopts, []string{id})) + err := withTermStatus(gopts, func(ctx context.Context, term ui.Terminal) error { + return runKeyRemove(ctx, gopts, []string{id}, term) + }) + rtest.OK(t, err) } } @@ -105,7 +121,10 @@ func TestKeyAddRemove(t *testing.T) { env.gopts.password = passwordList[len(passwordList)-1] t.Logf("testing access with last password %q\n", env.gopts.password) - rtest.OK(t, runKeyList(context.TODO(), env.gopts, []string{})) + err := withTermStatus(env.gopts, func(ctx context.Context, term ui.Terminal) error { + return runKeyList(ctx, env.gopts, []string{}, term) + }) + rtest.OK(t, err) testRunCheck(t, env.gopts) testRunKeyAddNewKeyUserHost(t, env.gopts) @@ -116,18 +135,22 @@ func TestKeyAddInvalid(t *testing.T) { defer cleanup() testRunInit(t, env.gopts) - err := runKeyAdd(context.TODO(), env.gopts, KeyAddOptions{ - NewPasswordFile: "some-file", - InsecureNoPassword: true, - }, []string{}) + err := withTermStatus(env.gopts, func(ctx context.Context, term ui.Terminal) error { + return runKeyAdd(ctx, env.gopts, KeyAddOptions{ + NewPasswordFile: "some-file", + InsecureNoPassword: true, + }, []string{}, term) + }) rtest.Assert(t, strings.Contains(err.Error(), "only either"), "unexpected error message, got %q", err) pwfile := filepath.Join(t.TempDir(), "pwfile") rtest.OK(t, os.WriteFile(pwfile, []byte{}, 0o666)) - err = runKeyAdd(context.TODO(), env.gopts, KeyAddOptions{ - NewPasswordFile: pwfile, - }, []string{}) + err = withTermStatus(env.gopts, func(ctx context.Context, term ui.Terminal) error { + return runKeyAdd(ctx, env.gopts, KeyAddOptions{ + NewPasswordFile: pwfile, + }, []string{}, term) + }) rtest.Assert(t, strings.Contains(err.Error(), "an empty password is not allowed by default"), "unexpected error message, got %q", err) } @@ -138,9 +161,12 @@ func TestKeyAddEmpty(t *testing.T) { defer cleanup() testRunInit(t, env.gopts) - rtest.OK(t, runKeyAdd(context.TODO(), env.gopts, KeyAddOptions{ - InsecureNoPassword: true, - }, []string{})) + err := withTermStatus(env.gopts, func(ctx context.Context, term ui.Terminal) error { + return runKeyAdd(ctx, env.gopts, KeyAddOptions{ + InsecureNoPassword: true, + }, []string{}, term) + }) + rtest.OK(t, err) env.gopts.password = "" env.gopts.InsecureNoPassword = true @@ -170,16 +196,23 @@ func TestKeyProblems(t *testing.T) { testKeyNewPassword = "" }() - err := runKeyPasswd(context.TODO(), env.gopts, KeyPasswdOptions{}, []string{}) + err := withTermStatus(env.gopts, func(ctx context.Context, term ui.Terminal) error { + return runKeyPasswd(ctx, env.gopts, KeyPasswdOptions{}, []string{}, term) + }) t.Log(err) rtest.Assert(t, err != nil, "expected passwd change to fail") - err = runKeyAdd(context.TODO(), env.gopts, KeyAddOptions{}, []string{}) + err = withTermStatus(env.gopts, func(ctx context.Context, term ui.Terminal) error { + return runKeyAdd(ctx, env.gopts, KeyAddOptions{}, []string{}, term) + }) t.Log(err) rtest.Assert(t, err != nil, "expected key adding to fail") t.Logf("testing access with initial password %q\n", env.gopts.password) - rtest.OK(t, runKeyList(context.TODO(), env.gopts, []string{})) + err = withTermStatus(env.gopts, func(ctx context.Context, term ui.Terminal) error { + return runKeyList(ctx, env.gopts, []string{}, term) + }) + rtest.OK(t, err) testRunCheck(t, env.gopts) } @@ -192,23 +225,33 @@ func TestKeyCommandInvalidArguments(t *testing.T) { return &emptySaveBackend{r}, nil } - err := runKeyAdd(context.TODO(), env.gopts, KeyAddOptions{}, []string{"johndoe"}) + err := withTermStatus(env.gopts, func(ctx context.Context, term ui.Terminal) error { + return runKeyAdd(ctx, env.gopts, KeyAddOptions{}, []string{"johndoe"}, term) + }) t.Log(err) rtest.Assert(t, err != nil && strings.Contains(err.Error(), "no arguments"), "unexpected error for key add: %v", err) - err = runKeyPasswd(context.TODO(), env.gopts, KeyPasswdOptions{}, []string{"johndoe"}) + err = withTermStatus(env.gopts, func(ctx context.Context, term ui.Terminal) error { + return runKeyPasswd(ctx, env.gopts, KeyPasswdOptions{}, []string{"johndoe"}, term) + }) t.Log(err) rtest.Assert(t, err != nil && strings.Contains(err.Error(), "no arguments"), "unexpected error for key passwd: %v", err) - err = runKeyList(context.TODO(), env.gopts, []string{"johndoe"}) + err = withTermStatus(env.gopts, func(ctx context.Context, term ui.Terminal) error { + return runKeyList(ctx, env.gopts, []string{"johndoe"}, term) + }) t.Log(err) rtest.Assert(t, err != nil && strings.Contains(err.Error(), "no arguments"), "unexpected error for key list: %v", err) - err = runKeyRemove(context.TODO(), env.gopts, []string{}) + err = withTermStatus(env.gopts, func(ctx context.Context, term ui.Terminal) error { + return runKeyRemove(ctx, env.gopts, []string{}, term) + }) t.Log(err) rtest.Assert(t, err != nil && strings.Contains(err.Error(), "one argument"), "unexpected error for key remove: %v", err) - err = runKeyRemove(context.TODO(), env.gopts, []string{"john", "doe"}) + err = withTermStatus(env.gopts, func(ctx context.Context, term ui.Terminal) error { + return runKeyRemove(ctx, env.gopts, []string{"john", "doe"}, term) + }) t.Log(err) rtest.Assert(t, err != nil && strings.Contains(err.Error(), "one argument"), "unexpected error for key remove: %v", err) } diff --git a/cmd/restic/cmd_key_list.go b/cmd/restic/cmd_key_list.go index 6a0509f0f..897601675 100644 --- a/cmd/restic/cmd_key_list.go +++ b/cmd/restic/cmd_key_list.go @@ -8,6 +8,8 @@ import ( "github.com/restic/restic/internal/repository" "github.com/restic/restic/internal/restic" + "github.com/restic/restic/internal/ui" + "github.com/restic/restic/internal/ui/progress" "github.com/restic/restic/internal/ui/table" "github.com/spf13/cobra" ) @@ -32,27 +34,30 @@ Exit status is 12 if the password is incorrect. `, DisableAutoGenTag: true, RunE: func(cmd *cobra.Command, args []string) error { - return runKeyList(cmd.Context(), globalOptions, args) + term, cancel := setupTermstatus() + defer cancel() + return runKeyList(cmd.Context(), globalOptions, args, term) }, } return cmd } -func runKeyList(ctx context.Context, gopts GlobalOptions, args []string) error { +func runKeyList(ctx context.Context, gopts GlobalOptions, args []string, term ui.Terminal) error { if len(args) > 0 { return fmt.Errorf("the key list command expects no arguments, only options - please see `restic help key list` for usage and flags") } - ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock) + printer := newTerminalProgressPrinter(gopts.JSON, gopts.verbosity, term) + ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock, printer) if err != nil { return err } defer unlock() - return listKeys(ctx, repo, gopts) + return listKeys(ctx, repo, gopts, printer) } -func listKeys(ctx context.Context, s *repository.Repository, gopts GlobalOptions) error { +func listKeys(ctx context.Context, s *repository.Repository, gopts GlobalOptions, printer progress.Printer) error { type keyInfo struct { Current bool `json:"current"` ID string `json:"id"` @@ -68,7 +73,7 @@ func listKeys(ctx context.Context, s *repository.Repository, gopts GlobalOptions err := restic.ParallelList(ctx, s, restic.KeyFile, s.Connections(), func(ctx context.Context, id restic.ID, _ int64) error { k, err := repository.LoadKey(ctx, s, id) if err != nil { - Warnf("LoadKey() failed: %v\n", err) + printer.E("LoadKey() failed: %v", err) return nil } diff --git a/cmd/restic/cmd_key_passwd.go b/cmd/restic/cmd_key_passwd.go index 378325216..3e1be711b 100644 --- a/cmd/restic/cmd_key_passwd.go +++ b/cmd/restic/cmd_key_passwd.go @@ -6,6 +6,8 @@ import ( "github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/repository" + "github.com/restic/restic/internal/ui" + "github.com/restic/restic/internal/ui/progress" "github.com/spf13/cobra" "github.com/spf13/pflag" ) @@ -31,7 +33,9 @@ Exit status is 12 if the password is incorrect. `, DisableAutoGenTag: true, RunE: func(cmd *cobra.Command, args []string) error { - return runKeyPasswd(cmd.Context(), globalOptions, opts, args) + term, cancel := setupTermstatus() + defer cancel() + return runKeyPasswd(cmd.Context(), globalOptions, opts, args, term) }, } @@ -47,22 +51,23 @@ func (opts *KeyPasswdOptions) AddFlags(flags *pflag.FlagSet) { opts.KeyAddOptions.Add(flags) } -func runKeyPasswd(ctx context.Context, gopts GlobalOptions, opts KeyPasswdOptions, args []string) error { +func runKeyPasswd(ctx context.Context, gopts GlobalOptions, opts KeyPasswdOptions, args []string, term ui.Terminal) error { if len(args) > 0 { return fmt.Errorf("the key passwd command expects no arguments, only options - please see `restic help key passwd` for usage and flags") } - ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, false) + printer := newTerminalProgressPrinter(false, gopts.verbosity, term) + ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, false, printer) if err != nil { return err } defer unlock() - return changePassword(ctx, repo, gopts, opts) + return changePassword(ctx, repo, gopts, opts, printer) } -func changePassword(ctx context.Context, repo *repository.Repository, gopts GlobalOptions, opts KeyPasswdOptions) error { - pw, err := getNewPassword(ctx, gopts, opts.NewPasswordFile, opts.InsecureNoPassword) +func changePassword(ctx context.Context, repo *repository.Repository, gopts GlobalOptions, opts KeyPasswdOptions, printer progress.Printer) error { + pw, err := getNewPassword(ctx, gopts, opts.NewPasswordFile, opts.InsecureNoPassword, printer) if err != nil { return err } @@ -83,7 +88,7 @@ func changePassword(ctx context.Context, repo *repository.Repository, gopts Glob return err } - Verbosef("saved new key as %s\n", id) + printer.P("saved new key as %s", id) return nil } diff --git a/cmd/restic/cmd_key_remove.go b/cmd/restic/cmd_key_remove.go index f6713e4c2..5adb97d80 100644 --- a/cmd/restic/cmd_key_remove.go +++ b/cmd/restic/cmd_key_remove.go @@ -7,6 +7,8 @@ import ( "github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/repository" "github.com/restic/restic/internal/restic" + "github.com/restic/restic/internal/ui" + "github.com/restic/restic/internal/ui/progress" "github.com/spf13/cobra" ) @@ -29,27 +31,30 @@ Exit status is 12 if the password is incorrect. `, DisableAutoGenTag: true, RunE: func(cmd *cobra.Command, args []string) error { - return runKeyRemove(cmd.Context(), globalOptions, args) + term, cancel := setupTermstatus() + defer cancel() + return runKeyRemove(cmd.Context(), globalOptions, args, term) }, } return cmd } -func runKeyRemove(ctx context.Context, gopts GlobalOptions, args []string) error { +func runKeyRemove(ctx context.Context, gopts GlobalOptions, args []string, term ui.Terminal) error { if len(args) != 1 { return fmt.Errorf("key remove expects one argument as the key id") } - ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, false) + printer := newTerminalProgressPrinter(gopts.JSON, gopts.verbosity, term) + ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, false, printer) if err != nil { return err } defer unlock() - return deleteKey(ctx, repo, args[0]) + return deleteKey(ctx, repo, args[0], printer) } -func deleteKey(ctx context.Context, repo *repository.Repository, idPrefix string) error { +func deleteKey(ctx context.Context, repo *repository.Repository, idPrefix string, printer progress.Printer) error { id, err := restic.Find(ctx, repo, restic.KeyFile, idPrefix) if err != nil { return err @@ -64,6 +69,6 @@ func deleteKey(ctx context.Context, repo *repository.Repository, idPrefix string return err } - Verbosef("removed key %v\n", id) + printer.P("removed key %v", id) return nil } diff --git a/cmd/restic/cmd_list.go b/cmd/restic/cmd_list.go index cf0cec414..fc425f07d 100644 --- a/cmd/restic/cmd_list.go +++ b/cmd/restic/cmd_list.go @@ -7,6 +7,7 @@ import ( "github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/repository/index" "github.com/restic/restic/internal/restic" + "github.com/restic/restic/internal/ui" "github.com/spf13/cobra" ) @@ -33,7 +34,9 @@ Exit status is 12 if the password is incorrect. DisableAutoGenTag: true, GroupID: cmdGroupDefault, RunE: func(cmd *cobra.Command, args []string) error { - return runList(cmd.Context(), globalOptions, args) + term, cancel := setupTermstatus() + defer cancel() + return runList(cmd.Context(), globalOptions, args, term) }, ValidArgs: listAllowedArgs, Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs), @@ -41,12 +44,14 @@ Exit status is 12 if the password is incorrect. return cmd } -func runList(ctx context.Context, gopts GlobalOptions, args []string) error { +func runList(ctx context.Context, gopts GlobalOptions, args []string, term ui.Terminal) error { + printer := newTerminalProgressPrinter(false, gopts.verbosity, term) + if len(args) != 1 { return errors.Fatal("type not specified") } - ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock || args[0] == "locks") + ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock || args[0] == "locks", printer) if err != nil { return err } @@ -70,7 +75,7 @@ func runList(ctx context.Context, gopts GlobalOptions, args []string) error { return err } return idx.Each(ctx, func(blobs restic.PackedBlob) { - Printf("%v %v\n", blobs.Type, blobs.ID) + printer.S("%v %v", blobs.Type, blobs.ID) }) }) default: @@ -78,7 +83,7 @@ func runList(ctx context.Context, gopts GlobalOptions, args []string) error { } return repo.List(ctx, t, func(id restic.ID, _ int64) error { - Printf("%s\n", id) + printer.S("%s", id) return nil }) } diff --git a/cmd/restic/cmd_list_integration_test.go b/cmd/restic/cmd_list_integration_test.go index ef2b8bf8f..69fef1d6b 100644 --- a/cmd/restic/cmd_list_integration_test.go +++ b/cmd/restic/cmd_list_integration_test.go @@ -8,11 +8,14 @@ import ( "github.com/restic/restic/internal/restic" rtest "github.com/restic/restic/internal/test" + "github.com/restic/restic/internal/ui" ) -func testRunList(t testing.TB, tpe string, opts GlobalOptions) restic.IDs { - buf, err := withCaptureStdout(func() error { - return runList(context.TODO(), opts, []string{tpe}) +func testRunList(t testing.TB, opts GlobalOptions, tpe string) restic.IDs { + buf, err := withCaptureStdout(opts, func(opts GlobalOptions) error { + return withTermStatus(opts, func(ctx context.Context, term ui.Terminal) error { + return runList(ctx, opts, []string{tpe}, term) + }) }) rtest.OK(t, err) return parseIDsFromReader(t, buf) @@ -38,7 +41,7 @@ func parseIDsFromReader(t testing.TB, rd io.Reader) restic.IDs { func testListSnapshots(t testing.TB, opts GlobalOptions, expected int) restic.IDs { t.Helper() - snapshotIDs := testRunList(t, "snapshots", opts) + snapshotIDs := testRunList(t, opts, "snapshots") rtest.Assert(t, len(snapshotIDs) == expected, "expected %v snapshot, got %v", expected, snapshotIDs) return snapshotIDs } diff --git a/cmd/restic/cmd_ls.go b/cmd/restic/cmd_ls.go index bba81a070..532d2f4dd 100644 --- a/cmd/restic/cmd_ls.go +++ b/cmd/restic/cmd_ls.go @@ -18,6 +18,7 @@ import ( "github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/fs" "github.com/restic/restic/internal/restic" + "github.com/restic/restic/internal/ui" "github.com/restic/restic/internal/walker" ) @@ -59,7 +60,9 @@ Exit status is 12 if the password is incorrect. DisableAutoGenTag: true, GroupID: cmdGroupDefault, RunE: func(cmd *cobra.Command, args []string) error { - return runLs(cmd.Context(), opts, globalOptions, args) + term, cancel := setupTermstatus() + defer cancel() + return runLs(cmd.Context(), opts, globalOptions, args, term) }, } opts.AddFlags(cmd.Flags()) @@ -270,15 +273,19 @@ type textLsPrinter struct { dirs []string ListLong bool HumanReadable bool + termPrinter interface { + P(msg string, args ...interface{}) + S(msg string, args ...interface{}) + } } func (p *textLsPrinter) Snapshot(sn *restic.Snapshot) error { - Verbosef("%v filtered by %v:\n", sn, p.dirs) + p.termPrinter.P("%v filtered by %v:", sn, p.dirs) return nil } func (p *textLsPrinter) Node(path string, node *restic.Node, isPrefixDirectory bool) error { if !isPrefixDirectory { - Printf("%s\n", formatNode(path, node, p.ListLong, p.HumanReadable)) + p.termPrinter.S("%s", formatNode(path, node, p.ListLong, p.HumanReadable)) } return nil } @@ -296,7 +303,9 @@ type toSortOutput struct { node *restic.Node } -func runLs(ctx context.Context, opts LsOptions, gopts GlobalOptions, args []string) error { +func runLs(ctx context.Context, opts LsOptions, gopts GlobalOptions, args []string, term ui.Terminal) error { + termPrinter := newTerminalProgressPrinter(gopts.JSON, gopts.verbosity, term) + if len(args) == 0 { return errors.Fatal("no snapshot ID specified, specify snapshot ID or use special ID 'latest'") } @@ -355,7 +364,7 @@ func runLs(ctx context.Context, opts LsOptions, gopts GlobalOptions, args []stri return false } - ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock) + ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock, termPrinter) if err != nil { return err } @@ -366,7 +375,7 @@ func runLs(ctx context.Context, opts LsOptions, gopts GlobalOptions, args []stri return err } - bar := newIndexProgress(gopts.Quiet, gopts.JSON) + bar := newIndexTerminalProgress(termPrinter) if err = repo.LoadIndex(ctx, bar); err != nil { return err } @@ -386,6 +395,7 @@ func runLs(ctx context.Context, opts LsOptions, gopts GlobalOptions, args []stri dirs: dirs, ListLong: opts.ListLong, HumanReadable: opts.HumanReadable, + termPrinter: termPrinter, } } if opts.Sort != SortModeName || opts.Reverse { diff --git a/cmd/restic/cmd_ls_integration_test.go b/cmd/restic/cmd_ls_integration_test.go index f72a4533a..b39e9e582 100644 --- a/cmd/restic/cmd_ls_integration_test.go +++ b/cmd/restic/cmd_ls_integration_test.go @@ -10,12 +10,15 @@ import ( "github.com/restic/restic/internal/restic" rtest "github.com/restic/restic/internal/test" + "github.com/restic/restic/internal/ui" ) func testRunLsWithOpts(t testing.TB, gopts GlobalOptions, opts LsOptions, args []string) []byte { - buf, err := withCaptureStdout(func() error { + buf, err := withCaptureStdout(gopts, func(gopts GlobalOptions) error { gopts.Quiet = true - return runLs(context.TODO(), opts, gopts, args) + return withTermStatus(gopts, func(ctx context.Context, term ui.Terminal) error { + return runLs(context.TODO(), opts, gopts, args, term) + }) }) rtest.OK(t, err) return buf.Bytes() diff --git a/cmd/restic/cmd_migrate.go b/cmd/restic/cmd_migrate.go index d4e7d0ac1..8e1d23c04 100644 --- a/cmd/restic/cmd_migrate.go +++ b/cmd/restic/cmd_migrate.go @@ -5,8 +5,8 @@ import ( "github.com/restic/restic/internal/migrations" "github.com/restic/restic/internal/restic" + "github.com/restic/restic/internal/ui" "github.com/restic/restic/internal/ui/progress" - "github.com/restic/restic/internal/ui/termstatus" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -77,7 +77,7 @@ func checkMigrations(ctx context.Context, repo restic.Repository, printer progre return nil } -func applyMigrations(ctx context.Context, opts MigrateOptions, gopts GlobalOptions, repo restic.Repository, args []string, term *termstatus.Terminal, printer progress.Printer) error { +func applyMigrations(ctx context.Context, opts MigrateOptions, gopts GlobalOptions, repo restic.Repository, args []string, term ui.Terminal, printer progress.Printer) error { var firsterr error for _, name := range args { found := false @@ -135,10 +135,10 @@ func applyMigrations(ctx context.Context, opts MigrateOptions, gopts GlobalOptio return firsterr } -func runMigrate(ctx context.Context, opts MigrateOptions, gopts GlobalOptions, args []string, term *termstatus.Terminal) error { - printer := newTerminalProgressPrinter(gopts.verbosity, term) +func runMigrate(ctx context.Context, opts MigrateOptions, gopts GlobalOptions, args []string, term ui.Terminal) error { + printer := newTerminalProgressPrinter(false, gopts.verbosity, term) - ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, false) + ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, false, printer) if err != nil { return err } diff --git a/cmd/restic/cmd_mount.go b/cmd/restic/cmd_mount.go index d2c905460..a476422fd 100644 --- a/cmd/restic/cmd_mount.go +++ b/cmd/restic/cmd_mount.go @@ -16,6 +16,7 @@ import ( "github.com/restic/restic/internal/debug" "github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/restic" + "github.com/restic/restic/internal/ui" "github.com/restic/restic/internal/fuse" @@ -81,7 +82,9 @@ Exit status is 12 if the password is incorrect. DisableAutoGenTag: true, GroupID: cmdGroupDefault, RunE: func(cmd *cobra.Command, args []string) error { - return runMount(cmd.Context(), opts, globalOptions, args) + term, cancel := setupTermstatus() + defer cancel() + return runMount(cmd.Context(), opts, globalOptions, args, term) }, } @@ -112,7 +115,9 @@ func (opts *MountOptions) AddFlags(f *pflag.FlagSet) { _ = f.MarkDeprecated("snapshot-template", "use --time-template") } -func runMount(ctx context.Context, opts MountOptions, gopts GlobalOptions, args []string) error { +func runMount(ctx context.Context, opts MountOptions, gopts GlobalOptions, args []string, term ui.Terminal) error { + printer := newTerminalProgressPrinter(false, gopts.verbosity, term) + if opts.TimeTemplate == "" { return errors.Fatal("time template string cannot be empty") } @@ -130,20 +135,20 @@ func runMount(ctx context.Context, opts MountOptions, gopts GlobalOptions, args // Check the existence of the mount point at the earliest stage to // prevent unnecessary computations while opening the repository. if _, err := os.Stat(mountpoint); errors.Is(err, os.ErrNotExist) { - Verbosef("Mountpoint %s doesn't exist\n", mountpoint) + printer.P("Mountpoint %s doesn't exist", mountpoint) return err } debug.Log("start mount") defer debug.Log("finish mount") - ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock) + ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock, printer) if err != nil { return err } defer unlock() - bar := newIndexProgress(gopts.Quiet, gopts.JSON) + bar := newIndexTerminalProgress(printer) err = repo.LoadIndex(ctx, bar) if err != nil { return err @@ -183,9 +188,9 @@ func runMount(ctx context.Context, opts MountOptions, gopts GlobalOptions, args } root := fuse.NewRoot(repo, cfg) - Printf("Now serving the repository at %s\n", mountpoint) - Printf("Use another terminal or tool to browse the contents of this folder.\n") - Printf("When finished, quit with Ctrl-c here or umount the mountpoint.\n") + printer.S("Now serving the repository at %s", mountpoint) + printer.S("Use another terminal or tool to browse the contents of this folder.") + printer.S("When finished, quit with Ctrl-c here or umount the mountpoint.") debug.Log("serving mount at %v", mountpoint) @@ -201,7 +206,7 @@ func runMount(ctx context.Context, opts MountOptions, gopts GlobalOptions, args debug.Log("running umount cleanup handler for mount at %v", mountpoint) err := systemFuse.Unmount(mountpoint) if err != nil { - Warnf("unable to umount (maybe already umounted or still in use?): %v\n", err) + printer.E("unable to umount (maybe already umounted or still in use?): %v", err) } return ErrOK diff --git a/cmd/restic/cmd_mount_integration_test.go b/cmd/restic/cmd_mount_integration_test.go index c5f4d193a..ea1451ba6 100644 --- a/cmd/restic/cmd_mount_integration_test.go +++ b/cmd/restic/cmd_mount_integration_test.go @@ -16,6 +16,7 @@ import ( "github.com/restic/restic/internal/debug" "github.com/restic/restic/internal/restic" rtest "github.com/restic/restic/internal/test" + "github.com/restic/restic/internal/ui" ) const ( @@ -61,7 +62,9 @@ func testRunMount(t testing.TB, gopts GlobalOptions, dir string, wg *sync.WaitGr opts := MountOptions{ TimeTemplate: time.RFC3339, } - rtest.OK(t, runMount(context.TODO(), opts, gopts, []string{dir})) + rtest.OK(t, withTermStatus(gopts, func(ctx context.Context, term ui.Terminal) error { + return runMount(context.TODO(), opts, gopts, []string{dir}, term) + })) } func testRunUmount(t testing.TB, dir string) { @@ -125,34 +128,41 @@ func checkSnapshots(t testing.TB, gopts GlobalOptions, mountpoint string, snapsh } } - _, repo, unlock, err := openWithReadLock(context.TODO(), gopts, false) - rtest.OK(t, err) - defer unlock() - - for _, id := range snapshotIDs { - snapshot, err := restic.LoadSnapshot(context.TODO(), repo, id) - rtest.OK(t, err) - - ts := snapshot.Time.Format(time.RFC3339) - present, ok := namesMap[ts] - if !ok { - t.Errorf("Snapshot %v (%q) isn't present in fuse dir", id.Str(), ts) + err := withTermStatus(gopts, func(ctx context.Context, term ui.Terminal) error { + printer := newTerminalProgressPrinter(gopts.JSON, gopts.verbosity, term) + _, repo, unlock, err := openWithReadLock(ctx, gopts, false, printer) + if err != nil { + return err } + defer unlock() - for i := 1; present; i++ { - ts = fmt.Sprintf("%s-%d", snapshot.Time.Format(time.RFC3339), i) - present, ok = namesMap[ts] + for _, id := range snapshotIDs { + snapshot, err := restic.LoadSnapshot(ctx, repo, id) + rtest.OK(t, err) + + ts := snapshot.Time.Format(time.RFC3339) + present, ok := namesMap[ts] if !ok { t.Errorf("Snapshot %v (%q) isn't present in fuse dir", id.Str(), ts) } - if !present { - break - } - } + for i := 1; present; i++ { + ts = fmt.Sprintf("%s-%d", snapshot.Time.Format(time.RFC3339), i) + present, ok = namesMap[ts] + if !ok { + t.Errorf("Snapshot %v (%q) isn't present in fuse dir", id.Str(), ts) + } - namesMap[ts] = true - } + if !present { + break + } + } + + namesMap[ts] = true + } + return nil + }) + rtest.OK(t, err) for name, present := range namesMap { rtest.Assert(t, present, "Directory %s is present in fuse dir but is not a snapshot", name) @@ -177,7 +187,7 @@ func TestMount(t *testing.T) { // first backup testRunBackup(t, "", []string{env.testdata}, BackupOptions{}, env.gopts) - snapshotIDs := testRunList(t, "snapshots", env.gopts) + snapshotIDs := testRunList(t, env.gopts, "snapshots") rtest.Assert(t, len(snapshotIDs) == 1, "expected one snapshot, got %v", snapshotIDs) @@ -185,7 +195,7 @@ func TestMount(t *testing.T) { // second backup, implicit incremental testRunBackup(t, "", []string{env.testdata}, BackupOptions{}, env.gopts) - snapshotIDs = testRunList(t, "snapshots", env.gopts) + snapshotIDs = testRunList(t, env.gopts, "snapshots") rtest.Assert(t, len(snapshotIDs) == 2, "expected two snapshots, got %v", snapshotIDs) @@ -194,7 +204,7 @@ func TestMount(t *testing.T) { // third backup, explicit incremental bopts := BackupOptions{Parent: snapshotIDs[0].String()} testRunBackup(t, "", []string{env.testdata}, bopts, env.gopts) - snapshotIDs = testRunList(t, "snapshots", env.gopts) + snapshotIDs = testRunList(t, env.gopts, "snapshots") rtest.Assert(t, len(snapshotIDs) == 3, "expected three snapshots, got %v", snapshotIDs) diff --git a/cmd/restic/cmd_prune.go b/cmd/restic/cmd_prune.go index ff43d83c0..3adc6a90e 100644 --- a/cmd/restic/cmd_prune.go +++ b/cmd/restic/cmd_prune.go @@ -13,7 +13,6 @@ import ( "github.com/restic/restic/internal/restic" "github.com/restic/restic/internal/ui" "github.com/restic/restic/internal/ui/progress" - "github.com/restic/restic/internal/ui/termstatus" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -155,7 +154,7 @@ func verifyPruneOptions(opts *PruneOptions) error { return nil } -func runPrune(ctx context.Context, opts PruneOptions, gopts GlobalOptions, term *termstatus.Terminal) error { +func runPrune(ctx context.Context, opts PruneOptions, gopts GlobalOptions, term ui.Terminal) error { err := verifyPruneOptions(&opts) if err != nil { return err @@ -169,7 +168,8 @@ func runPrune(ctx context.Context, opts PruneOptions, gopts GlobalOptions, term return errors.Fatal("--no-lock is only applicable in combination with --dry-run for prune command") } - ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, opts.DryRun && gopts.NoLock) + printer := newTerminalProgressPrinter(false, gopts.verbosity, term) + ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, opts.DryRun && gopts.NoLock, printer) if err != nil { return err } @@ -183,19 +183,16 @@ func runPrune(ctx context.Context, opts PruneOptions, gopts GlobalOptions, term opts.unsafeRecovery = true } - return runPruneWithRepo(ctx, opts, gopts, repo, restic.NewIDSet(), term) + return runPruneWithRepo(ctx, opts, repo, restic.NewIDSet(), printer) } -func runPruneWithRepo(ctx context.Context, opts PruneOptions, gopts GlobalOptions, repo *repository.Repository, ignoreSnapshots restic.IDSet, term *termstatus.Terminal) error { +func runPruneWithRepo(ctx context.Context, opts PruneOptions, repo *repository.Repository, ignoreSnapshots restic.IDSet, printer progress.Printer) error { if repo.Cache() == nil { - Print("warning: running prune without a cache, this may be very slow!\n") + printer.S("warning: running prune without a cache, this may be very slow!") } - printer := newTerminalProgressPrinter(gopts.verbosity, term) - - printer.P("loading indexes...\n") // loading the index before the snapshots is ok, as we use an exclusive lock here - bar := newIndexTerminalProgress(gopts.Quiet, gopts.JSON, term) + bar := newIndexTerminalProgress(printer) err := repo.LoadIndex(ctx, bar) if err != nil { return err diff --git a/cmd/restic/cmd_prune_integration_test.go b/cmd/restic/cmd_prune_integration_test.go index 9de2e0b8d..d9103fc8f 100644 --- a/cmd/restic/cmd_prune_integration_test.go +++ b/cmd/restic/cmd_prune_integration_test.go @@ -9,7 +9,7 @@ import ( "github.com/restic/restic/internal/backend" "github.com/restic/restic/internal/repository" rtest "github.com/restic/restic/internal/test" - "github.com/restic/restic/internal/ui/termstatus" + "github.com/restic/restic/internal/ui" ) func testRunPrune(t testing.TB, gopts GlobalOptions, opts PruneOptions) { @@ -29,7 +29,7 @@ func testRunPruneOutput(gopts GlobalOptions, opts PruneOptions) error { defer func() { gopts.backendTestHook = oldHook }() - return withTermStatus(gopts, func(ctx context.Context, term *termstatus.Terminal) error { + return withTermStatus(gopts, func(ctx context.Context, term ui.Terminal) error { return runPrune(context.TODO(), opts, gopts, term) }) } @@ -90,7 +90,7 @@ func createPrunableRepo(t *testing.T, env *testEnvironment) { } func testRunForgetJSON(t testing.TB, gopts GlobalOptions, args ...string) { - buf, err := withCaptureStdout(func() error { + buf, err := withCaptureStdout(gopts, func(gopts GlobalOptions) error { gopts.JSON = true opts := ForgetOptions{ DryRun: true, @@ -99,7 +99,7 @@ func testRunForgetJSON(t testing.TB, gopts GlobalOptions, args ...string) { pruneOpts := PruneOptions{ MaxUnused: "5%", } - return withTermStatus(gopts, func(ctx context.Context, term *termstatus.Terminal) error { + return withTermStatus(gopts, func(ctx context.Context, term ui.Terminal) error { return runForget(context.TODO(), opts, pruneOpts, gopts, term, args) }) }) @@ -122,7 +122,7 @@ func testPrune(t *testing.T, pruneOpts PruneOptions, checkOpts CheckOptions) { createPrunableRepo(t, env) testRunPrune(t, env.gopts, pruneOpts) - rtest.OK(t, withTermStatus(env.gopts, func(ctx context.Context, term *termstatus.Terminal) error { + rtest.OK(t, withTermStatus(env.gopts, func(ctx context.Context, term ui.Terminal) error { _, err := runCheck(context.TODO(), checkOpts, env.gopts, nil, term) return err })) @@ -158,7 +158,7 @@ func TestPruneWithDamagedRepository(t *testing.T) { env.gopts.backendTestHook = oldHook }() // prune should fail - rtest.Equals(t, repository.ErrPacksMissing, withTermStatus(env.gopts, func(ctx context.Context, term *termstatus.Terminal) error { + rtest.Equals(t, repository.ErrPacksMissing, withTermStatus(env.gopts, func(ctx context.Context, term ui.Terminal) error { return runPrune(context.TODO(), pruneDefaultOptions, env.gopts, term) }), "prune should have reported index not complete error") } @@ -231,7 +231,7 @@ func testEdgeCaseRepo(t *testing.T, tarfile string, optionsCheck CheckOptions, o if checkOK { testRunCheck(t, env.gopts) } else { - rtest.Assert(t, withTermStatus(env.gopts, func(ctx context.Context, term *termstatus.Terminal) error { + rtest.Assert(t, withTermStatus(env.gopts, func(ctx context.Context, term ui.Terminal) error { _, err := runCheck(context.TODO(), optionsCheck, env.gopts, nil, term) return err }) != nil, @@ -242,7 +242,7 @@ func testEdgeCaseRepo(t *testing.T, tarfile string, optionsCheck CheckOptions, o testRunPrune(t, env.gopts, optionsPrune) testRunCheck(t, env.gopts) } else { - rtest.Assert(t, withTermStatus(env.gopts, func(ctx context.Context, term *termstatus.Terminal) error { + rtest.Assert(t, withTermStatus(env.gopts, func(ctx context.Context, term ui.Terminal) error { return runPrune(context.TODO(), optionsPrune, env.gopts, term) }) != nil, "prune should have reported an error") diff --git a/cmd/restic/cmd_recover.go b/cmd/restic/cmd_recover.go index c1c71333f..274066eed 100644 --- a/cmd/restic/cmd_recover.go +++ b/cmd/restic/cmd_recover.go @@ -8,8 +8,8 @@ import ( "github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/repository" "github.com/restic/restic/internal/restic" + "github.com/restic/restic/internal/ui" "github.com/restic/restic/internal/ui/progress" - "github.com/restic/restic/internal/ui/termstatus" "github.com/spf13/cobra" "golang.org/x/sync/errgroup" ) @@ -43,20 +43,19 @@ Exit status is 12 if the password is incorrect. return cmd } -func runRecover(ctx context.Context, gopts GlobalOptions, term *termstatus.Terminal) error { +func runRecover(ctx context.Context, gopts GlobalOptions, term ui.Terminal) error { hostname, err := os.Hostname() if err != nil { return err } - ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, false) + printer := newTerminalProgressPrinter(false, gopts.verbosity, term) + ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, false, printer) if err != nil { return err } defer unlock() - printer := newTerminalProgressPrinter(gopts.verbosity, term) - snapshotLister, err := restic.MemorizeList(ctx, repo, restic.SnapshotFile) if err != nil { return err @@ -69,7 +68,7 @@ func runRecover(ctx context.Context, gopts GlobalOptions, term *termstatus.Termi } printer.P("load index files\n") - bar := newIndexTerminalProgress(gopts.Quiet, gopts.JSON, term) + bar := newIndexTerminalProgress(printer) if err = repo.LoadIndex(ctx, bar); err != nil { return err } @@ -88,7 +87,8 @@ func runRecover(ctx context.Context, gopts GlobalOptions, term *termstatus.Termi } printer.P("load %d trees\n", len(trees)) - bar = newTerminalProgressMax(!gopts.Quiet, uint64(len(trees)), "trees loaded", term) + bar = printer.NewCounter("trees loaded") + bar.SetMax(uint64(len(trees))) for id := range trees { tree, err := restic.LoadTree(ctx, repo, id) if ctx.Err() != nil { diff --git a/cmd/restic/cmd_recover_integration_test.go b/cmd/restic/cmd_recover_integration_test.go index c3d210200..91dec1505 100644 --- a/cmd/restic/cmd_recover_integration_test.go +++ b/cmd/restic/cmd_recover_integration_test.go @@ -5,11 +5,11 @@ import ( "testing" rtest "github.com/restic/restic/internal/test" - "github.com/restic/restic/internal/ui/termstatus" + "github.com/restic/restic/internal/ui" ) func testRunRecover(t testing.TB, gopts GlobalOptions) { - rtest.OK(t, withTermStatus(gopts, func(ctx context.Context, term *termstatus.Terminal) error { + rtest.OK(t, withTermStatus(gopts, func(ctx context.Context, term ui.Terminal) error { return runRecover(context.TODO(), gopts, term) })) } @@ -33,5 +33,7 @@ func TestRecover(t *testing.T) { ids = testListSnapshots(t, env.gopts, 1) testRunCheck(t, env.gopts) // check that the root tree is included in the snapshot - rtest.OK(t, runCat(context.TODO(), env.gopts, []string{"tree", ids[0].String() + ":" + sn.Tree.Str()})) + rtest.OK(t, withTermStatus(env.gopts, func(ctx context.Context, term ui.Terminal) error { + return runCat(context.TODO(), env.gopts, []string{"tree", ids[0].String() + ":" + sn.Tree.Str()}, term) + })) } diff --git a/cmd/restic/cmd_repair_index.go b/cmd/restic/cmd_repair_index.go index 079b322a9..52383f720 100644 --- a/cmd/restic/cmd_repair_index.go +++ b/cmd/restic/cmd_repair_index.go @@ -4,7 +4,7 @@ import ( "context" "github.com/restic/restic/internal/repository" - "github.com/restic/restic/internal/ui/termstatus" + "github.com/restic/restic/internal/ui" "github.com/spf13/cobra" "github.com/spf13/pflag" ) @@ -72,15 +72,15 @@ func newRebuildIndexCommand() *cobra.Command { return cmd } -func runRebuildIndex(ctx context.Context, opts RepairIndexOptions, gopts GlobalOptions, term *termstatus.Terminal) error { - ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, false) +func runRebuildIndex(ctx context.Context, opts RepairIndexOptions, gopts GlobalOptions, term ui.Terminal) error { + printer := newTerminalProgressPrinter(false, gopts.verbosity, term) + + ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, false, printer) if err != nil { return err } defer unlock() - printer := newTerminalProgressPrinter(gopts.verbosity, term) - err = repository.RepairIndex(ctx, repo, repository.RepairIndexOptions{ ReadAllPacks: opts.ReadAllPacks, }, printer) diff --git a/cmd/restic/cmd_repair_index_integration_test.go b/cmd/restic/cmd_repair_index_integration_test.go index 2b76bf4b3..6d1e81eee 100644 --- a/cmd/restic/cmd_repair_index_integration_test.go +++ b/cmd/restic/cmd_repair_index_integration_test.go @@ -13,12 +13,12 @@ import ( "github.com/restic/restic/internal/repository/index" "github.com/restic/restic/internal/restic" rtest "github.com/restic/restic/internal/test" - "github.com/restic/restic/internal/ui/termstatus" + "github.com/restic/restic/internal/ui" ) func testRunRebuildIndex(t testing.TB, gopts GlobalOptions) { rtest.OK(t, withRestoreGlobalOptions(func() error { - return withTermStatus(gopts, func(ctx context.Context, term *termstatus.Terminal) error { + return withTermStatus(gopts, func(ctx context.Context, term ui.Terminal) error { globalOptions.stdout = io.Discard return runRebuildIndex(context.TODO(), RepairIndexOptions{}, gopts, term) }) @@ -132,7 +132,7 @@ func TestRebuildIndexFailsOnAppendOnly(t *testing.T) { env.gopts.backendTestHook = func(r backend.Backend) (backend.Backend, error) { return &appendOnlyBackend{r}, nil } - return withTermStatus(env.gopts, func(ctx context.Context, term *termstatus.Terminal) error { + return withTermStatus(env.gopts, func(ctx context.Context, term ui.Terminal) error { globalOptions.stdout = io.Discard return runRebuildIndex(context.TODO(), RepairIndexOptions{}, env.gopts, term) }) diff --git a/cmd/restic/cmd_repair_packs.go b/cmd/restic/cmd_repair_packs.go index aaaa2f08f..e8d6a1196 100644 --- a/cmd/restic/cmd_repair_packs.go +++ b/cmd/restic/cmd_repair_packs.go @@ -9,7 +9,7 @@ import ( "github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/repository" "github.com/restic/restic/internal/restic" - "github.com/restic/restic/internal/ui/termstatus" + "github.com/restic/restic/internal/ui" "github.com/spf13/cobra" ) @@ -40,7 +40,7 @@ Exit status is 12 if the password is incorrect. return cmd } -func runRepairPacks(ctx context.Context, gopts GlobalOptions, term *termstatus.Terminal, args []string) error { +func runRepairPacks(ctx context.Context, gopts GlobalOptions, term ui.Terminal, args []string) error { ids := restic.NewIDSet() for _, arg := range args { id, err := restic.ParseID(arg) @@ -53,15 +53,15 @@ func runRepairPacks(ctx context.Context, gopts GlobalOptions, term *termstatus.T return errors.Fatal("no ids specified") } - ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, false) + printer := newTerminalProgressPrinter(false, gopts.verbosity, term) + + ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, false, printer) if err != nil { return err } defer unlock() - printer := newTerminalProgressPrinter(gopts.verbosity, term) - - bar := newIndexTerminalProgress(gopts.Quiet, gopts.JSON, term) + bar := newIndexTerminalProgress(printer) err = repo.LoadIndex(ctx, bar) if err != nil { return errors.Fatalf("%s", err) @@ -93,6 +93,6 @@ func runRepairPacks(ctx context.Context, gopts GlobalOptions, term *termstatus.T return errors.Fatalf("%s", err) } - Warnf("\nUse `restic repair snapshots --forget` to remove the corrupted data blobs from all snapshots\n") + printer.E("\nUse `restic repair snapshots --forget` to remove the corrupted data blobs from all snapshots") return nil } diff --git a/cmd/restic/cmd_repair_snapshots.go b/cmd/restic/cmd_repair_snapshots.go index 95506a400..49ab7b151 100644 --- a/cmd/restic/cmd_repair_snapshots.go +++ b/cmd/restic/cmd_repair_snapshots.go @@ -5,6 +5,7 @@ import ( "github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/restic" + "github.com/restic/restic/internal/ui" "github.com/restic/restic/internal/walker" "github.com/spf13/cobra" @@ -49,7 +50,9 @@ Exit status is 12 if the password is incorrect. `, DisableAutoGenTag: true, RunE: func(cmd *cobra.Command, args []string) error { - return runRepairSnapshots(cmd.Context(), globalOptions, opts, args) + term, cancel := setupTermstatus() + defer cancel() + return runRepairSnapshots(cmd.Context(), globalOptions, opts, args, term) }, } @@ -72,8 +75,10 @@ func (opts *RepairOptions) AddFlags(f *pflag.FlagSet) { initMultiSnapshotFilter(f, &opts.SnapshotFilter, true) } -func runRepairSnapshots(ctx context.Context, gopts GlobalOptions, opts RepairOptions, args []string) error { - ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, opts.DryRun) +func runRepairSnapshots(ctx context.Context, gopts GlobalOptions, opts RepairOptions, args []string, term ui.Terminal) error { + printer := newTerminalProgressPrinter(false, gopts.verbosity, term) + + ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, opts.DryRun, printer) if err != nil { return err } @@ -84,7 +89,7 @@ func runRepairSnapshots(ctx context.Context, gopts GlobalOptions, opts RepairOpt return err } - bar := newIndexProgress(gopts.Quiet, gopts.JSON) + bar := newIndexTerminalProgress(printer) if err := repo.LoadIndex(ctx, bar); err != nil { return err } @@ -96,7 +101,7 @@ func runRepairSnapshots(ctx context.Context, gopts GlobalOptions, opts RepairOpt rewriter := walker.NewTreeRewriter(walker.RewriteOpts{ RewriteNode: func(node *restic.Node, path string) *restic.Node { if node.Type == restic.NodeTypeIrregular || node.Type == restic.NodeTypeInvalid { - Verbosef(" file %q: removed node with invalid type %q\n", path, node.Type) + printer.P(" file %q: removed node with invalid type %q", path, node.Type) return nil } if node.Type != restic.NodeTypeFile { @@ -116,9 +121,9 @@ func runRepairSnapshots(ctx context.Context, gopts GlobalOptions, opts RepairOpt } } if !ok { - Verbosef(" file %q: removed missing content\n", path) + printer.P(" file %q: removed missing content", path) } else if newSize != node.Size { - Verbosef(" file %q: fixed incorrect size\n", path) + printer.P(" file %q: fixed incorrect size", path) } // no-ops if already correct node.Content = newContent @@ -127,12 +132,12 @@ func runRepairSnapshots(ctx context.Context, gopts GlobalOptions, opts RepairOpt }, RewriteFailedTree: func(_ restic.ID, path string, _ error) (restic.ID, error) { if path == "/" { - Verbosef(" dir %q: not readable\n", path) + printer.P(" dir %q: not readable", path) // remove snapshots with invalid root node return restic.ID{}, nil } // If a subtree fails to load, remove it - Verbosef(" dir %q: replaced with empty directory\n", path) + printer.P(" dir %q: replaced with empty directory", path) emptyID, err := restic.SaveTree(ctx, repo, &restic.Tree{}) if err != nil { return restic.ID{}, err @@ -143,13 +148,13 @@ func runRepairSnapshots(ctx context.Context, gopts GlobalOptions, opts RepairOpt }) changedCount := 0 - for sn := range FindFilteredSnapshots(ctx, snapshotLister, repo, &opts.SnapshotFilter, args) { - Verbosef("\n%v\n", sn) + for sn := range FindFilteredSnapshots(ctx, snapshotLister, repo, &opts.SnapshotFilter, args, printer) { + printer.P("\n%v", sn) changed, err := filterAndReplaceSnapshot(ctx, repo, sn, func(ctx context.Context, sn *restic.Snapshot) (restic.ID, *restic.SnapshotSummary, error) { id, err := rewriter.RewriteTree(ctx, repo, "/", *sn.Tree) return id, nil, err - }, opts.DryRun, opts.Forget, nil, "repaired") + }, opts.DryRun, opts.Forget, nil, "repaired", printer) if err != nil { return errors.Fatalf("unable to rewrite snapshot ID %q: %v", sn.ID().Str(), err) } @@ -161,18 +166,18 @@ func runRepairSnapshots(ctx context.Context, gopts GlobalOptions, opts RepairOpt return ctx.Err() } - Verbosef("\n") + printer.P("") if changedCount == 0 { if !opts.DryRun { - Verbosef("no snapshots were modified\n") + printer.P("no snapshots were modified") } else { - Verbosef("no snapshots would be modified\n") + printer.P("no snapshots would be modified") } } else { if !opts.DryRun { - Verbosef("modified %v snapshots\n", changedCount) + printer.P("modified %v snapshots", changedCount) } else { - Verbosef("would modify %v snapshots\n", changedCount) + printer.P("would modify %v snapshots", changedCount) } } diff --git a/cmd/restic/cmd_repair_snapshots_integration_test.go b/cmd/restic/cmd_repair_snapshots_integration_test.go index 9f65c9328..6594d211c 100644 --- a/cmd/restic/cmd_repair_snapshots_integration_test.go +++ b/cmd/restic/cmd_repair_snapshots_integration_test.go @@ -12,6 +12,7 @@ import ( "github.com/restic/restic/internal/restic" rtest "github.com/restic/restic/internal/test" + "github.com/restic/restic/internal/ui" ) func testRunRepairSnapshot(t testing.TB, gopts GlobalOptions, forget bool) { @@ -19,7 +20,9 @@ func testRunRepairSnapshot(t testing.TB, gopts GlobalOptions, forget bool) { Forget: forget, } - rtest.OK(t, runRepairSnapshots(context.TODO(), gopts, opts, nil)) + rtest.OK(t, withTermStatus(gopts, func(ctx context.Context, term ui.Terminal) error { + return runRepairSnapshots(context.TODO(), gopts, opts, nil, term) + })) } func createRandomFile(t testing.TB, env *testEnvironment, path string, size int) { @@ -77,7 +80,7 @@ func TestRepairSnapshotsWithLostTree(t *testing.T) { createRandomFile(t, env, "foo/bar/file", 12345) testRunBackup(t, "", []string{env.testdata}, BackupOptions{}, env.gopts) oldSnapshot := testListSnapshots(t, env.gopts, 1) - oldPacks := testRunList(t, "packs", env.gopts) + oldPacks := testRunList(t, env.gopts, "packs") // keep foo/bar unchanged createRandomFile(t, env, "foo/bar2", 1024) @@ -106,7 +109,7 @@ func TestRepairSnapshotsWithLostRootTree(t *testing.T) { createRandomFile(t, env, "foo/bar/file", 12345) testRunBackup(t, "", []string{env.testdata}, BackupOptions{}, env.gopts) testListSnapshots(t, env.gopts, 1) - oldPacks := testRunList(t, "packs", env.gopts) + oldPacks := testRunList(t, env.gopts, "packs") // remove all trees removePacks(env.gopts, t, restic.NewIDSet(oldPacks...)) diff --git a/cmd/restic/cmd_restore.go b/cmd/restic/cmd_restore.go index f0c84c628..915829b0c 100644 --- a/cmd/restic/cmd_restore.go +++ b/cmd/restic/cmd_restore.go @@ -10,10 +10,9 @@ import ( "github.com/restic/restic/internal/filter" "github.com/restic/restic/internal/restic" "github.com/restic/restic/internal/restorer" - "github.com/restic/restic/internal/terminal" "github.com/restic/restic/internal/ui" + "github.com/restic/restic/internal/ui/progress" restoreui "github.com/restic/restic/internal/ui/restore" - "github.com/restic/restic/internal/ui/termstatus" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -90,14 +89,15 @@ func (opts *RestoreOptions) AddFlags(f *pflag.FlagSet) { } func runRestore(ctx context.Context, opts RestoreOptions, gopts GlobalOptions, - term *termstatus.Terminal, args []string) error { + term ui.Terminal, args []string) error { - excludePatternFns, err := opts.ExcludePatternOptions.CollectPatterns(Warnf) + msg := newTerminalProgressPrinter(gopts.JSON, gopts.verbosity, term) + excludePatternFns, err := opts.ExcludePatternOptions.CollectPatterns(msg.E) if err != nil { return err } - includePatternFns, err := opts.IncludePatternOptions.CollectPatterns(Warnf) + includePatternFns, err := opts.IncludePatternOptions.CollectPatterns(msg.E) if err != nil { return err } @@ -132,7 +132,7 @@ func runRestore(ctx context.Context, opts RestoreOptions, gopts GlobalOptions, debug.Log("restore %v to %v", snapshotIDString, opts.Target) - ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock) + ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock, msg) if err != nil { return err } @@ -147,7 +147,7 @@ func runRestore(ctx context.Context, opts RestoreOptions, gopts GlobalOptions, return errors.Fatalf("failed to find snapshot: %v", err) } - bar := newIndexTerminalProgress(gopts.Quiet, gopts.JSON, term) + bar := newIndexTerminalProgress(msg) err = repo.LoadIndex(ctx, bar) if err != nil { return err @@ -158,7 +158,6 @@ func runRestore(ctx context.Context, opts RestoreOptions, gopts GlobalOptions, return err } - msg := ui.NewMessage(term, gopts.verbosity) var printer restoreui.ProgressPrinter if gopts.JSON { printer = restoreui.NewJSONProgress(term, gopts.verbosity) @@ -235,7 +234,7 @@ func runRestore(ctx context.Context, opts RestoreOptions, gopts GlobalOptions, res.SelectFilter = selectIncludeFilter } - res.XattrSelectFilter, err = getXattrSelectFilter(opts) + res.XattrSelectFilter, err = getXattrSelectFilter(opts, msg) if err != nil { return err } @@ -261,7 +260,7 @@ func runRestore(ctx context.Context, opts RestoreOptions, gopts GlobalOptions, } var count int t0 := time.Now() - bar := newTerminalProgressMax(!gopts.Quiet && !gopts.JSON && terminal.StdoutIsTerminal(), 0, "files verified", term) + bar := msg.NewCounterTerminalOnly("files verified") count, err = res.VerifyFiles(ctx, opts.Target, countRestoredFiles, bar) if err != nil { return err @@ -279,7 +278,7 @@ func runRestore(ctx context.Context, opts RestoreOptions, gopts GlobalOptions, return nil } -func getXattrSelectFilter(opts RestoreOptions) (func(xattrName string) bool, error) { +func getXattrSelectFilter(opts RestoreOptions, printer progress.Printer) (func(xattrName string) bool, error) { hasXattrExcludes := len(opts.ExcludeXattrPattern) > 0 hasXattrIncludes := len(opts.IncludeXattrPattern) > 0 @@ -293,7 +292,7 @@ func getXattrSelectFilter(opts RestoreOptions) (func(xattrName string) bool, err } return func(xattrName string) bool { - shouldReject := filter.RejectByPattern(opts.ExcludeXattrPattern, Warnf)(xattrName) + shouldReject := filter.RejectByPattern(opts.ExcludeXattrPattern, printer.E)(xattrName) return !shouldReject }, nil } @@ -305,7 +304,7 @@ func getXattrSelectFilter(opts RestoreOptions) (func(xattrName string) bool, err } return func(xattrName string) bool { - shouldInclude, _ := filter.IncludeByPattern(opts.IncludeXattrPattern, Warnf)(xattrName) + shouldInclude, _ := filter.IncludeByPattern(opts.IncludeXattrPattern, printer.E)(xattrName) return shouldInclude }, nil } diff --git a/cmd/restic/cmd_restore_integration_test.go b/cmd/restic/cmd_restore_integration_test.go index 945c24a37..9746b28af 100644 --- a/cmd/restic/cmd_restore_integration_test.go +++ b/cmd/restic/cmd_restore_integration_test.go @@ -14,7 +14,7 @@ import ( "github.com/restic/restic/internal/restic" rtest "github.com/restic/restic/internal/test" - "github.com/restic/restic/internal/ui/termstatus" + "github.com/restic/restic/internal/ui" ) func testRunRestore(t testing.TB, opts GlobalOptions, dir string, snapshotID string) { @@ -31,7 +31,7 @@ func testRunRestoreExcludes(t testing.TB, gopts GlobalOptions, dir string, snaps } func testRunRestoreAssumeFailure(snapshotID string, opts RestoreOptions, gopts GlobalOptions) error { - return withTermStatus(gopts, func(ctx context.Context, term *termstatus.Terminal) error { + return withTermStatus(gopts, func(ctx context.Context, term ui.Terminal) error { return runRestore(ctx, opts, gopts, term, []string{snapshotID}) }) } diff --git a/cmd/restic/cmd_rewrite.go b/cmd/restic/cmd_rewrite.go index 4e5b39932..b2d771bb4 100644 --- a/cmd/restic/cmd_rewrite.go +++ b/cmd/restic/cmd_rewrite.go @@ -13,6 +13,8 @@ import ( "github.com/restic/restic/internal/filter" "github.com/restic/restic/internal/repository" "github.com/restic/restic/internal/restic" + "github.com/restic/restic/internal/ui" + "github.com/restic/restic/internal/ui/progress" "github.com/restic/restic/internal/walker" ) @@ -58,7 +60,9 @@ Exit status is 12 if the password is incorrect. GroupID: cmdGroupDefault, DisableAutoGenTag: true, RunE: func(cmd *cobra.Command, args []string) error { - return runRewrite(cmd.Context(), opts, globalOptions, args) + term, cancel := setupTermstatus() + defer cancel() + return runRewrite(cmd.Context(), opts, globalOptions, args, term) }, } @@ -122,12 +126,12 @@ func (opts *RewriteOptions) AddFlags(f *pflag.FlagSet) { // be updated accordingly. type rewriteFilterFunc func(ctx context.Context, sn *restic.Snapshot) (restic.ID, *restic.SnapshotSummary, error) -func rewriteSnapshot(ctx context.Context, repo *repository.Repository, sn *restic.Snapshot, opts RewriteOptions) (bool, error) { +func rewriteSnapshot(ctx context.Context, repo *repository.Repository, sn *restic.Snapshot, opts RewriteOptions, printer progress.Printer) (bool, error) { if sn.Tree == nil { return false, errors.Errorf("snapshot %v has nil tree", sn.ID().Str()) } - rejectByNameFuncs, err := opts.ExcludePatternOptions.CollectPatterns(Warnf) + rejectByNameFuncs, err := opts.ExcludePatternOptions.CollectPatterns(printer.E) if err != nil { return false, err } @@ -154,7 +158,7 @@ func rewriteSnapshot(ctx context.Context, repo *repository.Repository, sn *resti if selectByName(path) { return node } - Verbosef("excluding %s\n", path) + printer.P("excluding %s", path) return nil } @@ -182,11 +186,11 @@ func rewriteSnapshot(ctx context.Context, repo *repository.Repository, sn *resti } return filterAndReplaceSnapshot(ctx, repo, sn, - filter, opts.DryRun, opts.Forget, metadata, "rewrite") + filter, opts.DryRun, opts.Forget, metadata, "rewrite", printer) } func filterAndReplaceSnapshot(ctx context.Context, repo restic.Repository, sn *restic.Snapshot, - filter rewriteFilterFunc, dryRun bool, forget bool, newMetadata *snapshotMetadata, addTag string) (bool, error) { + filter rewriteFilterFunc, dryRun bool, forget bool, newMetadata *snapshotMetadata, addTag string, printer progress.Printer) (bool, error) { wg, wgCtx := errgroup.WithContext(ctx) repo.StartPackUploader(wgCtx, wg) @@ -209,13 +213,13 @@ func filterAndReplaceSnapshot(ctx context.Context, repo restic.Repository, sn *r if filteredTree.IsNull() { if dryRun { - Verbosef("would delete empty snapshot\n") + printer.P("would delete empty snapshot") } else { if err = repo.RemoveUnpacked(ctx, restic.WriteableSnapshotFile, *sn.ID()); err != nil { return false, err } debug.Log("removed empty snapshot %v", sn.ID()) - Verbosef("removed empty snapshot %v\n", sn.ID().Str()) + printer.P("removed empty snapshot %v", sn.ID().Str()) } return true, nil } @@ -232,18 +236,18 @@ func filterAndReplaceSnapshot(ctx context.Context, repo restic.Repository, sn *r debug.Log("Snapshot %v modified", sn) if dryRun { - Verbosef("would save new snapshot\n") + printer.P("would save new snapshot") if forget { - Verbosef("would remove old snapshot\n") + printer.P("would remove old snapshot") } if newMetadata != nil && newMetadata.Time != nil { - Verbosef("would set time to %s\n", newMetadata.Time) + printer.P("would set time to %s", newMetadata.Time) } if newMetadata != nil && newMetadata.Hostname != "" { - Verbosef("would set hostname to %s\n", newMetadata.Hostname) + printer.P("would set hostname to %s", newMetadata.Hostname) } return true, nil @@ -261,12 +265,12 @@ func filterAndReplaceSnapshot(ctx context.Context, repo restic.Repository, sn *r } if newMetadata != nil && newMetadata.Time != nil { - Verbosef("setting time to %s\n", *newMetadata.Time) + printer.P("setting time to %s", *newMetadata.Time) sn.Time = *newMetadata.Time } if newMetadata != nil && newMetadata.Hostname != "" { - Verbosef("setting host to %s\n", newMetadata.Hostname) + printer.P("setting host to %s", newMetadata.Hostname) sn.Hostname = newMetadata.Hostname } @@ -275,23 +279,25 @@ func filterAndReplaceSnapshot(ctx context.Context, repo restic.Repository, sn *r if err != nil { return false, err } - Verbosef("saved new snapshot %v\n", id.Str()) + printer.P("saved new snapshot %v", id.Str()) if forget { if err = repo.RemoveUnpacked(ctx, restic.WriteableSnapshotFile, *sn.ID()); err != nil { return false, err } debug.Log("removed old snapshot %v", sn.ID()) - Verbosef("removed old snapshot %v\n", sn.ID().Str()) + printer.P("removed old snapshot %v", sn.ID().Str()) } return true, nil } -func runRewrite(ctx context.Context, opts RewriteOptions, gopts GlobalOptions, args []string) error { +func runRewrite(ctx context.Context, opts RewriteOptions, gopts GlobalOptions, args []string, term ui.Terminal) error { if !opts.SnapshotSummary && opts.ExcludePatternOptions.Empty() && opts.Metadata.empty() { return errors.Fatal("Nothing to do: no excludes provided and no new metadata provided") } + printer := newTerminalProgressPrinter(false, gopts.verbosity, term) + var ( repo *repository.Repository unlock func() @@ -299,10 +305,10 @@ func runRewrite(ctx context.Context, opts RewriteOptions, gopts GlobalOptions, a ) if opts.Forget { - Verbosef("create exclusive lock for repository\n") - ctx, repo, unlock, err = openWithExclusiveLock(ctx, gopts, opts.DryRun) + printer.P("create exclusive lock for repository") + ctx, repo, unlock, err = openWithExclusiveLock(ctx, gopts, opts.DryRun, printer) } else { - ctx, repo, unlock, err = openWithAppendLock(ctx, gopts, opts.DryRun) + ctx, repo, unlock, err = openWithAppendLock(ctx, gopts, opts.DryRun, printer) } if err != nil { return err @@ -314,15 +320,15 @@ func runRewrite(ctx context.Context, opts RewriteOptions, gopts GlobalOptions, a return err } - bar := newIndexProgress(gopts.Quiet, gopts.JSON) + bar := newIndexTerminalProgress(printer) if err = repo.LoadIndex(ctx, bar); err != nil { return err } changedCount := 0 - for sn := range FindFilteredSnapshots(ctx, snapshotLister, repo, &opts.SnapshotFilter, args) { - Verbosef("\n%v\n", sn) - changed, err := rewriteSnapshot(ctx, repo, sn, opts) + for sn := range FindFilteredSnapshots(ctx, snapshotLister, repo, &opts.SnapshotFilter, args, printer) { + printer.P("\n%v", sn) + changed, err := rewriteSnapshot(ctx, repo, sn, opts, printer) if err != nil { return errors.Fatalf("unable to rewrite snapshot ID %q: %v", sn.ID().Str(), err) } @@ -334,18 +340,18 @@ func runRewrite(ctx context.Context, opts RewriteOptions, gopts GlobalOptions, a return ctx.Err() } - Verbosef("\n") + printer.P("") if changedCount == 0 { if !opts.DryRun { - Verbosef("no snapshots were modified\n") + printer.P("no snapshots were modified") } else { - Verbosef("no snapshots would be modified\n") + printer.P("no snapshots would be modified") } } else { if !opts.DryRun { - Verbosef("modified %v snapshots\n", changedCount) + printer.P("modified %v snapshots", changedCount) } else { - Verbosef("would modify %v snapshots\n", changedCount) + printer.P("would modify %v snapshots", changedCount) } } diff --git a/cmd/restic/cmd_rewrite_integration_test.go b/cmd/restic/cmd_rewrite_integration_test.go index 188353333..f011cdcc1 100644 --- a/cmd/restic/cmd_rewrite_integration_test.go +++ b/cmd/restic/cmd_rewrite_integration_test.go @@ -20,7 +20,9 @@ func testRunRewriteExclude(t testing.TB, gopts GlobalOptions, excludes []string, Metadata: metadata, } - rtest.OK(t, runRewrite(context.TODO(), opts, gopts, nil)) + rtest.OK(t, withTermStatus(gopts, func(ctx context.Context, term ui.Terminal) error { + return runRewrite(context.TODO(), opts, gopts, nil, term) + })) } func createBasicRewriteRepo(t testing.TB, env *testEnvironment) restic.ID { @@ -28,7 +30,7 @@ func createBasicRewriteRepo(t testing.TB, env *testEnvironment) restic.ID { // create backup testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, BackupOptions{}, env.gopts) - snapshotIDs := testRunList(t, "snapshots", env.gopts) + snapshotIDs := testRunList(t, env.gopts, "snapshots") rtest.Assert(t, len(snapshotIDs) == 1, "expected one snapshot, got %v", snapshotIDs) testRunCheck(t, env.gopts) @@ -38,11 +40,16 @@ func createBasicRewriteRepo(t testing.TB, env *testEnvironment) restic.ID { func getSnapshot(t testing.TB, snapshotID restic.ID, env *testEnvironment) *restic.Snapshot { t.Helper() - ctx, repo, unlock, err := openWithReadLock(context.TODO(), env.gopts, false) - rtest.OK(t, err) - defer unlock() + var snapshots []*restic.Snapshot + err := withTermStatus(env.gopts, func(ctx context.Context, term ui.Terminal) error { + printer := newTerminalProgressPrinter(env.gopts.JSON, env.gopts.verbosity, term) + ctx, repo, unlock, err := openWithReadLock(ctx, env.gopts, false, printer) + rtest.OK(t, err) + defer unlock() - snapshots, err := restic.TestLoadAllSnapshots(ctx, repo, nil) + snapshots, err = restic.TestLoadAllSnapshots(ctx, repo, nil) + return err + }) rtest.OK(t, err) for _, s := range snapshots { @@ -60,7 +67,7 @@ func TestRewrite(t *testing.T) { // exclude some data testRunRewriteExclude(t, env.gopts, []string{"3"}, false, snapshotMetadataArgs{Hostname: "", Time: ""}) - snapshotIDs := testRunList(t, "snapshots", env.gopts) + snapshotIDs := testRunList(t, env.gopts, "snapshots") rtest.Assert(t, len(snapshotIDs) == 2, "expected two snapshots, got %v", snapshotIDs) testRunCheck(t, env.gopts) } @@ -72,7 +79,7 @@ func TestRewriteUnchanged(t *testing.T) { // use an exclude that will not exclude anything testRunRewriteExclude(t, env.gopts, []string{"3dflkhjgdflhkjetrlkhjgfdlhkj"}, false, snapshotMetadataArgs{Hostname: "", Time: ""}) - newSnapshotIDs := testRunList(t, "snapshots", env.gopts) + newSnapshotIDs := testRunList(t, env.gopts, "snapshots") rtest.Assert(t, len(newSnapshotIDs) == 1, "expected one snapshot, got %v", newSnapshotIDs) rtest.Assert(t, snapshotID == newSnapshotIDs[0], "snapshot id changed unexpectedly") testRunCheck(t, env.gopts) @@ -109,11 +116,16 @@ func testRewriteMetadata(t *testing.T, metadata snapshotMetadataArgs) { createBasicRewriteRepo(t, env) testRunRewriteExclude(t, env.gopts, []string{}, true, metadata) - ctx, repo, unlock, err := openWithReadLock(context.TODO(), env.gopts, false) - rtest.OK(t, err) - defer unlock() + var snapshots []*restic.Snapshot + err := withTermStatus(env.gopts, func(ctx context.Context, term ui.Terminal) error { + printer := newTerminalProgressPrinter(env.gopts.JSON, env.gopts.verbosity, term) + ctx, repo, unlock, err := openWithReadLock(ctx, env.gopts, false, printer) + rtest.OK(t, err) + defer unlock() - snapshots, err := restic.TestLoadAllSnapshots(ctx, repo, nil) + snapshots, err = restic.TestLoadAllSnapshots(ctx, repo, nil) + return err + }) rtest.OK(t, err) rtest.Assert(t, len(snapshots) == 1, "expected one snapshot, got %v", len(snapshots)) newSnapshot := snapshots[0] @@ -145,30 +157,39 @@ func TestRewriteSnaphotSummary(t *testing.T) { defer cleanup() createBasicRewriteRepo(t, env) - rtest.OK(t, runRewrite(context.TODO(), RewriteOptions{SnapshotSummary: true}, env.gopts, []string{})) + rtest.OK(t, withTermStatus(env.gopts, func(ctx context.Context, term ui.Terminal) error { + return runRewrite(context.TODO(), RewriteOptions{SnapshotSummary: true}, env.gopts, []string{}, term) + })) // no new snapshot should be created as the snapshot already has a summary snapshots := testListSnapshots(t, env.gopts, 1) // replace snapshot by one without a summary - _, repo, unlock, err := openWithExclusiveLock(context.TODO(), env.gopts, false) + var oldSummary *restic.SnapshotSummary + err := withTermStatus(env.gopts, func(ctx context.Context, term ui.Terminal) error { + printer := newTerminalProgressPrinter(env.gopts.JSON, env.gopts.verbosity, term) + _, repo, unlock, err := openWithExclusiveLock(ctx, env.gopts, false, printer) + rtest.OK(t, err) + defer unlock() + + sn, err := restic.LoadSnapshot(ctx, repo, snapshots[0]) + rtest.OK(t, err) + oldSummary = sn.Summary + sn.Summary = nil + rtest.OK(t, repo.RemoveUnpacked(ctx, restic.WriteableSnapshotFile, snapshots[0])) + snapshots[0], err = restic.SaveSnapshot(ctx, repo, sn) + return err + }) rtest.OK(t, err) - sn, err := restic.LoadSnapshot(context.TODO(), repo, snapshots[0]) - rtest.OK(t, err) - oldSummary := sn.Summary - sn.Summary = nil - rtest.OK(t, repo.RemoveUnpacked(context.TODO(), restic.WriteableSnapshotFile, snapshots[0])) - snapshots[0], err = restic.SaveSnapshot(context.TODO(), repo, sn) - rtest.OK(t, err) - unlock() // rewrite snapshot and lookup ID of new snapshot - rtest.OK(t, runRewrite(context.TODO(), RewriteOptions{SnapshotSummary: true}, env.gopts, []string{})) + rtest.OK(t, withTermStatus(env.gopts, func(ctx context.Context, term ui.Terminal) error { + return runRewrite(context.TODO(), RewriteOptions{SnapshotSummary: true}, env.gopts, []string{}, term) + })) newSnapshots := testListSnapshots(t, env.gopts, 2) newSnapshot := restic.NewIDSet(newSnapshots...).Sub(restic.NewIDSet(snapshots...)).List()[0] - sn, err = restic.LoadSnapshot(context.TODO(), repo, newSnapshot) - rtest.OK(t, err) - rtest.Assert(t, sn.Summary != nil, "snapshot should have summary attached") - rtest.Equals(t, oldSummary.TotalBytesProcessed, sn.Summary.TotalBytesProcessed, "unexpected TotalBytesProcessed value") - rtest.Equals(t, oldSummary.TotalFilesProcessed, sn.Summary.TotalFilesProcessed, "unexpected TotalFilesProcessed value") + newSn := testLoadSnapshot(t, env.gopts, newSnapshot) + rtest.Assert(t, newSn.Summary != nil, "snapshot should have summary attached") + rtest.Equals(t, oldSummary.TotalBytesProcessed, newSn.Summary.TotalBytesProcessed, "unexpected TotalBytesProcessed value") + rtest.Equals(t, oldSummary.TotalFilesProcessed, newSn.Summary.TotalFilesProcessed, "unexpected TotalFilesProcessed value") } diff --git a/cmd/restic/cmd_self_update.go b/cmd/restic/cmd_self_update.go index 2247274ac..b4173ab78 100644 --- a/cmd/restic/cmd_self_update.go +++ b/cmd/restic/cmd_self_update.go @@ -9,6 +9,7 @@ import ( "github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/selfupdate" + "github.com/restic/restic/internal/ui" "github.com/spf13/cobra" "github.com/spf13/pflag" ) @@ -42,7 +43,9 @@ Exit status is 12 if the password is incorrect. `, DisableAutoGenTag: true, RunE: func(cmd *cobra.Command, args []string) error { - return runSelfUpdate(cmd.Context(), opts, globalOptions, args) + term, cancel := setupTermstatus() + defer cancel() + return runSelfUpdate(cmd.Context(), opts, globalOptions, args, term) }, } @@ -59,7 +62,7 @@ func (opts *SelfUpdateOptions) AddFlags(f *pflag.FlagSet) { f.StringVar(&opts.Output, "output", "", "Save the downloaded file as `filename` (default: running binary itself)") } -func runSelfUpdate(ctx context.Context, opts SelfUpdateOptions, gopts GlobalOptions, args []string) error { +func runSelfUpdate(ctx context.Context, opts SelfUpdateOptions, gopts GlobalOptions, args []string, term ui.Terminal) error { if opts.Output == "" { file, err := os.Executable() if err != nil { @@ -85,15 +88,16 @@ func runSelfUpdate(ctx context.Context, opts SelfUpdateOptions, gopts GlobalOpti } } - Verbosef("writing restic to %v\n", opts.Output) + printer := newTerminalProgressPrinter(false, gopts.verbosity, term) + printer.P("writing restic to %v", opts.Output) - v, err := selfupdate.DownloadLatestStableRelease(ctx, opts.Output, version, Verbosef) + v, err := selfupdate.DownloadLatestStableRelease(ctx, opts.Output, version, printer.P) if err != nil { return errors.Fatalf("unable to update restic: %v", err) } if v != version { - Printf("successfully updated restic to version %v\n", v) + printer.S("successfully updated restic to version %v", v) } return nil diff --git a/cmd/restic/cmd_snapshots.go b/cmd/restic/cmd_snapshots.go index 8c12ac017..5ce82c996 100644 --- a/cmd/restic/cmd_snapshots.go +++ b/cmd/restic/cmd_snapshots.go @@ -36,7 +36,9 @@ Exit status is 12 if the password is incorrect. GroupID: cmdGroupDefault, DisableAutoGenTag: true, RunE: func(cmd *cobra.Command, args []string) error { - return runSnapshots(cmd.Context(), opts, globalOptions, args) + term, cancel := setupTermstatus() + defer cancel() + return runSnapshots(cmd.Context(), opts, globalOptions, args, term) }, } @@ -66,15 +68,16 @@ func (opts *SnapshotOptions) AddFlags(f *pflag.FlagSet) { f.VarP(&opts.GroupBy, "group-by", "g", "`group` snapshots by host, paths and/or tags, separated by comma") } -func runSnapshots(ctx context.Context, opts SnapshotOptions, gopts GlobalOptions, args []string) error { - ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock) +func runSnapshots(ctx context.Context, opts SnapshotOptions, gopts GlobalOptions, args []string, term ui.Terminal) error { + printer := newTerminalProgressPrinter(gopts.JSON, gopts.verbosity, term) + ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock, printer) if err != nil { return err } defer unlock() var snapshots restic.Snapshots - for sn := range FindFilteredSnapshots(ctx, repo, repo, &opts.SnapshotFilter, args) { + for sn := range FindFilteredSnapshots(ctx, repo, repo, &opts.SnapshotFilter, args, printer) { snapshots = append(snapshots, sn) } if ctx.Err() != nil { @@ -104,7 +107,7 @@ func runSnapshots(ctx context.Context, opts SnapshotOptions, gopts GlobalOptions if gopts.JSON { err := printSnapshotGroupJSON(globalOptions.stdout, snapshotGroups, grouped) if err != nil { - Warnf("error printing snapshots: %v\n", err) + printer.E("error printing snapshots: %v", err) } return nil } @@ -117,11 +120,13 @@ func runSnapshots(ctx context.Context, opts SnapshotOptions, gopts GlobalOptions if grouped { err := PrintSnapshotGroupHeader(globalOptions.stdout, k) if err != nil { - Warnf("error printing snapshots: %v\n", err) - return nil + return err } } - PrintSnapshots(globalOptions.stdout, list, nil, opts.Compact) + err := PrintSnapshots(globalOptions.stdout, list, nil, opts.Compact) + if err != nil { + return err + } } return nil @@ -165,7 +170,7 @@ func FilterLatestSnapshots(list restic.Snapshots, limit int) restic.Snapshots { } // PrintSnapshots prints a text table of the snapshots in list to stdout. -func PrintSnapshots(stdout io.Writer, list restic.Snapshots, reasons []restic.KeepReason, compact bool) { +func PrintSnapshots(stdout io.Writer, list restic.Snapshots, reasons []restic.KeepReason, compact bool) error { // keep the reasons a snasphot is being kept in a map, so that it doesn't // get lost when the list of snapshots is sorted keepReasons := make(map[restic.ID]restic.KeepReason, len(reasons)) @@ -277,10 +282,7 @@ func PrintSnapshots(stdout io.Writer, list restic.Snapshots, reasons []restic.Ke } } - err := tab.Write(stdout) - if err != nil { - Warnf("error printing: %v\n", err) - } + return tab.Write(stdout) } // PrintSnapshotGroupHeader prints which group of the group-by option the diff --git a/cmd/restic/cmd_snapshots_integration_test.go b/cmd/restic/cmd_snapshots_integration_test.go index 6eaa8faa4..a009b2908 100644 --- a/cmd/restic/cmd_snapshots_integration_test.go +++ b/cmd/restic/cmd_snapshots_integration_test.go @@ -7,14 +7,17 @@ import ( "github.com/restic/restic/internal/restic" rtest "github.com/restic/restic/internal/test" + "github.com/restic/restic/internal/ui" ) func testRunSnapshots(t testing.TB, gopts GlobalOptions) (newest *Snapshot, snapmap map[restic.ID]Snapshot) { - buf, err := withCaptureStdout(func() error { + buf, err := withCaptureStdout(gopts, func(gopts GlobalOptions) error { gopts.JSON = true opts := SnapshotOptions{} - return runSnapshots(context.TODO(), opts, gopts, []string{}) + return withTermStatus(gopts, func(ctx context.Context, term ui.Terminal) error { + return runSnapshots(ctx, opts, gopts, []string{}, term) + }) }) rtest.OK(t, err) diff --git a/cmd/restic/cmd_stats.go b/cmd/restic/cmd_stats.go index 36c567fee..00f885ffa 100644 --- a/cmd/restic/cmd_stats.go +++ b/cmd/restic/cmd_stats.go @@ -14,6 +14,7 @@ import ( "github.com/restic/restic/internal/restic" "github.com/restic/restic/internal/restorer" "github.com/restic/restic/internal/ui" + "github.com/restic/restic/internal/ui/progress" "github.com/restic/restic/internal/ui/table" "github.com/restic/restic/internal/walker" @@ -62,7 +63,9 @@ Exit status is 12 if the password is incorrect. GroupID: cmdGroupDefault, DisableAutoGenTag: true, RunE: func(cmd *cobra.Command, args []string) error { - return runStats(cmd.Context(), opts, globalOptions, args) + term, cancel := setupTermstatus() + defer cancel() + return runStats(cmd.Context(), opts, globalOptions, args, term) }, } @@ -92,13 +95,15 @@ func must(err error) { } } -func runStats(ctx context.Context, opts StatsOptions, gopts GlobalOptions, args []string) error { +func runStats(ctx context.Context, opts StatsOptions, gopts GlobalOptions, args []string, term ui.Terminal) error { err := verifyStatsInput(opts) if err != nil { return err } - ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock) + printer := newTerminalProgressPrinter(gopts.JSON, gopts.verbosity, term) + + ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock, printer) if err != nil { return err } @@ -108,17 +113,17 @@ func runStats(ctx context.Context, opts StatsOptions, gopts GlobalOptions, args if err != nil { return err } - bar := newIndexProgress(gopts.Quiet, gopts.JSON) + bar := newIndexTerminalProgress(printer) if err = repo.LoadIndex(ctx, bar); err != nil { return err } if opts.countMode == countModeDebug { - return statsDebug(ctx, repo) + return statsDebug(ctx, repo, printer) } if !gopts.JSON { - Printf("scanning...\n") + printer.S("scanning...") } // create a container for the stats (and other needed state) @@ -129,7 +134,7 @@ func runStats(ctx context.Context, opts StatsOptions, gopts GlobalOptions, args SnapshotsCount: 0, } - for sn := range FindFilteredSnapshots(ctx, snapshotLister, repo, &opts.SnapshotFilter, args) { + for sn := range FindFilteredSnapshots(ctx, snapshotLister, repo, &opts.SnapshotFilter, args, printer) { err = statsWalkSnapshot(ctx, sn, repo, opts, stats) if err != nil { return fmt.Errorf("error walking snapshot: %v", err) @@ -173,26 +178,26 @@ func runStats(ctx context.Context, opts StatsOptions, gopts GlobalOptions, args return nil } - Printf("Stats in %s mode:\n", opts.countMode) - Printf(" Snapshots processed: %d\n", stats.SnapshotsCount) + printer.S("Stats in %s mode:", opts.countMode) + printer.S(" Snapshots processed: %d", stats.SnapshotsCount) if stats.TotalBlobCount > 0 { - Printf(" Total Blob Count: %d\n", stats.TotalBlobCount) + printer.S(" Total Blob Count: %d", stats.TotalBlobCount) } if stats.TotalFileCount > 0 { - Printf(" Total File Count: %d\n", stats.TotalFileCount) + printer.S(" Total File Count: %d", stats.TotalFileCount) } if stats.TotalUncompressedSize > 0 { - Printf(" Total Uncompressed Size: %-5s\n", ui.FormatBytes(stats.TotalUncompressedSize)) + printer.S(" Total Uncompressed Size: %-5s", ui.FormatBytes(stats.TotalUncompressedSize)) } - Printf(" Total Size: %-5s\n", ui.FormatBytes(stats.TotalSize)) + printer.S(" Total Size: %-5s", ui.FormatBytes(stats.TotalSize)) if stats.CompressionProgress > 0 { - Printf(" Compression Progress: %.2f%%\n", stats.CompressionProgress) + printer.S(" Compression Progress: %.2f%%", stats.CompressionProgress) } if stats.CompressionRatio > 0 { - Printf(" Compression Ratio: %.2fx\n", stats.CompressionRatio) + printer.S(" Compression Ratio: %.2fx", stats.CompressionRatio) } if stats.CompressionSpaceSaving > 0 { - Printf("Compression Space Saving: %.2f%%\n", stats.CompressionSpaceSaving) + printer.S("Compression Space Saving: %.2f%%", stats.CompressionSpaceSaving) } return nil @@ -359,14 +364,14 @@ const ( countModeDebug = "debug" ) -func statsDebug(ctx context.Context, repo restic.Repository) error { - Warnf("Collecting size statistics\n\n") +func statsDebug(ctx context.Context, repo restic.Repository, printer progress.Printer) error { + printer.E("Collecting size statistics\n\n") for _, t := range []restic.FileType{restic.KeyFile, restic.LockFile, restic.IndexFile, restic.PackFile} { hist, err := statsDebugFileType(ctx, repo, t) if err != nil { return err } - Warnf("File Type: %v\n%v\n", t, hist) + printer.E("File Type: %v\n%v", t, hist) } hist, err := statsDebugBlobs(ctx, repo) @@ -374,7 +379,7 @@ func statsDebug(ctx context.Context, repo restic.Repository) error { return err } for _, t := range []restic.BlobType{restic.DataBlob, restic.TreeBlob} { - Warnf("Blob Type: %v\n%v\n\n", t, hist[t]) + printer.E("Blob Type: %v\n%v\n\n", t, hist[t]) } return nil diff --git a/cmd/restic/cmd_tag.go b/cmd/restic/cmd_tag.go index 39e9a16b5..fde4209bc 100644 --- a/cmd/restic/cmd_tag.go +++ b/cmd/restic/cmd_tag.go @@ -11,7 +11,6 @@ import ( "github.com/restic/restic/internal/repository" "github.com/restic/restic/internal/restic" "github.com/restic/restic/internal/ui" - "github.com/restic/restic/internal/ui/termstatus" ) func newTagCommand() *cobra.Command { @@ -119,7 +118,9 @@ func changeTags(ctx context.Context, repo *repository.Repository, sn *restic.Sna return changed, nil } -func runTag(ctx context.Context, opts TagOptions, gopts GlobalOptions, term *termstatus.Terminal, args []string) error { +func runTag(ctx context.Context, opts TagOptions, gopts GlobalOptions, term ui.Terminal, args []string) error { + printer := newTerminalProgressPrinter(gopts.JSON, gopts.verbosity, term) + if len(opts.SetTags) == 0 && len(opts.AddTags) == 0 && len(opts.RemoveTags) == 0 { return errors.Fatal("nothing to do!") } @@ -127,23 +128,23 @@ func runTag(ctx context.Context, opts TagOptions, gopts GlobalOptions, term *ter return errors.Fatal("--set and --add/--remove cannot be given at the same time") } - Verbosef("create exclusive lock for repository\n") - ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, false) + printer.P("create exclusive lock for repository") + ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, false, printer) if err != nil { return err } defer unlock() printFunc := func(c changedSnapshot) { - Verboseff("old snapshot ID: %v -> new snapshot ID: %v\n", c.OldSnapshotID, c.NewSnapshotID) + printer.V("old snapshot ID: %v -> new snapshot ID: %v", c.OldSnapshotID, c.NewSnapshotID) } summary := changedSnapshotsSummary{MessageType: "summary", ChangedSnapshots: 0} printSummary := func(c changedSnapshotsSummary) { if c.ChangedSnapshots == 0 { - Verbosef("no snapshots were modified\n") + printer.P("no snapshots were modified") } else { - Verbosef("modified %v snapshots\n", c.ChangedSnapshots) + printer.P("modified %v snapshots", c.ChangedSnapshots) } } @@ -156,10 +157,10 @@ func runTag(ctx context.Context, opts TagOptions, gopts GlobalOptions, term *ter } } - for sn := range FindFilteredSnapshots(ctx, repo, repo, &opts.SnapshotFilter, args) { + for sn := range FindFilteredSnapshots(ctx, repo, repo, &opts.SnapshotFilter, args, printer) { changed, err := changeTags(ctx, repo, sn, opts.SetTags.Flatten(), opts.AddTags.Flatten(), opts.RemoveTags.Flatten(), printFunc) if err != nil { - Warnf("unable to modify the tags for snapshot ID %q, ignoring: %v\n", sn.ID(), err) + printer.E("unable to modify the tags for snapshot ID %q, ignoring: %v", sn.ID(), err) continue } if changed { diff --git a/cmd/restic/cmd_unlock.go b/cmd/restic/cmd_unlock.go index 5932ffcaa..8cea239c6 100644 --- a/cmd/restic/cmd_unlock.go +++ b/cmd/restic/cmd_unlock.go @@ -4,6 +4,7 @@ import ( "context" "github.com/restic/restic/internal/repository" + "github.com/restic/restic/internal/ui" "github.com/spf13/cobra" "github.com/spf13/pflag" ) @@ -26,7 +27,9 @@ Exit status is 1 if there was any error. GroupID: cmdGroupDefault, DisableAutoGenTag: true, RunE: func(cmd *cobra.Command, _ []string) error { - return runUnlock(cmd.Context(), opts, globalOptions) + term, cancel := setupTermstatus() + defer cancel() + return runUnlock(cmd.Context(), opts, globalOptions, term) }, } opts.AddFlags(cmd.Flags()) @@ -42,8 +45,9 @@ func (opts *UnlockOptions) AddFlags(f *pflag.FlagSet) { f.BoolVar(&opts.RemoveAll, "remove-all", false, "remove all locks, even non-stale ones") } -func runUnlock(ctx context.Context, opts UnlockOptions, gopts GlobalOptions) error { - repo, err := OpenRepository(ctx, gopts) +func runUnlock(ctx context.Context, opts UnlockOptions, gopts GlobalOptions, term ui.Terminal) error { + printer := newTerminalProgressPrinter(gopts.JSON, gopts.verbosity, term) + repo, err := OpenRepository(ctx, gopts, printer) if err != nil { return err } @@ -59,7 +63,7 @@ func runUnlock(ctx context.Context, opts UnlockOptions, gopts GlobalOptions) err } if processed > 0 { - Verbosef("successfully removed %d locks\n", processed) + printer.P("successfully removed %d locks", processed) } return nil } diff --git a/cmd/restic/cmd_version.go b/cmd/restic/cmd_version.go index 533098b9b..1acfba5ab 100644 --- a/cmd/restic/cmd_version.go +++ b/cmd/restic/cmd_version.go @@ -2,7 +2,6 @@ package main import ( "encoding/json" - "fmt" "runtime" "github.com/spf13/cobra" @@ -24,6 +23,10 @@ Exit status is 1 if there was any error. `, DisableAutoGenTag: true, Run: func(_ *cobra.Command, _ []string) { + term, cancel := setupTermstatus() + defer cancel() + printer := newTerminalProgressPrinter(globalOptions.JSON, globalOptions.verbosity, term) + if globalOptions.JSON { type jsonVersion struct { MessageType string `json:"message_type"` // version @@ -43,14 +46,13 @@ Exit status is 1 if there was any error. err := json.NewEncoder(globalOptions.stdout).Encode(jsonS) if err != nil { - Warnf("JSON encode failed: %v\n", err) + printer.E("JSON encode failed: %v\n", err) return } } else { - fmt.Printf("restic %s compiled with %v on %v/%v\n", + printer.S("restic %s compiled with %v on %v/%v\n", version, runtime.Version(), runtime.GOOS, runtime.GOARCH) } - }, } return cmd diff --git a/cmd/restic/find.go b/cmd/restic/find.go index faf7024e1..eab8b5d90 100644 --- a/cmd/restic/find.go +++ b/cmd/restic/find.go @@ -5,6 +5,7 @@ import ( "os" "github.com/restic/restic/internal/restic" + "github.com/restic/restic/internal/ui/progress" "github.com/spf13/pflag" ) @@ -39,19 +40,19 @@ func initSingleSnapshotFilter(flags *pflag.FlagSet, filt *restic.SnapshotFilter) } // FindFilteredSnapshots yields Snapshots, either given explicitly by `snapshotIDs` or filtered from the list of all snapshots. -func FindFilteredSnapshots(ctx context.Context, be restic.Lister, loader restic.LoaderUnpacked, f *restic.SnapshotFilter, snapshotIDs []string) <-chan *restic.Snapshot { +func FindFilteredSnapshots(ctx context.Context, be restic.Lister, loader restic.LoaderUnpacked, f *restic.SnapshotFilter, snapshotIDs []string, printer progress.Printer) <-chan *restic.Snapshot { out := make(chan *restic.Snapshot) go func() { defer close(out) be, err := restic.MemorizeList(ctx, be, restic.SnapshotFile) if err != nil { - Warnf("could not load snapshots: %v\n", err) + printer.E("could not load snapshots: %v", err) return } err = f.FindAll(ctx, be, loader, snapshotIDs, func(id string, sn *restic.Snapshot, err error) error { if err != nil { - Warnf("Ignoring %q: %v\n", id, err) + printer.E("Ignoring %q: %v", id, err) } else { select { case <-ctx.Done(): @@ -62,7 +63,7 @@ func FindFilteredSnapshots(ctx context.Context, be restic.Lister, loader restic. return nil }) if err != nil { - Warnf("could not load snapshots: %v\n", err) + printer.E("could not load snapshots: %v", err) } }() return out diff --git a/cmd/restic/global.go b/cmd/restic/global.go index df9461272..bb477b760 100644 --- a/cmd/restic/global.go +++ b/cmd/restic/global.go @@ -34,6 +34,7 @@ import ( "github.com/restic/restic/internal/restic" "github.com/restic/restic/internal/terminal" "github.com/restic/restic/internal/textfile" + "github.com/restic/restic/internal/ui/progress" "github.com/spf13/pflag" "github.com/restic/restic/internal/errors" @@ -196,53 +197,6 @@ func collectBackends() *location.Registry { return backends } -// Printf writes the message to the configured stdout stream. -func Printf(format string, args ...interface{}) { - _, err := fmt.Fprintf(globalOptions.stdout, format, args...) - if err != nil { - fmt.Fprintf(os.Stderr, "unable to write to stdout: %v\n", err) - } -} - -// Print writes the message to the configured stdout stream. -func Print(args ...interface{}) { - _, err := fmt.Fprint(globalOptions.stdout, args...) - if err != nil { - fmt.Fprintf(os.Stderr, "unable to write to stdout: %v\n", err) - } -} - -// Println writes the message to the configured stdout stream. -func Println(args ...interface{}) { - _, err := fmt.Fprintln(globalOptions.stdout, args...) - if err != nil { - fmt.Fprintf(os.Stderr, "unable to write to stdout: %v\n", err) - } -} - -// Verbosef calls Printf to write the message when the verbose flag is set. -func Verbosef(format string, args ...interface{}) { - if globalOptions.verbosity >= 1 { - Printf(format, args...) - } -} - -// Verboseff calls Printf to write the message when the verbosity is >= 2 -func Verboseff(format string, args ...interface{}) { - if globalOptions.verbosity >= 2 { - Printf(format, args...) - } -} - -// Warnf writes the message to the configured stderr stream. -func Warnf(format string, args ...interface{}) { - _, err := fmt.Fprintf(globalOptions.stderr, format, args...) - if err != nil { - fmt.Fprintf(os.Stderr, "unable to write to stderr: %v\n", err) - } - debug.Log(format, args...) -} - // resolvePassword determines the password to be used for opening the repository. func resolvePassword(opts *GlobalOptions, envStr string) (string, error) { if opts.PasswordFile != "" && opts.PasswordCommand != "" { @@ -293,7 +247,7 @@ func readPassword(in io.Reader) (password string, err error) { // ReadPassword reads the password from a password file, the environment // variable RESTIC_PASSWORD or prompts the user. If the context is canceled, // the function leaks the password reading goroutine. -func ReadPassword(ctx context.Context, opts GlobalOptions, prompt string) (string, error) { +func ReadPassword(ctx context.Context, opts GlobalOptions, prompt string, printer progress.Printer) (string, error) { if opts.InsecureNoPassword { if opts.password != "" { return "", errors.Fatal("--insecure-no-password must not be specified together with providing a password via a cli option or environment variable") @@ -313,10 +267,10 @@ func ReadPassword(ctx context.Context, opts GlobalOptions, prompt string) (strin if terminal.StdinIsTerminal() { password, err = terminal.ReadPassword(ctx, os.Stdin, os.Stderr, prompt) } else { - password, err = readPassword(os.Stdin) if terminal.StdoutIsTerminal() { - Verbosef("reading repository password from stdin\n") + printer.P("reading repository password from stdin") } + password, err = readPassword(os.Stdin) } if err != nil { @@ -333,13 +287,13 @@ func ReadPassword(ctx context.Context, opts GlobalOptions, prompt string) (strin // ReadPasswordTwice calls ReadPassword two times and returns an error when the // passwords don't match. If the context is canceled, the function leaks the // password reading goroutine. -func ReadPasswordTwice(ctx context.Context, gopts GlobalOptions, prompt1, prompt2 string) (string, error) { - pw1, err := ReadPassword(ctx, gopts, prompt1) +func ReadPasswordTwice(ctx context.Context, gopts GlobalOptions, prompt1, prompt2 string, printer progress.Printer) (string, error) { + pw1, err := ReadPassword(ctx, gopts, prompt1, printer) if err != nil { return "", err } if terminal.StdinIsTerminal() { - pw2, err := ReadPassword(ctx, gopts, prompt2) + pw2, err := ReadPassword(ctx, gopts, prompt2, printer) if err != nil { return "", err } @@ -380,13 +334,13 @@ func ReadRepo(opts GlobalOptions) (string, error) { const maxKeys = 20 // OpenRepository reads the password and opens the repository. -func OpenRepository(ctx context.Context, opts GlobalOptions) (*repository.Repository, error) { +func OpenRepository(ctx context.Context, opts GlobalOptions, printer progress.Printer) (*repository.Repository, error) { repo, err := ReadRepo(opts) if err != nil { return nil, err } - be, err := open(ctx, repo, opts, opts.extended) + be, err := open(ctx, repo, opts, opts.extended, printer) if err != nil { return nil, err } @@ -406,13 +360,13 @@ func OpenRepository(ctx context.Context, opts GlobalOptions) (*repository.Reposi } for ; passwordTriesLeft > 0; passwordTriesLeft-- { - opts.password, err = ReadPassword(ctx, opts, "enter password for repository: ") + opts.password, err = ReadPassword(ctx, opts, "enter password for repository: ", printer) if ctx.Err() != nil { return nil, ctx.Err() } if err != nil && passwordTriesLeft > 1 { opts.password = "" - fmt.Printf("%s. Try again\n", err) + printer.E("%s. Try again", err) } if err != nil { continue @@ -421,7 +375,7 @@ func OpenRepository(ctx context.Context, opts GlobalOptions) (*repository.Reposi err = s.SearchKey(ctx, opts.password, maxKeys, opts.KeyHint) if err != nil && passwordTriesLeft > 1 { opts.password = "" - fmt.Fprintf(os.Stderr, "%s. Try again\n", err) + printer.E("%s. Try again", err) } } if err != nil { @@ -441,7 +395,7 @@ func OpenRepository(ctx context.Context, opts GlobalOptions) (*repository.Reposi if s.Config().Version >= 2 { extra = ", compression level " + opts.Compression.String() } - Verbosef("repository %v opened (version %v%s)\n", id, s.Config().Version, extra) + printer.P("repository %v opened (version %v%s)", id, s.Config().Version, extra) } } @@ -451,12 +405,12 @@ func OpenRepository(ctx context.Context, opts GlobalOptions) (*repository.Reposi c, err := cache.New(s.Config().ID, opts.CacheDir) if err != nil { - Warnf("unable to open cache: %v\n", err) + printer.E("unable to open cache: %v", err) return s, nil } if c.Created && !opts.JSON && terminal.StdoutIsTerminal() { - Verbosef("created new cache in %v\n", c.Base) + printer.P("created new cache in %v", c.Base) } // start using the cache @@ -464,7 +418,7 @@ func OpenRepository(ctx context.Context, opts GlobalOptions) (*repository.Reposi oldCacheDirs, err := cache.Old(c.Base) if err != nil { - Warnf("unable to find old cache directories: %v", err) + printer.E("unable to find old cache directories: %v", err) } // nothing more to do if no old cache dirs could be found @@ -475,18 +429,18 @@ func OpenRepository(ctx context.Context, opts GlobalOptions) (*repository.Reposi // cleanup old cache dirs if instructed to do so if opts.CleanupCache { if terminal.StdoutIsTerminal() && !opts.JSON { - Verbosef("removing %d old cache dirs from %v\n", len(oldCacheDirs), c.Base) + printer.P("removing %d old cache dirs from %v", len(oldCacheDirs), c.Base) } for _, item := range oldCacheDirs { dir := filepath.Join(c.Base, item.Name()) err = os.RemoveAll(dir) if err != nil { - Warnf("unable to remove %v: %v\n", dir, err) + printer.E("unable to remove %v: %v", dir, err) } } } else { if terminal.StdoutIsTerminal() { - Verbosef("found %d old cache directories in %v, run `restic cache --cleanup` to remove them\n", + printer.P("found %d old cache directories in %v, run `restic cache --cleanup` to remove them", len(oldCacheDirs), c.Base) } } @@ -510,7 +464,7 @@ func parseConfig(loc location.Location, opts options.Options) (interface{}, erro return cfg, nil } -func innerOpen(ctx context.Context, s string, gopts GlobalOptions, opts options.Options, create bool) (backend.Backend, error) { +func innerOpen(ctx context.Context, s string, gopts GlobalOptions, opts options.Options, create bool, printer progress.Printer) (backend.Backend, error) { debug.Log("parsing location %v", location.StripPassword(gopts.backends, s)) loc, err := location.Parse(gopts.backends, s) if err != nil { @@ -547,6 +501,10 @@ func innerOpen(ctx context.Context, s string, gopts GlobalOptions, opts options. return nil, fmt.Errorf("Fatal: %w at %v: %v", ErrNoRepository, location.StripPassword(gopts.backends, s), err) } if err != nil { + if create { + // init already wraps the error message + return nil, err + } return nil, errors.Fatalf("unable to open repository at %v: %v", location.StripPassword(gopts.backends, s), err) } @@ -563,13 +521,13 @@ func innerOpen(ctx context.Context, s string, gopts GlobalOptions, opts options. report := func(msg string, err error, d time.Duration) { if d >= 0 { - Warnf("%v returned error, retrying after %v: %v\n", msg, d, err) + printer.E("%v returned error, retrying after %v: %v", msg, d, err) } else { - Warnf("%v failed: %v\n", msg, err) + printer.E("%v failed: %v", msg, err) } } success := func(msg string, retries int) { - Warnf("%v operation successful after %d retries\n", msg, retries) + printer.E("%v operation successful after %d retries", msg, retries) } be = retry.New(be, 15*time.Minute, report, success) @@ -585,8 +543,8 @@ func innerOpen(ctx context.Context, s string, gopts GlobalOptions, opts options. } // Open the backend specified by a location config. -func open(ctx context.Context, s string, gopts GlobalOptions, opts options.Options) (backend.Backend, error) { - be, err := innerOpen(ctx, s, gopts, opts, false) +func open(ctx context.Context, s string, gopts GlobalOptions, opts options.Options, printer progress.Printer) (backend.Backend, error) { + be, err := innerOpen(ctx, s, gopts, opts, false, printer) if err != nil { return nil, err } @@ -608,6 +566,6 @@ func open(ctx context.Context, s string, gopts GlobalOptions, opts options.Optio } // Create the backend specified by URI. -func create(ctx context.Context, s string, gopts GlobalOptions, opts options.Options) (backend.Backend, error) { - return innerOpen(ctx, s, gopts, opts, true) +func create(ctx context.Context, s string, gopts GlobalOptions, opts options.Options, printer progress.Printer) (backend.Backend, error) { + return innerOpen(ctx, s, gopts, opts, true, printer) } diff --git a/cmd/restic/global_test.go b/cmd/restic/global_test.go index 8e97ece29..57bf19862 100644 --- a/cmd/restic/global_test.go +++ b/cmd/restic/global_test.go @@ -9,22 +9,9 @@ import ( "github.com/restic/restic/internal/errors" rtest "github.com/restic/restic/internal/test" + "github.com/restic/restic/internal/ui/progress" ) -func Test_PrintFunctionsRespectsGlobalStdout(t *testing.T) { - for _, p := range []func(){ - func() { Println("message") }, - func() { Print("message\n") }, - func() { Printf("mes%s\n", "sage") }, - } { - buf, _ := withCaptureStdout(func() error { - p() - return nil - }) - rtest.Equals(t, "message\n", buf.String()) - } -} - type errorReader struct{ err error } func (r *errorReader) Read([]byte) (int, error) { return 0, r.err } @@ -66,11 +53,11 @@ func TestReadRepo(t *testing.T) { func TestReadEmptyPassword(t *testing.T) { opts := GlobalOptions{InsecureNoPassword: true} - password, err := ReadPassword(context.TODO(), opts, "test") + password, err := ReadPassword(context.TODO(), opts, "test", &progress.NoopPrinter{}) rtest.OK(t, err) rtest.Equals(t, "", password, "got unexpected password") opts.password = "invalid" - _, err = ReadPassword(context.TODO(), opts, "test") + _, err = ReadPassword(context.TODO(), opts, "test", &progress.NoopPrinter{}) rtest.Assert(t, strings.Contains(err.Error(), "must not be specified together with providing a password via a cli option or environment variable"), "unexpected error message, got %v", err) } diff --git a/cmd/restic/integration_helpers_test.go b/cmd/restic/integration_helpers_test.go index dff84522a..35eb5c253 100644 --- a/cmd/restic/integration_helpers_test.go +++ b/cmd/restic/integration_helpers_test.go @@ -20,6 +20,7 @@ import ( "github.com/restic/restic/internal/repository" "github.com/restic/restic/internal/restic" rtest "github.com/restic/restic/internal/test" + "github.com/restic/restic/internal/ui" "github.com/restic/restic/internal/ui/termstatus" ) @@ -246,32 +247,41 @@ func testSetupBackupData(t testing.TB, env *testEnvironment) string { } func listPacks(gopts GlobalOptions, t *testing.T) restic.IDSet { - ctx, r, unlock, err := openWithReadLock(context.TODO(), gopts, false) + var packs restic.IDSet + err := withTermStatus(gopts, func(ctx context.Context, term ui.Terminal) error { + printer := newTerminalProgressPrinter(gopts.JSON, gopts.verbosity, term) + ctx, r, unlock, err := openWithReadLock(ctx, gopts, false, printer) + rtest.OK(t, err) + defer unlock() + + packs = restic.NewIDSet() + + return r.List(ctx, restic.PackFile, func(id restic.ID, size int64) error { + packs.Insert(id) + return nil + }) + }) rtest.OK(t, err) - defer unlock() - - packs := restic.NewIDSet() - - rtest.OK(t, r.List(ctx, restic.PackFile, func(id restic.ID, size int64) error { - packs.Insert(id) - return nil - })) return packs } func listTreePacks(gopts GlobalOptions, t *testing.T) restic.IDSet { - ctx, r, unlock, err := openWithReadLock(context.TODO(), gopts, false) + var treePacks restic.IDSet + err := withTermStatus(gopts, func(ctx context.Context, term ui.Terminal) error { + printer := newTerminalProgressPrinter(gopts.JSON, gopts.verbosity, term) + ctx, r, unlock, err := openWithReadLock(ctx, gopts, false, printer) + rtest.OK(t, err) + defer unlock() + + rtest.OK(t, r.LoadIndex(ctx, nil)) + treePacks = restic.NewIDSet() + return r.ListBlobs(ctx, func(pb restic.PackedBlob) { + if pb.Type == restic.TreeBlob { + treePacks.Insert(pb.PackID) + } + }) + }) rtest.OK(t, err) - defer unlock() - - rtest.OK(t, r.LoadIndex(ctx, nil)) - treePacks := restic.NewIDSet() - rtest.OK(t, r.ListBlobs(ctx, func(pb restic.PackedBlob) { - if pb.Type == restic.TreeBlob { - treePacks.Insert(pb.PackID) - } - })) - return treePacks } @@ -288,38 +298,47 @@ func captureBackend(gopts *GlobalOptions) func() backend.Backend { func removePacks(gopts GlobalOptions, t testing.TB, remove restic.IDSet) { be := captureBackend(&gopts) - ctx, _, unlock, err := openWithExclusiveLock(context.TODO(), gopts, false) - rtest.OK(t, err) - defer unlock() + err := withTermStatus(gopts, func(ctx context.Context, term ui.Terminal) error { + printer := newTerminalProgressPrinter(gopts.JSON, gopts.verbosity, term) + ctx, _, unlock, err := openWithExclusiveLock(ctx, gopts, false, printer) + rtest.OK(t, err) + defer unlock() - for id := range remove { - rtest.OK(t, be().Remove(ctx, backend.Handle{Type: restic.PackFile, Name: id.String()})) - } + for id := range remove { + rtest.OK(t, be().Remove(ctx, backend.Handle{Type: restic.PackFile, Name: id.String()})) + } + return nil + }) + rtest.OK(t, err) } func removePacksExcept(gopts GlobalOptions, t testing.TB, keep restic.IDSet, removeTreePacks bool) { be := captureBackend(&gopts) - ctx, r, unlock, err := openWithExclusiveLock(context.TODO(), gopts, false) + err := withTermStatus(gopts, func(ctx context.Context, term ui.Terminal) error { + printer := newTerminalProgressPrinter(gopts.JSON, gopts.verbosity, term) + ctx, r, unlock, err := openWithExclusiveLock(ctx, gopts, false, printer) + rtest.OK(t, err) + defer unlock() + + // Get all tree packs + rtest.OK(t, r.LoadIndex(ctx, nil)) + + treePacks := restic.NewIDSet() + rtest.OK(t, r.ListBlobs(ctx, func(pb restic.PackedBlob) { + if pb.Type == restic.TreeBlob { + treePacks.Insert(pb.PackID) + } + })) + + // remove all packs containing data blobs + return r.List(ctx, restic.PackFile, func(id restic.ID, size int64) error { + if treePacks.Has(id) != removeTreePacks || keep.Has(id) { + return nil + } + return be().Remove(ctx, backend.Handle{Type: restic.PackFile, Name: id.String()}) + }) + }) rtest.OK(t, err) - defer unlock() - - // Get all tree packs - rtest.OK(t, r.LoadIndex(ctx, nil)) - - treePacks := restic.NewIDSet() - rtest.OK(t, r.ListBlobs(ctx, func(pb restic.PackedBlob) { - if pb.Type == restic.TreeBlob { - treePacks.Insert(pb.PackID) - } - })) - - // remove all packs containing data blobs - rtest.OK(t, r.List(ctx, restic.PackFile, func(id restic.ID, size int64) error { - if treePacks.Has(id) != removeTreePacks || keep.Has(id) { - return nil - } - return be().Remove(ctx, backend.Handle{Type: restic.PackFile, Name: id.String()}) - })) } func includes(haystack []string, needle string) bool { @@ -333,7 +352,7 @@ func includes(haystack []string, needle string) bool { } func loadSnapshotMap(t testing.TB, gopts GlobalOptions) map[string]struct{} { - snapshotIDs := testRunList(t, "snapshots", gopts) + snapshotIDs := testRunList(t, gopts, "snapshots") m := make(map[string]struct{}) for _, id := range snapshotIDs { @@ -355,10 +374,16 @@ func lastSnapshot(old, new map[string]struct{}) (map[string]struct{}, string) { } func testLoadSnapshot(t testing.TB, gopts GlobalOptions, id restic.ID) *restic.Snapshot { - _, repo, unlock, err := openWithReadLock(context.TODO(), gopts, false) - defer unlock() - rtest.OK(t, err) - snapshot, err := restic.LoadSnapshot(context.TODO(), repo, id) + var snapshot *restic.Snapshot + err := withTermStatus(gopts, func(ctx context.Context, term ui.Terminal) error { + printer := newTerminalProgressPrinter(gopts.JSON, gopts.verbosity, term) + _, repo, unlock, err := openWithReadLock(ctx, gopts, false, printer) + rtest.OK(t, err) + defer unlock() + + snapshot, err = restic.LoadSnapshot(ctx, repo, id) + return err + }) rtest.OK(t, err) return snapshot } @@ -406,17 +431,18 @@ func withRestoreGlobalOptions(inner func() error) error { return inner() } -func withCaptureStdout(inner func() error) (*bytes.Buffer, error) { +func withCaptureStdout(gopts GlobalOptions, inner func(gopts GlobalOptions) error) (*bytes.Buffer, error) { buf := bytes.NewBuffer(nil) err := withRestoreGlobalOptions(func() error { globalOptions.stdout = buf - return inner() + gopts.stdout = buf + return inner(gopts) }) return buf, err } -func withTermStatus(gopts GlobalOptions, callback func(ctx context.Context, term *termstatus.Terminal) error) error { +func withTermStatus(gopts GlobalOptions, callback func(ctx context.Context, term ui.Terminal) error) error { ctx, cancel := context.WithCancel(context.TODO()) var wg sync.WaitGroup diff --git a/cmd/restic/integration_test.go b/cmd/restic/integration_test.go index 3ef98a168..c16f09bf1 100644 --- a/cmd/restic/integration_test.go +++ b/cmd/restic/integration_test.go @@ -12,7 +12,7 @@ import ( "github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/restic" rtest "github.com/restic/restic/internal/test" - "github.com/restic/restic/internal/ui/termstatus" + "github.com/restic/restic/internal/ui" ) func TestCheckRestoreNoLock(t *testing.T) { @@ -87,14 +87,14 @@ func TestListOnce(t *testing.T) { createPrunableRepo(t, env) testRunPrune(t, env.gopts, pruneOpts) - rtest.OK(t, withTermStatus(env.gopts, func(ctx context.Context, term *termstatus.Terminal) error { + rtest.OK(t, withTermStatus(env.gopts, func(ctx context.Context, term ui.Terminal) error { _, err := runCheck(context.TODO(), checkOpts, env.gopts, nil, term) return err })) - rtest.OK(t, withTermStatus(env.gopts, func(ctx context.Context, term *termstatus.Terminal) error { + rtest.OK(t, withTermStatus(env.gopts, func(ctx context.Context, term ui.Terminal) error { return runRebuildIndex(context.TODO(), RepairIndexOptions{}, env.gopts, term) })) - rtest.OK(t, withTermStatus(env.gopts, func(ctx context.Context, term *termstatus.Terminal) error { + rtest.OK(t, withTermStatus(env.gopts, func(ctx context.Context, term ui.Terminal) error { return runRebuildIndex(context.TODO(), RepairIndexOptions{ReadAllPacks: true}, env.gopts, term) })) } @@ -161,19 +161,24 @@ func TestFindListOnce(t *testing.T) { testRunBackup(t, "", []string{filepath.Join(env.testdata, "0", "0", "9", "3")}, opts, env.gopts) thirdSnapshot := restic.NewIDSet(testListSnapshots(t, env.gopts, 3)...) - ctx, repo, unlock, err := openWithReadLock(context.TODO(), env.gopts, false) - rtest.OK(t, err) - defer unlock() + var snapshotIDs restic.IDSet + rtest.OK(t, withTermStatus(env.gopts, func(ctx context.Context, term ui.Terminal) error { + printer := newTerminalProgressPrinter(env.gopts.JSON, env.gopts.verbosity, term) + ctx, repo, unlock, err := openWithReadLock(ctx, env.gopts, false, printer) + rtest.OK(t, err) + defer unlock() - snapshotIDs := restic.NewIDSet() - // specify the two oldest snapshots explicitly and use "latest" to reference the newest one - for sn := range FindFilteredSnapshots(ctx, repo, repo, &restic.SnapshotFilter{}, []string{ - secondSnapshot[0].String(), - secondSnapshot[1].String()[:8], - "latest", - }) { - snapshotIDs.Insert(*sn.ID()) - } + snapshotIDs = restic.NewIDSet() + // specify the two oldest snapshots explicitly and use "latest" to reference the newest one + for sn := range FindFilteredSnapshots(ctx, repo, repo, &restic.SnapshotFilter{}, []string{ + secondSnapshot[0].String(), + secondSnapshot[1].String()[:8], + "latest", + }, printer) { + snapshotIDs.Insert(*sn.ID()) + } + return nil + })) // the snapshots can only be listed once, if both lists match then the there has been only a single List() call rtest.Equals(t, thirdSnapshot, snapshotIDs) diff --git a/cmd/restic/lock.go b/cmd/restic/lock.go index 0e3dea6d5..eb95c4432 100644 --- a/cmd/restic/lock.go +++ b/cmd/restic/lock.go @@ -4,10 +4,11 @@ import ( "context" "github.com/restic/restic/internal/repository" + "github.com/restic/restic/internal/ui/progress" ) -func internalOpenWithLocked(ctx context.Context, gopts GlobalOptions, dryRun bool, exclusive bool) (context.Context, *repository.Repository, func(), error) { - repo, err := OpenRepository(ctx, gopts) +func internalOpenWithLocked(ctx context.Context, gopts GlobalOptions, dryRun bool, exclusive bool, printer progress.Printer) (context.Context, *repository.Repository, func(), error) { + repo, err := OpenRepository(ctx, gopts, printer) if err != nil { return nil, nil, nil, err } @@ -18,9 +19,9 @@ func internalOpenWithLocked(ctx context.Context, gopts GlobalOptions, dryRun boo lock, ctx, err = repository.Lock(ctx, repo, exclusive, gopts.RetryLock, func(msg string) { if !gopts.JSON { - Verbosef("%s", msg) + printer.P("%s", msg) } - }, Warnf) + }, printer.E) if err != nil { return nil, nil, nil, err } @@ -33,16 +34,16 @@ func internalOpenWithLocked(ctx context.Context, gopts GlobalOptions, dryRun boo return ctx, repo, unlock, nil } -func openWithReadLock(ctx context.Context, gopts GlobalOptions, noLock bool) (context.Context, *repository.Repository, func(), error) { +func openWithReadLock(ctx context.Context, gopts GlobalOptions, noLock bool, printer progress.Printer) (context.Context, *repository.Repository, func(), error) { // TODO enforce read-only operations once the locking code has moved to the repository - return internalOpenWithLocked(ctx, gopts, noLock, false) + return internalOpenWithLocked(ctx, gopts, noLock, false, printer) } -func openWithAppendLock(ctx context.Context, gopts GlobalOptions, dryRun bool) (context.Context, *repository.Repository, func(), error) { +func openWithAppendLock(ctx context.Context, gopts GlobalOptions, dryRun bool, printer progress.Printer) (context.Context, *repository.Repository, func(), error) { // TODO enforce non-exclusive operations once the locking code has moved to the repository - return internalOpenWithLocked(ctx, gopts, dryRun, false) + return internalOpenWithLocked(ctx, gopts, dryRun, false, printer) } -func openWithExclusiveLock(ctx context.Context, gopts GlobalOptions, dryRun bool) (context.Context, *repository.Repository, func(), error) { - return internalOpenWithLocked(ctx, gopts, dryRun, true) +func openWithExclusiveLock(ctx context.Context, gopts GlobalOptions, dryRun bool, printer progress.Printer) (context.Context, *repository.Repository, func(), error) { + return internalOpenWithLocked(ctx, gopts, dryRun, true, printer) } diff --git a/cmd/restic/main.go b/cmd/restic/main.go index f4b17aa92..179fd1d0d 100644 --- a/cmd/restic/main.go +++ b/cmd/restic/main.go @@ -141,7 +141,9 @@ func printExitError(code int, message string) { err := json.NewEncoder(globalOptions.stderr).Encode(jsonS) if err != nil { - Warnf("JSON encode failed: %v\n", err) + // ignore error as there's no good way to handle it + _, _ = fmt.Fprintf(os.Stderr, "JSON encode failed: %v\n", err) + debug.Log("JSON encode failed: %v\n", err) return } } else { diff --git a/cmd/restic/progress.go b/cmd/restic/progress.go index b2abd61d2..37ba0e623 100644 --- a/cmd/restic/progress.go +++ b/cmd/restic/progress.go @@ -4,13 +4,11 @@ import ( "fmt" "os" "strconv" - "strings" "time" "github.com/restic/restic/internal/terminal" "github.com/restic/restic/internal/ui" "github.com/restic/restic/internal/ui/progress" - "github.com/restic/restic/internal/ui/termstatus" ) // calculateProgressInterval returns the interval configured via RESTIC_PROGRESS_FPS @@ -30,8 +28,8 @@ func calculateProgressInterval(show bool, json bool) time.Duration { return interval } -// newGenericProgressMax returns a progress.Counter that prints to stdout or terminal if provided. -func newGenericProgressMax(show bool, max uint64, description string, print func(status string, final bool)) *progress.Counter { +// newTerminalProgressMax returns a progress.Counter that prints to terminal if provided. +func newTerminalProgressMax(show bool, max uint64, description string, term ui.Terminal) *progress.Counter { if !show { return nil } @@ -47,12 +45,6 @@ func newGenericProgressMax(show bool, max uint64, description string, print func ui.FormatDuration(d), ui.FormatPercent(v, max), v, max, description) } - print(status, final) - }) -} - -func newTerminalProgressMax(show bool, max uint64, description string, term *termstatus.Terminal) *progress.Counter { - return newGenericProgressMax(show, max, description, func(status string, final bool) { if final { term.SetStatus(nil) term.Print(status) @@ -62,56 +54,8 @@ func newTerminalProgressMax(show bool, max uint64, description string, term *ter }) } -// newProgressMax calls newTerminalProgress without a terminal (print to stdout) -func newProgressMax(show bool, max uint64, description string) *progress.Counter { - return newGenericProgressMax(show, max, description, printProgress) -} - -func printProgress(status string, final bool) { - canUpdateStatus := terminal.StdoutCanUpdateStatus() - - w := terminal.StdoutWidth() - if w > 0 { - if w < 3 { - status = termstatus.Truncate(status, w) - } else { - trunc := termstatus.Truncate(status, w-3) - if len(trunc) < len(status) { - status = trunc + "..." - } - } - } - - var carriageControl string - if !(strings.HasSuffix(status, "\r") || strings.HasSuffix(status, "\n")) { - if canUpdateStatus { - carriageControl = "\r" - } else { - carriageControl = "\n" - } - } - - if canUpdateStatus { - clearCurrentLine := terminal.ClearCurrentLine(os.Stdout.Fd()) - clearCurrentLine(os.Stdout, os.Stdout.Fd()) - } - - _, _ = os.Stdout.Write([]byte(status + carriageControl)) - if final { - _, _ = os.Stdout.Write([]byte("\n")) - } -} - -func newIndexProgress(quiet bool, json bool) *progress.Counter { - return newProgressMax(!quiet && !json && terminal.StdoutIsTerminal(), 0, "index files loaded") -} - -func newIndexTerminalProgress(quiet bool, json bool, term *termstatus.Terminal) *progress.Counter { - return newTerminalProgressMax(!quiet && !json && terminal.StdoutIsTerminal(), 0, "index files loaded", term) -} - type terminalProgressPrinter struct { - term *termstatus.Terminal + term ui.Terminal ui.Message show bool } @@ -120,10 +64,21 @@ func (t *terminalProgressPrinter) NewCounter(description string) *progress.Count return newTerminalProgressMax(t.show, 0, description, t.term) } -func newTerminalProgressPrinter(verbosity uint, term *termstatus.Terminal) progress.Printer { +func (t *terminalProgressPrinter) NewCounterTerminalOnly(description string) *progress.Counter { + return newTerminalProgressMax(t.show && terminal.StdoutIsTerminal(), 0, description, t.term) +} + +func newTerminalProgressPrinter(json bool, verbosity uint, term ui.Terminal) progress.Printer { + if json { + verbosity = 0 + } return &terminalProgressPrinter{ term: term, Message: *ui.NewMessage(term, verbosity), show: verbosity > 0, } } + +func newIndexTerminalProgress(printer progress.Printer) *progress.Counter { + return printer.NewCounterTerminalOnly("index files loaded") +} diff --git a/cmd/restic/secondary_repo.go b/cmd/restic/secondary_repo.go index db4c93bad..16c75f1ab 100644 --- a/cmd/restic/secondary_repo.go +++ b/cmd/restic/secondary_repo.go @@ -5,6 +5,7 @@ import ( "os" "github.com/restic/restic/internal/errors" + "github.com/restic/restic/internal/ui/progress" "github.com/spf13/pflag" ) @@ -59,7 +60,7 @@ func (opts *secondaryRepoOptions) AddFlags(f *pflag.FlagSet, repoPrefix string, opts.PasswordCommand = os.Getenv("RESTIC_FROM_PASSWORD_COMMAND") } -func fillSecondaryGlobalOpts(ctx context.Context, opts secondaryRepoOptions, gopts GlobalOptions, repoPrefix string) (GlobalOptions, bool, error) { +func fillSecondaryGlobalOpts(ctx context.Context, opts secondaryRepoOptions, gopts GlobalOptions, repoPrefix string, printer progress.Printer) (GlobalOptions, bool, error) { if opts.Repo == "" && opts.RepositoryFile == "" && opts.LegacyRepo == "" && opts.LegacyRepositoryFile == "" { return GlobalOptions{}, false, errors.Fatal("Please specify a source repository location (--from-repo or --from-repository-file)") } @@ -115,7 +116,7 @@ func fillSecondaryGlobalOpts(ctx context.Context, opts secondaryRepoOptions, gop return GlobalOptions{}, false, err } } - dstGopts.password, err = ReadPassword(ctx, dstGopts, "enter password for "+repoPrefix+" repository: ") + dstGopts.password, err = ReadPassword(ctx, dstGopts, "enter password for "+repoPrefix+" repository: ", printer) if err != nil { return GlobalOptions{}, false, err } diff --git a/cmd/restic/secondary_repo_test.go b/cmd/restic/secondary_repo_test.go index aa511ca99..2c31bcecf 100644 --- a/cmd/restic/secondary_repo_test.go +++ b/cmd/restic/secondary_repo_test.go @@ -7,6 +7,7 @@ import ( "testing" rtest "github.com/restic/restic/internal/test" + "github.com/restic/restic/internal/ui/progress" ) // TestFillSecondaryGlobalOpts tests valid and invalid data on fillSecondaryGlobalOpts-function @@ -171,7 +172,7 @@ func TestFillSecondaryGlobalOpts(t *testing.T) { // Test all valid cases for _, testCase := range validSecondaryRepoTestCases { - DstGOpts, isFromRepo, err := fillSecondaryGlobalOpts(context.TODO(), testCase.Opts, gOpts, "destination") + DstGOpts, isFromRepo, err := fillSecondaryGlobalOpts(context.TODO(), testCase.Opts, gOpts, "destination", &progress.NoopPrinter{}) rtest.OK(t, err) rtest.Equals(t, DstGOpts, testCase.DstGOpts) rtest.Equals(t, isFromRepo, testCase.FromRepo) @@ -179,7 +180,7 @@ func TestFillSecondaryGlobalOpts(t *testing.T) { // Test all invalid cases for _, testCase := range invalidSecondaryRepoTestCases { - _, _, err := fillSecondaryGlobalOpts(context.TODO(), testCase.Opts, gOpts, "destination") + _, _, err := fillSecondaryGlobalOpts(context.TODO(), testCase.Opts, gOpts, "destination", &progress.NoopPrinter{}) rtest.Assert(t, err != nil, "Expected error, but function did not return an error") } } diff --git a/internal/ui/message.go b/internal/ui/message.go index 6ba60f3c0..612fd72a2 100644 --- a/internal/ui/message.go +++ b/internal/ui/message.go @@ -19,33 +19,35 @@ func NewMessage(term Terminal, verbosity uint) *Message { } } -// E reports an error +// E reports an error. This message is always printed to stderr. func (m *Message) E(msg string, args ...interface{}) { m.term.Error(fmt.Sprintf(msg, args...)) } // S prints a message, this is should only be used for very important messages -// that are not errors. +// that are not errors. The message is even printed if --quiet is specified. func (m *Message) S(msg string, args ...interface{}) { m.term.Print(fmt.Sprintf(msg, args...)) } -// P prints a message if verbosity >= 1, this is used for normal messages which -// are not errors. +// P prints a message if verbosity >= 1 (neither --quiet nor --verbose is specified), +// this is used for normal messages which are not errors. func (m *Message) P(msg string, args ...interface{}) { if m.v >= 1 { m.term.Print(fmt.Sprintf(msg, args...)) } } -// V prints a message if verbosity >= 2, this is used for verbose messages. +// V prints a message if verbosity >= 2 (equivalent to --verbose), this is used for +// verbose messages. func (m *Message) V(msg string, args ...interface{}) { if m.v >= 2 { m.term.Print(fmt.Sprintf(msg, args...)) } } -// VV prints a message if verbosity >= 3, this is used for debug messages. +// VV prints a message if verbosity >= 3 (equivalent to --verbose=2), this is used for +// debug messages. func (m *Message) VV(msg string, args ...interface{}) { if m.v >= 3 { m.term.Print(fmt.Sprintf(msg, args...)) diff --git a/internal/ui/mock.go b/internal/ui/mock.go index 5a4debb02..36452f4be 100644 --- a/internal/ui/mock.go +++ b/internal/ui/mock.go @@ -1,5 +1,9 @@ package ui +import "io" + +var _ Terminal = &MockTerminal{} + type MockTerminal struct { Output []string Errors []string @@ -20,3 +24,7 @@ func (m *MockTerminal) SetStatus(lines []string) { func (m *MockTerminal) CanUpdateStatus() bool { return true } + +func (m *MockTerminal) OutputRaw() io.Writer { + return nil +} diff --git a/internal/ui/progress/printer.go b/internal/ui/progress/printer.go index c3e0d17a3..37d81f4d6 100644 --- a/internal/ui/progress/printer.go +++ b/internal/ui/progress/printer.go @@ -6,17 +6,27 @@ import "testing" // at different log levels. // It must be safe to call its methods from concurrent goroutines. type Printer interface { + // NewCounter returns a new progress counter. It is not shown if --quiet or --json is specified. NewCounter(description string) *Counter + // NewCounterTerminalOnly returns a new progress counter that is only shown if stdout points to a + // terminal. It is not shown if --quiet or --json is specified. + NewCounterTerminalOnly(description string) *Counter - // E prints to stderr + // E reports an error. This message is always printed to stderr. + // Appends a newline if not present. E(msg string, args ...interface{}) - // S prints to stdout + // S prints a message, this is should only be used for very important messages + // that are not errors. The message is even printed if --quiet is specified. + // Appends a newline if not present. S(msg string, args ...interface{}) - // P prints to stdout unless quiet was passed + // P prints a message if verbosity >= 1 (neither --quiet nor --verbose is specified), + // this is used for normal messages which are not errors. Appends a newline if not present. P(msg string, args ...interface{}) - // V prints to stdout if verbose is set once + // V prints a message if verbosity >= 2 (equivalent to --verbose), this is used for + // verbose messages. Appends a newline if not present. V(msg string, args ...interface{}) - // VV prints to stdout if verbose is set twice + // VV prints a message if verbosity >= 3 (equivalent to --verbose=2), this is used for + // debug messages. Appends a newline if not present. VV(msg string, args ...interface{}) } @@ -29,6 +39,10 @@ func (*NoopPrinter) NewCounter(_ string) *Counter { return nil } +func (*NoopPrinter) NewCounterTerminalOnly(_ string) *Counter { + return nil +} + func (*NoopPrinter) E(_ string, _ ...interface{}) {} func (*NoopPrinter) S(_ string, _ ...interface{}) {} @@ -56,6 +70,10 @@ func (p *TestPrinter) NewCounter(_ string) *Counter { return nil } +func (p *TestPrinter) NewCounterTerminalOnly(_ string) *Counter { + return nil +} + func (p *TestPrinter) E(msg string, args ...interface{}) { p.t.Logf("error: "+msg, args...) } diff --git a/internal/ui/terminal.go b/internal/ui/terminal.go index 2d9418a61..262f1bcf7 100644 --- a/internal/ui/terminal.go +++ b/internal/ui/terminal.go @@ -1,10 +1,20 @@ package ui +import "io" + // Terminal is used to write messages and display status lines which can be // updated. See termstatus.Terminal for a concrete implementation. type Terminal interface { + // Print writes a line to the terminal. Appends a newline if not present. Print(line string) + // Error writes an error to the terminal. Appends a newline if not present. Error(line string) + // SetStatus sets the status lines to the terminal. SetStatus(lines []string) + // CanUpdateStatus returns true if the terminal can update the status lines. CanUpdateStatus() bool + // OutputRaw returns the output writer. Should only be used if there is no + // other option. Must not be used in combination with Print, Error, SetStatus + // or any other method that writes to the terminal. + OutputRaw() io.Writer } diff --git a/internal/ui/termstatus/status.go b/internal/ui/termstatus/status.go index 71be8ec4e..a5ce24205 100644 --- a/internal/ui/termstatus/status.go +++ b/internal/ui/termstatus/status.go @@ -1,7 +1,6 @@ package termstatus import ( - "bufio" "context" "fmt" "io" @@ -13,13 +12,16 @@ import ( "golang.org/x/text/width" "github.com/restic/restic/internal/terminal" + "github.com/restic/restic/internal/ui" ) +var _ ui.Terminal = &Terminal{} + // Terminal is used to write messages and display status lines which can be // updated. When the output is redirected to a file, the status lines are not // printed. type Terminal struct { - wr *bufio.Writer + wr io.Writer fd uintptr errWriter io.Writer msg chan message @@ -57,7 +59,7 @@ type fder interface { // are printed even if the terminal supports it. func New(wr io.Writer, errWriter io.Writer, disableStatus bool) *Terminal { t := &Terminal{ - wr: bufio.NewWriter(wr), + wr: wr, errWriter: errWriter, msg: make(chan message), status: make(chan status), @@ -84,6 +86,13 @@ func (t *Terminal) CanUpdateStatus() bool { return t.canUpdateStatus } +// OutputRaw returns the output writer. Should only be used if there is no +// other option. Must not be used in combination with Print, Error, SetStatus +// or any other method that writes to the terminal. +func (t *Terminal) OutputRaw() io.Writer { + return t.wr +} + // Run updates the screen. It should be run in a separate goroutine. When // ctx is cancelled, the status lines are cleanly removed. func (t *Terminal) Run(ctx context.Context) { @@ -118,13 +127,6 @@ func (t *Terminal) run(ctx context.Context) { var dst io.Writer if msg.err { dst = t.errWriter - - // assume t.wr and t.errWriter are different, so we need to - // flush clearing the current line - err := t.wr.Flush() - if err != nil { - fmt.Fprintf(os.Stderr, "flush failed: %v\n", err) - } } else { dst = t.wr } @@ -135,11 +137,6 @@ func (t *Terminal) run(ctx context.Context) { } t.writeStatus(status) - - if err := t.wr.Flush(); err != nil { - fmt.Fprintf(os.Stderr, "flush failed: %v\n", err) - } - case stat := <-t.status: status = append(status[:0], stat.lines...) @@ -169,26 +166,15 @@ func (t *Terminal) writeStatus(status []string) { for _, line := range status { t.clearCurrentLine(t.wr, t.fd) - _, err := t.wr.WriteString(line) + _, err := t.wr.Write([]byte(line)) if err != nil { fmt.Fprintf(os.Stderr, "write failed: %v\n", err) } - - // flush is needed so that the current line is updated - err = t.wr.Flush() - if err != nil { - fmt.Fprintf(os.Stderr, "flush failed: %v\n", err) - } } if len(status) > 0 { t.moveCursorUp(t.wr, t.fd, len(status)-1) } - - err := t.wr.Flush() - if err != nil { - fmt.Fprintf(os.Stderr, "flush failed: %v\n", err) - } } // runWithoutStatus listens on the channels and just prints out the messages, @@ -199,28 +185,18 @@ func (t *Terminal) runWithoutStatus(ctx context.Context) { case <-ctx.Done(): return case msg := <-t.msg: - var flush func() error var dst io.Writer if msg.err { dst = t.errWriter } else { dst = t.wr - flush = t.wr.Flush } if _, err := io.WriteString(dst, msg.line); err != nil { _, _ = fmt.Fprintf(os.Stderr, "write failed: %v\n", err) } - if flush == nil { - continue - } - - if err := flush(); err != nil { - _, _ = fmt.Fprintf(os.Stderr, "flush failed: %v\n", err) - } - case stat := <-t.status: for _, line := range stat.lines { // Ensure that each message ends with exactly one newline. @@ -228,16 +204,13 @@ func (t *Terminal) runWithoutStatus(ctx context.Context) { _, _ = fmt.Fprintf(os.Stderr, "write failed: %v\n", err) } } - if err := t.wr.Flush(); err != nil { - _, _ = fmt.Fprintf(os.Stderr, "flush failed: %v\n", err) - } } } } func (t *Terminal) print(line string, isErr bool) { // make sure the line ends with a line break - if line[len(line)-1] != '\n' { + if len(line) == 0 || line[len(line)-1] != '\n' { line += "\n" }