Merge pull request #5510 from MichaelEischer/termstatus-everywhere-print-functions

Replace Printf/Verbosef/Warnf with termstatus
This commit is contained in:
Michael Eischer 2025-09-21 16:42:29 +02:00 committed by GitHub
commit 4a7b122fb6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
69 changed files with 1068 additions and 878 deletions

View file

@ -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")

View file

@ -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
}

View file

@ -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)
}

View file

@ -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)

View file

@ -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
}

View file

@ -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:

View file

@ -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",

View file

@ -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,

View file

@ -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())

View file

@ -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) {

View file

@ -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")
}
}

View file

@ -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

View file

@ -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
}

View file

@ -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 != "" {

View file

@ -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 {

View file

@ -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))

View file

@ -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

View file

@ -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)
})
}

View file

@ -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
}

View file

@ -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")
})
}

View file

@ -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
}

View file

@ -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,

View file

@ -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 {

View file

@ -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)
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
})
}

View file

@ -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
}

View file

@ -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 {

View file

@ -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()

View file

@ -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
}

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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")

View file

@ -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 {

View file

@ -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)
}))
}

View file

@ -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)

View file

@ -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)
})

View file

@ -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
}

View file

@ -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)
}
}

View file

@ -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...))

View file

@ -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
}

View file

@ -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})
})
}

View file

@ -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)
}
}

View file

@ -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")
}

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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 {

View file

@ -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
}

View file

@ -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

View file

@ -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

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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

View file

@ -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)

View file

@ -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)
}

View file

@ -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 {

View file

@ -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")
}

View file

@ -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
}

View file

@ -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")
}
}

View file

@ -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...))

View file

@ -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
}

View file

@ -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...)
}

View file

@ -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
}

View file

@ -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"
}