Merge pull request #5518 from MichaelEischer/termstatus-everywhere

Consolidate terminal input/output functionality in termstatus.Terminal
This commit is contained in:
Michael Eischer 2025-10-03 19:05:28 +02:00 committed by GitHub
commit 13e476e1eb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
152 changed files with 1213 additions and 1213 deletions

View file

@ -3,6 +3,7 @@ package main
import (
"context"
"fmt"
"io"
"os"
"os/signal"
"syscall"
@ -10,27 +11,27 @@ import (
"github.com/restic/restic/internal/debug"
)
func createGlobalContext() context.Context {
func createGlobalContext(stderr io.Writer) context.Context {
ctx, cancel := context.WithCancel(context.Background())
ch := make(chan os.Signal, 1)
go cleanupHandler(ch, cancel)
go cleanupHandler(ch, cancel, stderr)
signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
return ctx
}
// cleanupHandler handles the SIGINT and SIGTERM signals.
func cleanupHandler(c <-chan os.Signal, cancel context.CancelFunc) {
func cleanupHandler(c <-chan os.Signal, cancel context.CancelFunc, stderr io.Writer) {
s := <-c
debug.Log("signal %v received, cleaning up", s)
// ignore error as there's no good way to handle it
_, _ = fmt.Fprintf(os.Stderr, "\rsignal %v received, cleaning up \n", s)
_, _ = fmt.Fprintf(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")
_, _ = os.Stderr.WriteString(debug.DumpStacktrace())
_, _ = os.Stderr.WriteString("\n--- STACKTRACE END ---\n")
_, _ = stderr.Write([]byte("\n--- STACKTRACE START ---\n\n"))
_, _ = stderr.Write([]byte(debug.DumpStacktrace()))
_, _ = stderr.Write([]byte("\n--- STACKTRACE END ---\n"))
}
cancel()

View file

@ -30,7 +30,7 @@ import (
"github.com/restic/restic/internal/ui/backup"
)
func newBackupCommand() *cobra.Command {
func newBackupCommand(globalOptions *GlobalOptions) *cobra.Command {
var opts BackupOptions
cmd := &cobra.Command{
@ -63,9 +63,7 @@ Exit status is 12 if the password is incorrect.
GroupID: cmdGroupDefault,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
term, cancel := setupTermstatus()
defer cancel()
return runBackup(cmd.Context(), opts, globalOptions, term, args)
return runBackup(cmd.Context(), opts, *globalOptions, globalOptions.term, args)
},
}
@ -184,7 +182,7 @@ func filterExisting(items []string, warnf func(msg string, args ...interface{}))
// If filename is empty, readPatternsFromFile returns an empty slice.
// If filename is a dash (-), readPatternsFromFile will read the lines from the
// standard input.
func readLines(filename string) ([]string, error) {
func readLines(filename string, stdin io.ReadCloser) ([]string, error) {
if filename == "" {
return nil, nil
}
@ -195,7 +193,7 @@ func readLines(filename string) ([]string, error) {
)
if filename == "-" {
data, err = io.ReadAll(os.Stdin)
data, err = io.ReadAll(stdin)
} else {
data, err = textfile.Read(filename)
}
@ -220,8 +218,8 @@ func readLines(filename string) ([]string, error) {
// readFilenamesFromFileRaw reads a list of filenames from the given file,
// or stdin if filename is "-". Each filename is terminated by a zero byte,
// which is stripped off.
func readFilenamesFromFileRaw(filename string) (names []string, err error) {
f := os.Stdin
func readFilenamesFromFileRaw(filename string, stdin io.ReadCloser) (names []string, err error) {
f := stdin
if filename != "-" {
if f, err = os.Open(filename); err != nil {
return nil, err
@ -380,13 +378,13 @@ func collectRejectFuncs(opts BackupOptions, targets []string, fs fs.FS, warnf fu
}
// collectTargets returns a list of target files/dirs from several sources.
func collectTargets(opts BackupOptions, args []string, warnf func(msg string, args ...interface{})) (targets []string, err error) {
func collectTargets(opts BackupOptions, args []string, warnf func(msg string, args ...interface{}), stdin io.ReadCloser) (targets []string, err error) {
if opts.Stdin || opts.StdinCommand {
return nil, nil
}
for _, file := range opts.FilesFrom {
fromfile, err := readLines(file)
fromfile, err := readLines(file, stdin)
if err != nil {
return nil, err
}
@ -411,7 +409,7 @@ func collectTargets(opts BackupOptions, args []string, warnf func(msg string, ar
}
for _, file := range opts.FilesFromVerbatim {
fromfile, err := readLines(file)
fromfile, err := readLines(file, stdin)
if err != nil {
return nil, err
}
@ -424,7 +422,7 @@ func collectTargets(opts BackupOptions, args []string, warnf func(msg string, ar
}
for _, file := range opts.FilesFromRaw {
fromfile, err := readFilenamesFromFileRaw(file)
fromfile, err := readFilenamesFromFileRaw(file, stdin)
if err != nil {
return nil, err
}
@ -480,7 +478,12 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, ter
var vsscfg fs.VSSConfig
var err error
msg := newTerminalProgressPrinter(gopts.JSON, gopts.verbosity, term)
var printer backup.ProgressPrinter
if gopts.JSON {
printer = backup.NewJSONProgress(term, gopts.verbosity)
} else {
printer = backup.NewTextProgress(term, gopts.verbosity)
}
if runtime.GOOS == "windows" {
if vsscfg, err = fs.ParseVSSConfig(gopts.extended); err != nil {
return err
@ -492,7 +495,7 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, ter
return err
}
targets, err := collectTargets(opts, args, msg.E)
targets, err := collectTargets(opts, args, printer.E, term.InputRaw())
if err != nil {
return err
}
@ -507,27 +510,21 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, ter
}
if gopts.verbosity >= 2 && !gopts.JSON {
msg.P("open repository")
printer.P("open repository")
}
ctx, repo, unlock, err := openWithAppendLock(ctx, gopts, opts.DryRun, msg)
ctx, repo, unlock, err := openWithAppendLock(ctx, gopts, opts.DryRun, printer)
if err != nil {
return err
}
defer unlock()
var progressPrinter backup.ProgressPrinter
if gopts.JSON {
progressPrinter = backup.NewJSONProgress(term, gopts.verbosity)
} else {
progressPrinter = backup.NewTextProgress(term, gopts.verbosity)
}
progressReporter := backup.NewProgress(progressPrinter,
calculateProgressInterval(!gopts.Quiet, gopts.JSON))
progressReporter := backup.NewProgress(printer,
ui.CalculateProgressInterval(!gopts.Quiet, gopts.JSON, term.CanUpdateStatus()))
defer progressReporter.Done()
// rejectByNameFuncs collect functions that can reject items from the backup based on path only
rejectByNameFuncs, err := collectRejectByNameFuncs(opts, repo, msg.E)
rejectByNameFuncs, err := collectRejectByNameFuncs(opts, repo, printer.E)
if err != nil {
return err
}
@ -541,19 +538,18 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, ter
if !gopts.JSON {
if parentSnapshot != nil {
progressPrinter.P("using parent snapshot %v\n", parentSnapshot.ID().Str())
printer.P("using parent snapshot %v\n", parentSnapshot.ID().Str())
} else {
progressPrinter.P("no parent snapshot found, will read all files\n")
printer.P("no parent snapshot found, will read all files\n")
}
}
}
if !gopts.JSON {
progressPrinter.V("load index files")
printer.V("load index files")
}
bar := newIndexTerminalProgress(msg)
err = repo.LoadIndex(ctx, bar)
err = repo.LoadIndex(ctx, printer)
if err != nil {
return err
}
@ -570,7 +566,7 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, ter
messageHandler := func(msg string, args ...interface{}) {
if !gopts.JSON {
progressPrinter.P(msg, args...)
printer.P(msg, args...)
}
}
@ -581,12 +577,12 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, ter
if opts.Stdin || opts.StdinCommand {
if !gopts.JSON {
progressPrinter.V("read data from stdin")
printer.V("read data from stdin")
}
filename := path.Join("/", opts.StdinFilename)
var source io.ReadCloser = os.Stdin
source := term.InputRaw()
if opts.StdinCommand {
source, err = fs.NewCommandReader(ctx, args, globalOptions.stderr)
source, err = fs.NewCommandReader(ctx, args, printer.E)
if err != nil {
return err
}
@ -606,7 +602,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, msg.E)
rejectFuncs, err := collectRejectFuncs(opts, targets, targetFS, printer.E)
if err != nil {
return err
}
@ -622,11 +618,11 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, ter
sc := archiver.NewScanner(targetFS)
sc.SelectByName = selectByNameFilter
sc.Select = selectFilter
sc.Error = progressPrinter.ScannerError
sc.Error = printer.ScannerError
sc.Result = progressReporter.ReportTotal
if !gopts.JSON {
progressPrinter.V("start scan on %v", targets)
printer.V("start scan on %v", targets)
}
wg.Go(func() error { return sc.Scan(cancelCtx, targets) })
}
@ -671,7 +667,7 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, ter
}
if !gopts.JSON {
progressPrinter.V("start backup on %v", targets)
printer.V("start backup on %v", targets)
}
_, id, summary, err := arch.Snapshot(ctx, targets, snapshotOpts)

View file

@ -3,7 +3,6 @@ package main
import (
"context"
"fmt"
"io"
"os"
"path/filepath"
"runtime"
@ -13,11 +12,10 @@ 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"
)
func testRunBackupAssumeFailure(t testing.TB, dir string, target []string, opts BackupOptions, gopts GlobalOptions) error {
return withTermStatus(gopts, func(ctx context.Context, term ui.Terminal) error {
return withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
t.Logf("backing up %v in %v", target, dir)
if dir != "" {
cleanup := rtest.Chdir(t, dir)
@ -25,7 +23,7 @@ func testRunBackupAssumeFailure(t testing.TB, dir string, target []string, opts
}
opts.GroupBy = restic.SnapshotGroupByOptions{Host: true, Path: true}
return runBackup(ctx, opts, gopts, term, target)
return runBackup(ctx, opts, gopts, gopts.term, target)
})
}
@ -56,13 +54,13 @@ func testBackup(t *testing.T, useFsSnapshot bool) {
testListSnapshots(t, env.gopts, 1)
testRunCheck(t, env.gopts)
stat1 := dirStats(env.repo)
stat1 := dirStats(t, env.repo)
// second backup, implicit incremental
testRunBackup(t, "", []string{env.testdata}, opts, env.gopts)
snapshotIDs := testListSnapshots(t, env.gopts, 2)
stat2 := dirStats(env.repo)
stat2 := dirStats(t, env.repo)
if stat2.size > stat1.size+stat1.size/10 {
t.Error("repository size has grown by more than 10 percent")
}
@ -74,7 +72,7 @@ func testBackup(t *testing.T, useFsSnapshot bool) {
testRunBackup(t, "", []string{env.testdata}, opts, env.gopts)
snapshotIDs = testListSnapshots(t, env.gopts, 3)
stat3 := dirStats(env.repo)
stat3 := dirStats(t, env.repo)
if stat3.size > stat1.size+stat1.size/10 {
t.Error("repository size has grown by more than 10 percent")
}
@ -85,7 +83,7 @@ func testBackup(t *testing.T, useFsSnapshot bool) {
restoredir := filepath.Join(env.base, fmt.Sprintf("restore%d", i))
t.Logf("restoring snapshot %v to %v", snapshotID.Str(), restoredir)
testRunRestore(t, env.gopts, restoredir, snapshotID.String()+":"+toPathInSnapshot(filepath.Dir(env.testdata)))
diff := directoriesContentsDiff(env.testdata, filepath.Join(restoredir, "testdata"))
diff := directoriesContentsDiff(t, env.testdata, filepath.Join(restoredir, "testdata"))
rtest.Assert(t, diff == "", "directories are not equal: %v", diff)
}
@ -262,22 +260,17 @@ func TestBackupNonExistingFile(t *testing.T) {
testSetupBackupData(t, env)
_ = withRestoreGlobalOptions(func() error {
globalOptions.stderr = io.Discard
p := filepath.Join(env.testdata, "0", "0", "9")
dirs := []string{
filepath.Join(p, "0"),
filepath.Join(p, "1"),
filepath.Join(p, "nonexisting"),
filepath.Join(p, "5"),
}
p := filepath.Join(env.testdata, "0", "0", "9")
dirs := []string{
filepath.Join(p, "0"),
filepath.Join(p, "1"),
filepath.Join(p, "nonexisting"),
filepath.Join(p, "5"),
}
opts := BackupOptions{}
opts := BackupOptions{}
testRunBackup(t, "", dirs, opts, env.gopts)
return nil
})
testRunBackup(t, "", dirs, opts, env.gopts)
}
func TestBackupSelfHealing(t *testing.T) {
@ -438,13 +431,13 @@ func TestIncrementalBackup(t *testing.T) {
testRunBackup(t, "", []string{datadir}, opts, env.gopts)
testRunCheck(t, env.gopts)
stat1 := dirStats(env.repo)
stat1 := dirStats(t, env.repo)
rtest.OK(t, appendRandomData(testfile, incrementalSecondWrite))
testRunBackup(t, "", []string{datadir}, opts, env.gopts)
testRunCheck(t, env.gopts)
stat2 := dirStats(env.repo)
stat2 := dirStats(t, env.repo)
if stat2.size-stat1.size > incrementalFirstWrite {
t.Errorf("repository size has grown by more than %d bytes", incrementalFirstWrite)
}
@ -454,7 +447,7 @@ func TestIncrementalBackup(t *testing.T) {
testRunBackup(t, "", []string{datadir}, opts, env.gopts)
testRunCheck(t, env.gopts)
stat3 := dirStats(env.repo)
stat3 := dirStats(t, env.repo)
if stat3.size-stat2.size > incrementalFirstWrite {
t.Errorf("repository size has grown by more than %d bytes", incrementalFirstWrite)
}
@ -565,7 +558,7 @@ func TestHardLink(t *testing.T) {
restoredir := filepath.Join(env.base, fmt.Sprintf("restore%d", i))
t.Logf("restoring snapshot %v to %v", snapshotID.Str(), restoredir)
testRunRestore(t, env.gopts, restoredir, snapshotID.String())
diff := directoriesContentsDiff(env.testdata, filepath.Join(restoredir, "testdata"))
diff := directoriesContentsDiff(t, env.testdata, filepath.Join(restoredir, "testdata"))
rtest.Assert(t, diff == "", "directories are not equal %v", diff)
linkResults := createFileSetPerHardlink(filepath.Join(restoredir, "testdata"))

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")}, t.Logf)
targets, err := collectTargets(opts, []string{filepath.Join(dir, "cmdline arg")}, t.Logf, nil)
rtest.OK(t, err)
sort.Strings(targets)
rtest.Equals(t, expect, targets)

View file

@ -16,7 +16,7 @@ import (
"github.com/spf13/pflag"
)
func newCacheCommand() *cobra.Command {
func newCacheCommand(globalOptions *GlobalOptions) *cobra.Command {
var opts CacheOptions
cmd := &cobra.Command{
@ -34,9 +34,7 @@ Exit status is 1 if there was any error.
GroupID: cmdGroupDefault,
DisableAutoGenTag: true,
RunE: func(_ *cobra.Command, args []string) error {
term, cancel := setupTermstatus()
defer cancel()
return runCache(opts, globalOptions, args, term)
return runCache(opts, *globalOptions, args, globalOptions.term)
},
}
@ -58,7 +56,7 @@ func (opts *CacheOptions) AddFlags(f *pflag.FlagSet) {
}
func runCache(opts CacheOptions, gopts GlobalOptions, args []string, term ui.Terminal) error {
printer := newTerminalProgressPrinter(false, gopts.verbosity, term)
printer := ui.NewProgressPrinter(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")
@ -163,7 +161,7 @@ func runCache(opts CacheOptions, gopts GlobalOptions, args []string, term ui.Ter
})
}
_ = tab.Write(globalOptions.stdout)
_ = tab.Write(gopts.term.OutputWriter())
printer.S("%d cache dirs in %s", len(dirs), cachedir)
return nil

View file

@ -15,7 +15,7 @@ import (
var catAllowedCmds = []string{"config", "index", "snapshot", "key", "masterkey", "lock", "pack", "blob", "tree"}
func newCatCommand() *cobra.Command {
func newCatCommand(globalOptions *GlobalOptions) *cobra.Command {
cmd := &cobra.Command{
Use: "cat [flags] [masterkey|config|pack ID|blob ID|snapshot ID|index ID|key ID|lock ID|tree snapshot:subfolder]",
Short: "Print internal objects to stdout",
@ -34,9 +34,7 @@ Exit status is 12 if the password is incorrect.
GroupID: cmdGroupDefault,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
term, cancel := setupTermstatus()
defer cancel()
return runCat(cmd.Context(), globalOptions, args, term)
return runCat(cmd.Context(), *globalOptions, args, globalOptions.term)
},
ValidArgs: catAllowedCmds,
}
@ -67,7 +65,7 @@ func validateCatArgs(args []string) error {
}
func runCat(ctx context.Context, gopts GlobalOptions, args []string, term ui.Terminal) error {
printer := newTerminalProgressPrinter(gopts.JSON, gopts.verbosity, term)
printer := ui.NewProgressPrinter(gopts.JSON, gopts.verbosity, term)
if err := validateCatArgs(args); err != nil {
return err
@ -170,8 +168,7 @@ func runCat(ctx context.Context, gopts GlobalOptions, args []string, term ui.Ter
return err
case "blob":
bar := newIndexTerminalProgress(printer)
err = repo.LoadIndex(ctx, bar)
err = repo.LoadIndex(ctx, printer)
if err != nil {
return err
}
@ -198,8 +195,7 @@ func runCat(ctx context.Context, gopts GlobalOptions, args []string, term ui.Ter
return errors.Fatalf("could not find snapshot: %v", err)
}
bar := newIndexTerminalProgress(printer)
err = repo.LoadIndex(ctx, bar)
err = repo.LoadIndex(ctx, printer)
if err != nil {
return err
}

View file

@ -22,7 +22,7 @@ import (
"github.com/restic/restic/internal/ui/progress"
)
func newCheckCommand() *cobra.Command {
func newCheckCommand(globalOptions *GlobalOptions) *cobra.Command {
var opts CheckOptions
cmd := &cobra.Command{
Use: "check [flags]",
@ -46,14 +46,12 @@ Exit status is 12 if the password is incorrect.
GroupID: cmdGroupDefault,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
term, cancel := setupTermstatus()
defer cancel()
summary, err := runCheck(cmd.Context(), opts, globalOptions, args, term)
summary, err := runCheck(cmd.Context(), opts, *globalOptions, args, globalOptions.term)
if globalOptions.JSON {
if err != nil && summary.NumErrors == 0 {
summary.NumErrors = 1
}
term.Print(ui.ToJSONString(summary))
globalOptions.term.Print(ui.ToJSONString(summary))
}
return err
},
@ -227,7 +225,7 @@ func runCheck(ctx context.Context, opts CheckOptions, gopts GlobalOptions, args
var printer progress.Printer
if !gopts.JSON {
printer = newTerminalProgressPrinter(gopts.JSON, gopts.verbosity, term)
printer = ui.NewProgressPrinter(gopts.JSON, gopts.verbosity, term)
} else {
printer = newJSONErrorPrinter(term)
}
@ -251,8 +249,7 @@ func runCheck(ctx context.Context, opts CheckOptions, gopts GlobalOptions, args
}
printer.P("load indexes\n")
bar := newIndexTerminalProgress(printer)
hints, errs := chkr.LoadIndex(ctx, bar)
hints, errs := chkr.LoadIndex(ctx, printer)
if ctx.Err() != nil {
return summary, ctx.Err()
}
@ -540,5 +537,6 @@ func (p *jsonErrorPrinter) E(msg string, args ...interface{}) {
}
func (*jsonErrorPrinter) S(_ string, _ ...interface{}) {}
func (*jsonErrorPrinter) P(_ string, _ ...interface{}) {}
func (*jsonErrorPrinter) PT(_ string, _ ...interface{}) {}
func (*jsonErrorPrinter) V(_ string, _ ...interface{}) {}
func (*jsonErrorPrinter) VV(_ string, _ ...interface{}) {}

View file

@ -1,17 +1,15 @@
package main
import (
"bytes"
"context"
"testing"
rtest "github.com/restic/restic/internal/test"
"github.com/restic/restic/internal/ui"
)
func testRunCheck(t testing.TB, gopts GlobalOptions) {
t.Helper()
output, err := testRunCheckOutput(gopts, true)
output, err := testRunCheckOutput(t, gopts, true)
if err != nil {
t.Error(output)
t.Fatalf("unexpected error: %+v", err)
@ -20,19 +18,17 @@ func testRunCheck(t testing.TB, gopts GlobalOptions) {
func testRunCheckMustFail(t testing.TB, gopts GlobalOptions) {
t.Helper()
_, err := testRunCheckOutput(gopts, false)
_, err := testRunCheckOutput(t, gopts, false)
rtest.Assert(t, err != nil, "expected non nil error after check of damaged repository")
}
func testRunCheckOutput(gopts GlobalOptions, checkUnused bool) (string, error) {
buf := bytes.NewBuffer(nil)
gopts.stdout = buf
err := withTermStatus(gopts, func(ctx context.Context, term ui.Terminal) error {
func testRunCheckOutput(t testing.TB, gopts GlobalOptions, checkUnused bool) (string, error) {
buf, err := withCaptureStdout(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
opts := CheckOptions{
ReadData: true,
CheckUnused: checkUnused,
}
_, err := runCheck(context.TODO(), opts, gopts, nil, term)
_, err := runCheck(context.TODO(), opts, gopts, nil, gopts.term)
return err
})
return buf.String(), err

View file

@ -16,7 +16,7 @@ import (
"github.com/spf13/pflag"
)
func newCopyCommand() *cobra.Command {
func newCopyCommand(globalOptions *GlobalOptions) *cobra.Command {
var opts CopyOptions
cmd := &cobra.Command{
Use: "copy [flags] [snapshotID ...]",
@ -48,9 +48,7 @@ Exit status is 12 if the password is incorrect.
GroupID: cmdGroupDefault,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
term, cancel := setupTermstatus()
defer cancel()
return runCopy(cmd.Context(), opts, globalOptions, args, term)
return runCopy(cmd.Context(), opts, *globalOptions, args, globalOptions.term)
},
}
@ -70,8 +68,8 @@ func (opts *CopyOptions) AddFlags(f *pflag.FlagSet) {
}
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)
printer := ui.NewProgressPrinter(false, gopts.verbosity, term)
secondaryGopts, isFromRepo, err := fillSecondaryGlobalOpts(ctx, opts.secondaryRepoOptions, gopts, "destination")
if err != nil {
return err
}
@ -103,13 +101,11 @@ func runCopy(ctx context.Context, opts CopyOptions, gopts GlobalOptions, args []
}
debug.Log("Loading source index")
bar := newIndexTerminalProgress(printer)
if err := srcRepo.LoadIndex(ctx, bar); err != nil {
if err := srcRepo.LoadIndex(ctx, printer); err != nil {
return err
}
bar = newIndexTerminalProgress(printer)
debug.Log("Loading destination index")
if err := dstRepo.LoadIndex(ctx, bar); err != nil {
if err := dstRepo.LoadIndex(ctx, printer); err != nil {
return err
}

View file

@ -7,7 +7,6 @@ import (
"testing"
rtest "github.com/restic/restic/internal/test"
"github.com/restic/restic/internal/ui"
)
func testRunCopy(t testing.TB, srcGopts GlobalOptions, dstGopts GlobalOptions) {
@ -23,8 +22,8 @@ func testRunCopy(t testing.TB, srcGopts GlobalOptions, dstGopts GlobalOptions) {
},
}
rtest.OK(t, withTermStatus(gopts, func(ctx context.Context, term ui.Terminal) error {
return runCopy(context.TODO(), copyOpts, gopts, nil, term)
rtest.OK(t, withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
return runCopy(context.TODO(), copyOpts, gopts, nil, gopts.term)
}))
}
@ -48,8 +47,8 @@ func TestCopy(t *testing.T) {
copiedSnapshotIDs := testListSnapshots(t, env2.gopts, 3)
// Check that the copies size seems reasonable
stat := dirStats(env.repo)
stat2 := dirStats(env2.repo)
stat := dirStats(t, env.repo)
stat2 := dirStats(t, env2.repo)
sizeDiff := int64(stat.size) - int64(stat2.size)
if sizeDiff < 0 {
sizeDiff = -sizeDiff
@ -72,7 +71,7 @@ func TestCopy(t *testing.T) {
testRunRestore(t, env2.gopts, restoredir, snapshotID.String())
foundMatch := false
for cmpdir := range origRestores {
diff := directoriesContentsDiff(restoredir, cmpdir)
diff := directoriesContentsDiff(t, restoredir, cmpdir)
if diff == "" {
delete(origRestores, cmpdir)
foundMatch = true

View file

@ -31,25 +31,25 @@ import (
"github.com/restic/restic/internal/ui/progress"
)
func registerDebugCommand(cmd *cobra.Command) {
func registerDebugCommand(cmd *cobra.Command, globalOptions *GlobalOptions) {
cmd.AddCommand(
newDebugCommand(),
newDebugCommand(globalOptions),
)
}
func newDebugCommand() *cobra.Command {
func newDebugCommand(globalOptions *GlobalOptions) *cobra.Command {
cmd := &cobra.Command{
Use: "debug",
Short: "Debug commands",
GroupID: cmdGroupDefault,
DisableAutoGenTag: true,
}
cmd.AddCommand(newDebugDumpCommand())
cmd.AddCommand(newDebugExamineCommand())
cmd.AddCommand(newDebugDumpCommand(globalOptions))
cmd.AddCommand(newDebugExamineCommand(globalOptions))
return cmd
}
func newDebugDumpCommand() *cobra.Command {
func newDebugDumpCommand(globalOptions *GlobalOptions) *cobra.Command {
cmd := &cobra.Command{
Use: "dump [indexes|snapshots|all|packs]",
Short: "Dump data structures",
@ -68,15 +68,13 @@ Exit status is 12 if the password is incorrect.
`,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
term, cancel := setupTermstatus()
defer cancel()
return runDebugDump(cmd.Context(), globalOptions, args, term)
return runDebugDump(cmd.Context(), *globalOptions, args, globalOptions.term)
},
}
return cmd
}
func newDebugExamineCommand() *cobra.Command {
func newDebugExamineCommand(globalOptions *GlobalOptions) *cobra.Command {
var opts DebugExamineOptions
cmd := &cobra.Command{
@ -84,9 +82,7 @@ func newDebugExamineCommand() *cobra.Command {
Short: "Examine a pack file",
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
term, cancel := setupTermstatus()
defer cancel()
return runDebugExamine(cmd.Context(), globalOptions, opts, args, term)
return runDebugExamine(cmd.Context(), *globalOptions, opts, args, globalOptions.term)
},
}
@ -188,7 +184,7 @@ func dumpIndexes(ctx context.Context, repo restic.ListerLoaderUnpacked, wr io.Wr
}
func runDebugDump(ctx context.Context, gopts GlobalOptions, args []string, term ui.Terminal) error {
printer := newTerminalProgressPrinter(false, gopts.verbosity, term)
printer := ui.NewProgressPrinter(false, gopts.verbosity, term)
if len(args) != 1 {
return errors.Fatal("type not specified")
@ -204,20 +200,20 @@ func runDebugDump(ctx context.Context, gopts GlobalOptions, args []string, term
switch tpe {
case "indexes":
return dumpIndexes(ctx, repo, globalOptions.stdout, printer)
return dumpIndexes(ctx, repo, gopts.term.OutputWriter(), printer)
case "snapshots":
return debugPrintSnapshots(ctx, repo, globalOptions.stdout)
return debugPrintSnapshots(ctx, repo, gopts.term.OutputWriter())
case "packs":
return printPacks(ctx, repo, globalOptions.stdout, printer)
return printPacks(ctx, repo, gopts.term.OutputWriter(), printer)
case "all":
printer.S("snapshots:")
err := debugPrintSnapshots(ctx, repo, globalOptions.stdout)
err := debugPrintSnapshots(ctx, repo, gopts.term.OutputWriter())
if err != nil {
return err
}
printer.S("indexes:")
err = dumpIndexes(ctx, repo, globalOptions.stdout, printer)
err = dumpIndexes(ctx, repo, gopts.term.OutputWriter(), printer)
if err != nil {
return err
}
@ -459,7 +455,7 @@ func storePlainBlob(id restic.ID, prefix string, plain []byte, printer progress.
}
func runDebugExamine(ctx context.Context, gopts GlobalOptions, opts DebugExamineOptions, args []string, term ui.Terminal) error {
printer := newTerminalProgressPrinter(false, gopts.verbosity, term)
printer := ui.NewProgressPrinter(false, gopts.verbosity, term)
if opts.ExtractPack && gopts.NoLock {
return fmt.Errorf("--extract-pack and --no-lock are mutually exclusive")
@ -488,8 +484,7 @@ func runDebugExamine(ctx context.Context, gopts GlobalOptions, opts DebugExamine
return errors.Fatal("no pack files to examine")
}
bar := newIndexTerminalProgress(printer)
err = repo.LoadIndex(ctx, bar)
err = repo.LoadIndex(ctx, printer)
if err != nil {
return err
}

View file

@ -4,6 +4,6 @@ package main
import "github.com/spf13/cobra"
func registerDebugCommand(_ *cobra.Command) {
func registerDebugCommand(_ *cobra.Command, _ *GlobalOptions) {
// No commands to register in non-debug mode
}

View file

@ -15,7 +15,7 @@ import (
"github.com/spf13/pflag"
)
func newDiffCommand() *cobra.Command {
func newDiffCommand(globalOptions *GlobalOptions) *cobra.Command {
var opts DiffOptions
cmd := &cobra.Command{
@ -52,9 +52,7 @@ Exit status is 12 if the password is incorrect.
GroupID: cmdGroupDefault,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
term, cancel := setupTermstatus()
defer cancel()
return runDiff(cmd.Context(), opts, globalOptions, args, term)
return runDiff(cmd.Context(), opts, *globalOptions, args, globalOptions.term)
},
}
@ -367,7 +365,7 @@ func runDiff(ctx context.Context, opts DiffOptions, gopts GlobalOptions, args []
return errors.Fatalf("specify two snapshot IDs")
}
printer := newTerminalProgressPrinter(gopts.JSON, gopts.verbosity, term)
printer := ui.NewProgressPrinter(gopts.JSON, gopts.verbosity, term)
ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock, printer)
if err != nil {
@ -393,8 +391,7 @@ func runDiff(ctx context.Context, opts DiffOptions, gopts GlobalOptions, args []
if !gopts.JSON {
printer.P("comparing snapshot %v to %v:\n\n", sn1.ID().Str(), sn2.ID().Str())
}
bar := newIndexTerminalProgress(printer)
if err = repo.LoadIndex(ctx, bar); err != nil {
if err = repo.LoadIndex(ctx, printer); err != nil {
return err
}
@ -426,7 +423,7 @@ func runDiff(ctx context.Context, opts DiffOptions, gopts GlobalOptions, args []
}
if gopts.JSON {
enc := json.NewEncoder(globalOptions.stdout)
enc := json.NewEncoder(gopts.term.OutputWriter())
c.printChange = func(change *Change) {
err := enc.Encode(change)
if err != nil {
@ -460,7 +457,7 @@ func runDiff(ctx context.Context, opts DiffOptions, gopts GlobalOptions, args []
updateBlobs(repo, stats.BlobsAfter.Sub(both).Sub(stats.BlobsCommon), &stats.Added, printer.E)
if gopts.JSON {
err := json.NewEncoder(globalOptions.stdout).Encode(stats)
err := json.NewEncoder(gopts.term.OutputWriter()).Encode(stats)
if err != nil {
printer.E("JSON encode failed: %v", err)
}

View file

@ -12,17 +12,14 @@ 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(gopts, func(gopts GlobalOptions) error {
func testRunDiffOutput(t testing.TB, gopts GlobalOptions, firstSnapshotID string, secondSnapshotID string) (string, error) {
buf, err := withCaptureStdout(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
opts := DiffOptions{
ShowMetadata: false,
}
return withTermStatus(gopts, func(ctx context.Context, term ui.Terminal) error {
return runDiff(ctx, opts, gopts, []string{firstSnapshotID, secondSnapshotID}, term)
})
return runDiff(ctx, opts, gopts, []string{firstSnapshotID, secondSnapshotID}, gopts.term)
})
return buf.String(), err
}
@ -126,10 +123,10 @@ func TestDiff(t *testing.T) {
// quiet suppresses the diff output except for the summary
env.gopts.Quiet = false
_, err := testRunDiffOutput(env.gopts, "", secondSnapshotID)
_, err := testRunDiffOutput(t, env.gopts, "", secondSnapshotID)
rtest.Assert(t, err != nil, "expected error on invalid snapshot id")
out, err := testRunDiffOutput(env.gopts, firstSnapshotID, secondSnapshotID)
out, err := testRunDiffOutput(t, env.gopts, firstSnapshotID, secondSnapshotID)
rtest.OK(t, err)
for _, pattern := range diffOutputRegexPatterns {
@ -140,7 +137,7 @@ func TestDiff(t *testing.T) {
// check quiet output
env.gopts.Quiet = true
outQuiet, err := testRunDiffOutput(env.gopts, firstSnapshotID, secondSnapshotID)
outQuiet, err := testRunDiffOutput(t, env.gopts, firstSnapshotID, secondSnapshotID)
rtest.OK(t, err)
rtest.Assert(t, len(outQuiet) < len(out), "expected shorter output on quiet mode %v vs. %v", len(outQuiet), len(out))
@ -157,7 +154,7 @@ func TestDiffJSON(t *testing.T) {
// quiet suppresses the diff output except for the summary
env.gopts.Quiet = false
env.gopts.JSON = true
out, err := testRunDiffOutput(env.gopts, firstSnapshotID, secondSnapshotID)
out, err := testRunDiffOutput(t, env.gopts, firstSnapshotID, secondSnapshotID)
rtest.OK(t, err)
var stat DiffStatsContainer
@ -184,7 +181,7 @@ func TestDiffJSON(t *testing.T) {
// check quiet output
env.gopts.Quiet = true
outQuiet, err := testRunDiffOutput(env.gopts, firstSnapshotID, secondSnapshotID)
outQuiet, err := testRunDiffOutput(t, env.gopts, firstSnapshotID, secondSnapshotID)
rtest.OK(t, err)
stat = DiffStatsContainer{}

View file

@ -11,14 +11,13 @@ import (
"github.com/restic/restic/internal/dump"
"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"
)
func newDumpCommand() *cobra.Command {
func newDumpCommand(globalOptions *GlobalOptions) *cobra.Command {
var opts DumpOptions
cmd := &cobra.Command{
Use: "dump [flags] snapshotID file",
@ -48,9 +47,7 @@ Exit status is 12 if the password is incorrect.
GroupID: cmdGroupDefault,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
term, cancel := setupTermstatus()
defer cancel()
return runDump(cmd.Context(), opts, globalOptions, args, term)
return runDump(cmd.Context(), opts, *globalOptions, args, globalOptions.term)
},
}
@ -133,7 +130,7 @@ func runDump(ctx context.Context, opts DumpOptions, gopts GlobalOptions, args []
return errors.Fatal("no file and no snapshot ID specified")
}
printer := newTerminalProgressPrinter(gopts.JSON, gopts.verbosity, term)
printer := ui.NewProgressPrinter(gopts.JSON, gopts.verbosity, term)
switch opts.Archive {
case "tar", "zip":
@ -163,8 +160,7 @@ func runDump(ctx context.Context, opts DumpOptions, gopts GlobalOptions, args []
return errors.Fatalf("failed to find snapshot: %v", err)
}
bar := newIndexTerminalProgress(printer)
err = repo.LoadIndex(ctx, bar)
err = repo.LoadIndex(ctx, printer)
if err != nil {
return err
}
@ -180,7 +176,7 @@ func runDump(ctx context.Context, opts DumpOptions, gopts GlobalOptions, args []
}
outputFileWriter := term.OutputRaw()
canWriteArchiveFunc := checkStdoutArchive
canWriteArchiveFunc := checkStdoutArchive(term)
if opts.Target != "" {
file, err := os.Create(opts.Target)
@ -204,9 +200,9 @@ func runDump(ctx context.Context, opts DumpOptions, gopts GlobalOptions, args []
return nil
}
func checkStdoutArchive() error {
if terminal.StdoutIsTerminal() {
return fmt.Errorf("stdout is the terminal, please redirect output")
func checkStdoutArchive(term ui.Terminal) func() error {
if term.OutputIsTerminal() {
return func() error { return fmt.Errorf("stdout is the terminal, please redirect output") }
}
return nil
return func() error { return nil }
}

View file

@ -1,8 +1,6 @@
package main
import (
"fmt"
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/feature"
"github.com/restic/restic/internal/ui/table"
@ -10,7 +8,7 @@ import (
"github.com/spf13/cobra"
)
func newFeaturesCommand() *cobra.Command {
func newFeaturesCommand(globalOptions *GlobalOptions) *cobra.Command {
cmd := &cobra.Command{
Use: "features",
Short: "Print list of feature flags",
@ -39,7 +37,7 @@ Exit status is 1 if there was any error.
return errors.Fatal("the feature command expects no arguments")
}
fmt.Printf("All Feature Flags:\n")
globalOptions.term.Print("All Feature Flags:\n")
flags := feature.Flag.List()
tab := table.New()
@ -51,7 +49,7 @@ Exit status is 1 if there was any error.
for _, flag := range flags {
tab.AddRow(flag)
}
return tab.Write(globalOptions.stdout)
return tab.Write(globalOptions.term.OutputWriter())
},
}

View file

@ -20,7 +20,7 @@ import (
"github.com/restic/restic/internal/walker"
)
func newFindCommand() *cobra.Command {
func newFindCommand(globalOptions *GlobalOptions) *cobra.Command {
var opts FindOptions
cmd := &cobra.Command{
@ -51,9 +51,7 @@ Exit status is 12 if the password is incorrect.
GroupID: cmdGroupDefault,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
term, cancel := setupTermstatus()
defer cancel()
return runFind(cmd.Context(), opts, globalOptions, args, term)
return runFind(cmd.Context(), opts, *globalOptions, args, globalOptions.term)
},
}
@ -584,7 +582,7 @@ func runFind(ctx context.Context, opts FindOptions, gopts GlobalOptions, args []
return errors.Fatal("wrong number of arguments")
}
printer := newTerminalProgressPrinter(gopts.JSON, gopts.verbosity, term)
printer := ui.NewProgressPrinter(gopts.JSON, gopts.verbosity, term)
var err error
pat := findPattern{pattern: args}
@ -625,8 +623,7 @@ func runFind(ctx context.Context, opts FindOptions, gopts GlobalOptions, args []
if err != nil {
return err
}
bar := newIndexTerminalProgress(printer)
if err = repo.LoadIndex(ctx, bar); err != nil {
if err = repo.LoadIndex(ctx, printer); err != nil {
return err
}

View file

@ -8,16 +8,13 @@ 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(gopts, func(gopts GlobalOptions) error {
buf, err := withCaptureStdout(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
gopts.JSON = wantJSON
return withTermStatus(gopts, func(ctx context.Context, term ui.Terminal) error {
return runFind(ctx, opts, gopts, []string{pattern}, term)
})
return runFind(ctx, opts, gopts, []string{pattern}, gopts.term)
})
rtest.OK(t, err)
return buf.Bytes()

View file

@ -14,7 +14,7 @@ import (
"github.com/spf13/pflag"
)
func newForgetCommand() *cobra.Command {
func newForgetCommand(globalOptions *GlobalOptions) *cobra.Command {
var opts ForgetOptions
var pruneOpts PruneOptions
@ -49,9 +49,7 @@ Exit status is 12 if the password is incorrect.
GroupID: cmdGroupDefault,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
term, cancel := setupTermstatus()
defer cancel()
return runForget(cmd.Context(), opts, pruneOpts, globalOptions, term, args)
return runForget(cmd.Context(), opts, pruneOpts, *globalOptions, globalOptions.term, args)
},
}
@ -188,7 +186,7 @@ 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")
}
printer := newTerminalProgressPrinter(gopts.JSON, gopts.verbosity, term)
printer := ui.NewProgressPrinter(gopts.JSON, gopts.verbosity, term)
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, opts.DryRun && gopts.NoLock, printer)
if err != nil {
return err
@ -253,7 +251,7 @@ func runForget(ctx context.Context, opts ForgetOptions, pruneOptions PruneOption
}
if gopts.Verbose >= 1 && !gopts.JSON {
err = PrintSnapshotGroupHeader(globalOptions.stdout, k)
err = PrintSnapshotGroupHeader(gopts.term.OutputWriter(), k)
if err != nil {
return err
}
@ -276,7 +274,7 @@ 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))
if err := PrintSnapshots(globalOptions.stdout, keep, reasons, opts.Compact); err != nil {
if err := PrintSnapshots(gopts.term.OutputWriter(), keep, reasons, opts.Compact); err != nil {
return err
}
printer.P("\n")
@ -285,7 +283,7 @@ func runForget(ctx context.Context, opts ForgetOptions, pruneOptions PruneOption
if len(remove) != 0 && !gopts.Quiet && !gopts.JSON {
printer.P("remove %d snapshots:\n", len(remove))
if err := PrintSnapshots(globalOptions.stdout, remove, nil, opts.Compact); err != nil {
if err := PrintSnapshots(gopts.term.OutputWriter(), remove, nil, opts.Compact); err != nil {
return err
}
printer.P("\n")
@ -330,7 +328,7 @@ func runForget(ctx context.Context, opts ForgetOptions, pruneOptions PruneOption
}
if gopts.JSON && len(jsonGroups) > 0 {
err = printJSONForget(globalOptions.stdout, jsonGroups)
err = printJSONForget(gopts.term.OutputWriter(), jsonGroups)
if err != nil {
return err
}

View file

@ -8,20 +8,19 @@ import (
"github.com/restic/restic/internal/restic"
rtest "github.com/restic/restic/internal/test"
"github.com/restic/restic/internal/ui"
)
func testRunForgetMayFail(gopts GlobalOptions, opts ForgetOptions, args ...string) error {
func testRunForgetMayFail(t testing.TB, gopts GlobalOptions, opts ForgetOptions, args ...string) error {
pruneOpts := PruneOptions{
MaxUnused: "5%",
}
return withTermStatus(gopts, func(ctx context.Context, term ui.Terminal) error {
return runForget(context.TODO(), opts, pruneOpts, gopts, term, args)
return withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
return runForget(context.TODO(), opts, pruneOpts, gopts, gopts.term, args)
})
}
func testRunForget(t testing.TB, gopts GlobalOptions, opts ForgetOptions, args ...string) {
rtest.OK(t, testRunForgetMayFail(gopts, opts, args...))
rtest.OK(t, testRunForgetMayFail(t, gopts, opts, args...))
}
func TestRunForgetSafetyNet(t *testing.T) {
@ -38,20 +37,20 @@ func TestRunForgetSafetyNet(t *testing.T) {
testListSnapshots(t, env.gopts, 2)
// --keep-tags invalid
err := testRunForgetMayFail(env.gopts, ForgetOptions{
err := testRunForgetMayFail(t, env.gopts, ForgetOptions{
KeepTags: restic.TagLists{restic.TagList{"invalid"}},
GroupBy: restic.SnapshotGroupByOptions{Host: true, Path: true},
})
rtest.Assert(t, strings.Contains(err.Error(), `refusing to delete last snapshot of snapshot group "host example, path`), "wrong error message got %v", err)
// disallow `forget --unsafe-allow-remove-all`
err = testRunForgetMayFail(env.gopts, ForgetOptions{
err = testRunForgetMayFail(t, env.gopts, ForgetOptions{
UnsafeAllowRemoveAll: true,
})
rtest.Assert(t, strings.Contains(err.Error(), `--unsafe-allow-remove-all is not allowed unless a snapshot filter option is specified`), "wrong error message got %v", err)
// disallow `forget` without options
err = testRunForgetMayFail(env.gopts, ForgetOptions{})
err = testRunForgetMayFail(t, env.gopts, ForgetOptions{})
rtest.Assert(t, strings.Contains(err.Error(), `no policy was specified, no snapshots will be removed`), "wrong error message got %v", err)
// `forget --host example --unsafe-allow-remove-all` should work

View file

@ -6,7 +6,6 @@ import (
"time"
"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"
@ -14,7 +13,7 @@ import (
"github.com/spf13/pflag"
)
func newGenerateCommand() *cobra.Command {
func newGenerateCommand(globalOptions *GlobalOptions) *cobra.Command {
var opts generateOptions
cmd := &cobra.Command{
@ -32,9 +31,7 @@ Exit status is 1 if there was any error.
`,
DisableAutoGenTag: true,
RunE: func(_ *cobra.Command, args []string) error {
term, cancel := setupTermstatus()
defer cancel()
return runGenerate(opts, globalOptions, args, term)
return runGenerate(opts, *globalOptions, args, globalOptions.term)
},
}
opts.AddFlags(cmd.Flags())
@ -75,10 +72,8 @@ func writeManpages(root *cobra.Command, dir string, printer progress.Printer) er
return doc.GenManTree(root, header, dir)
}
func writeCompletion(filename string, shell string, generate func(w io.Writer) error, printer progress.Printer) (err error) {
if terminal.StdoutIsTerminal() {
printer.P("writing %s completion file to %v", shell, filename)
}
func writeCompletion(filename string, shell string, generate func(w io.Writer) error, printer progress.Printer, gopts GlobalOptions) (err error) {
printer.PT("writing %s completion file to %v", shell, filename)
var outWriter io.Writer
if filename != "-" {
var outFile *os.File
@ -89,7 +84,7 @@ func writeCompletion(filename string, shell string, generate func(w io.Writer) e
defer func() { err = outFile.Close() }()
outWriter = outFile
} else {
outWriter = globalOptions.stdout
outWriter = gopts.term.OutputWriter()
}
err = generate(outWriter)
@ -120,8 +115,8 @@ func runGenerate(opts generateOptions, gopts GlobalOptions, args []string, term
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()
printer := ui.NewProgressPrinter(gopts.JSON, gopts.verbosity, term)
cmdRoot := newRootCommand(&GlobalOptions{})
if opts.ManDir != "" {
err := writeManpages(cmdRoot, opts.ManDir, printer)
@ -136,28 +131,28 @@ func runGenerate(opts generateOptions, gopts GlobalOptions, args []string, term
}
if opts.BashCompletionFile != "" {
err := writeCompletion(opts.BashCompletionFile, "bash", cmdRoot.GenBashCompletion, printer)
err := writeCompletion(opts.BashCompletionFile, "bash", cmdRoot.GenBashCompletion, printer, gopts)
if err != nil {
return err
}
}
if opts.FishCompletionFile != "" {
err := writeCompletion(opts.FishCompletionFile, "fish", func(w io.Writer) error { return cmdRoot.GenFishCompletion(w, true) }, printer)
err := writeCompletion(opts.FishCompletionFile, "fish", func(w io.Writer) error { return cmdRoot.GenFishCompletion(w, true) }, printer, gopts)
if err != nil {
return err
}
}
if opts.ZSHCompletionFile != "" {
err := writeCompletion(opts.ZSHCompletionFile, "zsh", cmdRoot.GenZshCompletion, printer)
err := writeCompletion(opts.ZSHCompletionFile, "zsh", cmdRoot.GenZshCompletion, printer, gopts)
if err != nil {
return err
}
}
if opts.PowerShellCompletionFile != "" {
err := writeCompletion(opts.PowerShellCompletionFile, "powershell", cmdRoot.GenPowerShellCompletion, printer)
err := writeCompletion(opts.PowerShellCompletionFile, "powershell", cmdRoot.GenPowerShellCompletion, printer, gopts)
if err != nil {
return err
}

View file

@ -6,14 +6,11 @@ import (
"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)
})
func testRunGenerate(t testing.TB, gopts GlobalOptions, opts generateOptions) ([]byte, error) {
buf, err := withCaptureStdout(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
return runGenerate(opts, gopts, []string{}, gopts.term)
})
return buf.Bytes(), err
}
@ -31,14 +28,14 @@ func TestGenerateStdout(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
output, err := testRunGenerate(globalOptions, tc.opts)
output, err := testRunGenerate(t, GlobalOptions{}, tc.opts)
rtest.OK(t, err)
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) {
_, err := testRunGenerate(globalOptions, generateOptions{BashCompletionFile: "-", FishCompletionFile: "-"})
_, err := testRunGenerate(t, GlobalOptions{}, generateOptions{BashCompletionFile: "-", FishCompletionFile: "-"})
rtest.Assert(t, err != nil, "generate shell completions to stdout for two shells fails")
})
}

View file

@ -17,7 +17,7 @@ import (
"github.com/spf13/pflag"
)
func newInitCommand() *cobra.Command {
func newInitCommand(globalOptions *GlobalOptions) *cobra.Command {
var opts InitOptions
cmd := &cobra.Command{
@ -35,9 +35,7 @@ Exit status is 1 if there was any error.
GroupID: cmdGroupDefault,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
term, cancel := setupTermstatus()
defer cancel()
return runInit(cmd.Context(), opts, globalOptions, args, term)
return runInit(cmd.Context(), opts, *globalOptions, args, globalOptions.term)
},
}
opts.AddFlags(cmd.Flags())
@ -62,7 +60,7 @@ func runInit(ctx context.Context, opts InitOptions, gopts GlobalOptions, args []
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)
printer := ui.NewProgressPrinter(gopts.JSON, gopts.verbosity, term)
var version uint
switch opts.RepositoryVersion {
@ -94,8 +92,7 @@ func runInit(ctx context.Context, opts InitOptions, gopts GlobalOptions, args []
gopts.password, err = ReadPasswordTwice(ctx, gopts,
"enter password for new repository: ",
"enter password again: ",
printer)
"enter password again: ")
if err != nil {
return err
}
@ -134,7 +131,7 @@ func runInit(ctx context.Context, opts InitOptions, gopts GlobalOptions, args []
ID: s.Config().ID,
Repository: location.StripPassword(gopts.backends, gopts.Repo),
}
return json.NewEncoder(globalOptions.stdout).Encode(status)
return json.NewEncoder(gopts.term.OutputWriter()).Encode(status)
}
return nil
@ -142,7 +139,7 @@ func runInit(ctx context.Context, opts InitOptions, gopts GlobalOptions, args []
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", printer)
otherGopts, _, err := fillSecondaryGlobalOpts(ctx, opts.secondaryRepoOptions, gopts, "secondary")
if err != nil {
return nil, err
}

View file

@ -9,24 +9,23 @@ 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) {
func testRunInit(t testing.TB, gopts GlobalOptions) {
repository.TestUseLowSecurityKDFParameters(t)
restic.TestDisableCheckPolynomial(t)
restic.TestSetLockTimeout(t, 0)
err := withTermStatus(opts, func(ctx context.Context, term ui.Terminal) error {
return runInit(ctx, InitOptions{}, opts, nil, term)
err := withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
return runInit(ctx, InitOptions{}, gopts, nil, gopts.term)
})
rtest.OK(t, err)
t.Logf("repository initialized at %v", opts.Repo)
t.Logf("repository initialized at %v", gopts.Repo)
// create temporary junk files to verify that restic does not trip over them
for _, path := range []string{"index", "snapshots", "keys", "locks", filepath.Join("data", "00")} {
rtest.OK(t, os.WriteFile(filepath.Join(opts.Repo, path, "tmp12345"), []byte("junk file"), 0o600))
rtest.OK(t, os.WriteFile(filepath.Join(gopts.Repo, path, "tmp12345"), []byte("junk file"), 0o600))
}
}
@ -44,21 +43,29 @@ func TestInitCopyChunkerParams(t *testing.T) {
password: env2.gopts.password,
},
}
err := withTermStatus(env.gopts, func(ctx context.Context, term ui.Terminal) error {
return runInit(ctx, initOpts, env.gopts, nil, term)
err := withTermStatus(t, env.gopts, func(ctx context.Context, gopts GlobalOptions) error {
return runInit(ctx, initOpts, gopts, nil, gopts.term)
})
rtest.Assert(t, err != nil, "expected invalid init options to fail")
initOpts.CopyChunkerParameters = true
err = withTermStatus(env.gopts, func(ctx context.Context, term ui.Terminal) error {
return runInit(ctx, initOpts, env.gopts, nil, term)
err = withTermStatus(t, env.gopts, func(ctx context.Context, gopts GlobalOptions) error {
return runInit(ctx, initOpts, gopts, nil, gopts.term)
})
rtest.OK(t, err)
repo, err := OpenRepository(context.TODO(), env.gopts, &progress.NoopPrinter{})
var repo *repository.Repository
err = withTermStatus(t, env.gopts, func(ctx context.Context, gopts GlobalOptions) error {
repo, err = OpenRepository(ctx, gopts, &progress.NoopPrinter{})
return err
})
rtest.OK(t, err)
otherRepo, err := OpenRepository(context.TODO(), env2.gopts, &progress.NoopPrinter{})
var otherRepo *repository.Repository
err = withTermStatus(t, env2.gopts, func(ctx context.Context, gopts GlobalOptions) error {
otherRepo, err = OpenRepository(ctx, gopts, &progress.NoopPrinter{})
return err
})
rtest.OK(t, err)
rtest.Assert(t, repo.Config().ChunkerPolynomial == otherRepo.Config().ChunkerPolynomial,

View file

@ -4,7 +4,7 @@ import (
"github.com/spf13/cobra"
)
func newKeyCommand() *cobra.Command {
func newKeyCommand(globalOptions *GlobalOptions) *cobra.Command {
cmd := &cobra.Command{
Use: "key",
Short: "Manage keys (passwords)",
@ -17,10 +17,10 @@ per repository.
}
cmd.AddCommand(
newKeyAddCommand(),
newKeyListCommand(),
newKeyPasswdCommand(),
newKeyRemoveCommand(),
newKeyAddCommand(globalOptions),
newKeyListCommand(globalOptions),
newKeyPasswdCommand(globalOptions),
newKeyRemoveCommand(globalOptions),
)
return cmd
}

View file

@ -12,7 +12,7 @@ import (
"github.com/spf13/pflag"
)
func newKeyAddCommand() *cobra.Command {
func newKeyAddCommand(globalOptions *GlobalOptions) *cobra.Command {
var opts KeyAddOptions
cmd := &cobra.Command{
@ -32,9 +32,7 @@ Exit status is 12 if the password is incorrect.
`,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
term, cancel := setupTermstatus()
defer cancel()
return runKeyAdd(cmd.Context(), globalOptions, opts, args, term)
return runKeyAdd(cmd.Context(), *globalOptions, opts, args, globalOptions.term)
},
}
@ -61,7 +59,7 @@ func runKeyAdd(ctx context.Context, gopts GlobalOptions, opts KeyAddOptions, arg
return fmt.Errorf("the key add command expects no arguments, only options - please see `restic help key add` for usage and flags")
}
printer := newTerminalProgressPrinter(false, gopts.verbosity, term)
printer := ui.NewProgressPrinter(false, gopts.verbosity, term)
ctx, repo, unlock, err := openWithAppendLock(ctx, gopts, false, printer)
if err != nil {
return err
@ -72,7 +70,7 @@ func runKeyAdd(ctx context.Context, gopts GlobalOptions, opts KeyAddOptions, arg
}
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)
pw, err := getNewPassword(ctx, gopts, opts.NewPasswordFile, opts.InsecureNoPassword)
if err != nil {
return err
}
@ -95,7 +93,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, printer progress.Printer) (string, error) {
func getNewPassword(ctx context.Context, gopts GlobalOptions, newPasswordFile string, insecureNoPassword bool) (string, error) {
if testKeyNewPassword != "" {
return testKeyNewPassword, nil
}
@ -127,8 +125,7 @@ func getNewPassword(ctx context.Context, gopts GlobalOptions, newPasswordFile st
return ReadPasswordTwice(ctx, newopts,
"enter new password: ",
"enter password again: ",
printer)
"enter password again: ")
}
func switchToNewKeyAndRemoveIfBroken(ctx context.Context, repo *repository.Repository, key *repository.Key, pw string) error {

View file

@ -12,15 +12,12 @@ 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(gopts, func(gopts GlobalOptions) error {
return withTermStatus(gopts, func(ctx context.Context, term ui.Terminal) error {
return runKeyList(ctx, gopts, []string{}, term)
})
buf, err := withCaptureStdout(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
return runKeyList(ctx, gopts, []string{}, gopts.term)
})
rtest.OK(t, err)
@ -43,8 +40,8 @@ func testRunKeyAddNewKey(t testing.TB, newPassword string, gopts GlobalOptions)
testKeyNewPassword = ""
}()
err := withTermStatus(gopts, func(ctx context.Context, term ui.Terminal) error {
return runKeyAdd(ctx, gopts, KeyAddOptions{}, []string{}, term)
err := withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
return runKeyAdd(ctx, gopts, KeyAddOptions{}, []string{}, gopts.term)
})
rtest.OK(t, err)
}
@ -56,21 +53,24 @@ func testRunKeyAddNewKeyUserHost(t testing.TB, gopts GlobalOptions) {
}()
t.Log("adding key for john@example.com")
err := withTermStatus(gopts, func(ctx context.Context, term ui.Terminal) error {
err := withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
return runKeyAdd(ctx, gopts, KeyAddOptions{
Username: "john",
Hostname: "example.com",
}, []string{}, term)
}, []string{}, gopts.term)
})
rtest.OK(t, err)
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)
_ = withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
repo, err := OpenRepository(ctx, gopts, &progress.NoopPrinter{})
rtest.OK(t, err)
key, err := repository.SearchKey(ctx, repo, testKeyNewPassword, 2, "")
rtest.OK(t, err)
rtest.Equals(t, "john", key.Username)
rtest.Equals(t, "example.com", key.Hostname)
rtest.Equals(t, "john", key.Username)
rtest.Equals(t, "example.com", key.Hostname)
return nil
})
}
func testRunKeyPasswd(t testing.TB, newPassword string, gopts GlobalOptions) {
@ -79,8 +79,8 @@ func testRunKeyPasswd(t testing.TB, newPassword string, gopts GlobalOptions) {
testKeyNewPassword = ""
}()
err := withTermStatus(gopts, func(ctx context.Context, term ui.Terminal) error {
return runKeyPasswd(ctx, gopts, KeyPasswdOptions{}, []string{}, term)
err := withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
return runKeyPasswd(ctx, gopts, KeyPasswdOptions{}, []string{}, gopts.term)
})
rtest.OK(t, err)
}
@ -88,8 +88,8 @@ func testRunKeyPasswd(t testing.TB, newPassword string, gopts GlobalOptions) {
func testRunKeyRemove(t testing.TB, gopts GlobalOptions, IDs []string) {
t.Logf("remove %d keys: %q\n", len(IDs), IDs)
for _, id := range IDs {
err := withTermStatus(gopts, func(ctx context.Context, term ui.Terminal) error {
return runKeyRemove(ctx, gopts, []string{id}, term)
err := withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
return runKeyRemove(ctx, gopts, []string{id}, gopts.term)
})
rtest.OK(t, err)
}
@ -121,8 +121,8 @@ func TestKeyAddRemove(t *testing.T) {
env.gopts.password = passwordList[len(passwordList)-1]
t.Logf("testing access with last password %q\n", env.gopts.password)
err := withTermStatus(env.gopts, func(ctx context.Context, term ui.Terminal) error {
return runKeyList(ctx, env.gopts, []string{}, term)
err := withTermStatus(t, env.gopts, func(ctx context.Context, gopts GlobalOptions) error {
return runKeyList(ctx, gopts, []string{}, gopts.term)
})
rtest.OK(t, err)
testRunCheck(t, env.gopts)
@ -135,21 +135,21 @@ func TestKeyAddInvalid(t *testing.T) {
defer cleanup()
testRunInit(t, env.gopts)
err := withTermStatus(env.gopts, func(ctx context.Context, term ui.Terminal) error {
return runKeyAdd(ctx, env.gopts, KeyAddOptions{
err := withTermStatus(t, env.gopts, func(ctx context.Context, gopts GlobalOptions) error {
return runKeyAdd(ctx, gopts, KeyAddOptions{
NewPasswordFile: "some-file",
InsecureNoPassword: true,
}, []string{}, term)
}, []string{}, gopts.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 = withTermStatus(env.gopts, func(ctx context.Context, term ui.Terminal) error {
return runKeyAdd(ctx, env.gopts, KeyAddOptions{
err = withTermStatus(t, env.gopts, func(ctx context.Context, gopts GlobalOptions) error {
return runKeyAdd(ctx, gopts, KeyAddOptions{
NewPasswordFile: pwfile,
}, []string{}, term)
}, []string{}, gopts.term)
})
rtest.Assert(t, strings.Contains(err.Error(), "an empty password is not allowed by default"), "unexpected error message, got %q", err)
}
@ -161,10 +161,10 @@ func TestKeyAddEmpty(t *testing.T) {
defer cleanup()
testRunInit(t, env.gopts)
err := withTermStatus(env.gopts, func(ctx context.Context, term ui.Terminal) error {
return runKeyAdd(ctx, env.gopts, KeyAddOptions{
err := withTermStatus(t, env.gopts, func(ctx context.Context, gopts GlobalOptions) error {
return runKeyAdd(ctx, gopts, KeyAddOptions{
InsecureNoPassword: true,
}, []string{}, term)
}, []string{}, gopts.term)
})
rtest.OK(t, err)
@ -196,21 +196,21 @@ func TestKeyProblems(t *testing.T) {
testKeyNewPassword = ""
}()
err := withTermStatus(env.gopts, func(ctx context.Context, term ui.Terminal) error {
return runKeyPasswd(ctx, env.gopts, KeyPasswdOptions{}, []string{}, term)
err := withTermStatus(t, env.gopts, func(ctx context.Context, gopts GlobalOptions) error {
return runKeyPasswd(ctx, gopts, KeyPasswdOptions{}, []string{}, gopts.term)
})
t.Log(err)
rtest.Assert(t, err != nil, "expected passwd change to fail")
err = withTermStatus(env.gopts, func(ctx context.Context, term ui.Terminal) error {
return runKeyAdd(ctx, env.gopts, KeyAddOptions{}, []string{}, term)
err = withTermStatus(t, env.gopts, func(ctx context.Context, gopts GlobalOptions) error {
return runKeyAdd(ctx, gopts, KeyAddOptions{}, []string{}, gopts.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)
err = withTermStatus(env.gopts, func(ctx context.Context, term ui.Terminal) error {
return runKeyList(ctx, env.gopts, []string{}, term)
err = withTermStatus(t, env.gopts, func(ctx context.Context, gopts GlobalOptions) error {
return runKeyList(ctx, gopts, []string{}, gopts.term)
})
rtest.OK(t, err)
testRunCheck(t, env.gopts)
@ -225,32 +225,32 @@ func TestKeyCommandInvalidArguments(t *testing.T) {
return &emptySaveBackend{r}, nil
}
err := withTermStatus(env.gopts, func(ctx context.Context, term ui.Terminal) error {
return runKeyAdd(ctx, env.gopts, KeyAddOptions{}, []string{"johndoe"}, term)
err := withTermStatus(t, env.gopts, func(ctx context.Context, gopts GlobalOptions) error {
return runKeyAdd(ctx, gopts, KeyAddOptions{}, []string{"johndoe"}, gopts.term)
})
t.Log(err)
rtest.Assert(t, err != nil && strings.Contains(err.Error(), "no arguments"), "unexpected error for key add: %v", err)
err = withTermStatus(env.gopts, func(ctx context.Context, term ui.Terminal) error {
return runKeyPasswd(ctx, env.gopts, KeyPasswdOptions{}, []string{"johndoe"}, term)
err = withTermStatus(t, env.gopts, func(ctx context.Context, gopts GlobalOptions) error {
return runKeyPasswd(ctx, gopts, KeyPasswdOptions{}, []string{"johndoe"}, gopts.term)
})
t.Log(err)
rtest.Assert(t, err != nil && strings.Contains(err.Error(), "no arguments"), "unexpected error for key passwd: %v", err)
err = withTermStatus(env.gopts, func(ctx context.Context, term ui.Terminal) error {
return runKeyList(ctx, env.gopts, []string{"johndoe"}, term)
err = withTermStatus(t, env.gopts, func(ctx context.Context, gopts GlobalOptions) error {
return runKeyList(ctx, gopts, []string{"johndoe"}, gopts.term)
})
t.Log(err)
rtest.Assert(t, err != nil && strings.Contains(err.Error(), "no arguments"), "unexpected error for key list: %v", err)
err = withTermStatus(env.gopts, func(ctx context.Context, term ui.Terminal) error {
return runKeyRemove(ctx, env.gopts, []string{}, term)
err = withTermStatus(t, env.gopts, func(ctx context.Context, gopts GlobalOptions) error {
return runKeyRemove(ctx, gopts, []string{}, gopts.term)
})
t.Log(err)
rtest.Assert(t, err != nil && strings.Contains(err.Error(), "one argument"), "unexpected error for key remove: %v", err)
err = withTermStatus(env.gopts, func(ctx context.Context, term ui.Terminal) error {
return runKeyRemove(ctx, env.gopts, []string{"john", "doe"}, term)
err = withTermStatus(t, env.gopts, func(ctx context.Context, gopts GlobalOptions) error {
return runKeyRemove(ctx, gopts, []string{"john", "doe"}, gopts.term)
})
t.Log(err)
rtest.Assert(t, err != nil && strings.Contains(err.Error(), "one argument"), "unexpected error for key remove: %v", err)

View file

@ -14,7 +14,7 @@ import (
"github.com/spf13/cobra"
)
func newKeyListCommand() *cobra.Command {
func newKeyListCommand(globalOptions *GlobalOptions) *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Short: "List keys (passwords)",
@ -34,9 +34,7 @@ Exit status is 12 if the password is incorrect.
`,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
term, cancel := setupTermstatus()
defer cancel()
return runKeyList(cmd.Context(), globalOptions, args, term)
return runKeyList(cmd.Context(), *globalOptions, args, globalOptions.term)
},
}
return cmd
@ -47,7 +45,7 @@ func runKeyList(ctx context.Context, gopts GlobalOptions, args []string, term ui
return fmt.Errorf("the key list command expects no arguments, only options - please see `restic help key list` for usage and flags")
}
printer := newTerminalProgressPrinter(gopts.JSON, gopts.verbosity, term)
printer := ui.NewProgressPrinter(gopts.JSON, gopts.verbosity, term)
ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock, printer)
if err != nil {
return err
@ -97,7 +95,7 @@ func listKeys(ctx context.Context, s *repository.Repository, gopts GlobalOptions
}
if gopts.JSON {
return json.NewEncoder(globalOptions.stdout).Encode(keys)
return json.NewEncoder(gopts.term.OutputWriter()).Encode(keys)
}
tab := table.New()
@ -110,5 +108,5 @@ func listKeys(ctx context.Context, s *repository.Repository, gopts GlobalOptions
tab.AddRow(key)
}
return tab.Write(globalOptions.stdout)
return tab.Write(gopts.term.OutputWriter())
}

View file

@ -12,7 +12,7 @@ import (
"github.com/spf13/pflag"
)
func newKeyPasswdCommand() *cobra.Command {
func newKeyPasswdCommand(globalOptions *GlobalOptions) *cobra.Command {
var opts KeyPasswdOptions
cmd := &cobra.Command{
@ -33,9 +33,7 @@ Exit status is 12 if the password is incorrect.
`,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
term, cancel := setupTermstatus()
defer cancel()
return runKeyPasswd(cmd.Context(), globalOptions, opts, args, term)
return runKeyPasswd(cmd.Context(), *globalOptions, opts, args, globalOptions.term)
},
}
@ -56,7 +54,7 @@ func runKeyPasswd(ctx context.Context, gopts GlobalOptions, opts KeyPasswdOption
return fmt.Errorf("the key passwd command expects no arguments, only options - please see `restic help key passwd` for usage and flags")
}
printer := newTerminalProgressPrinter(false, gopts.verbosity, term)
printer := ui.NewProgressPrinter(false, gopts.verbosity, term)
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, false, printer)
if err != nil {
return err
@ -67,7 +65,7 @@ func runKeyPasswd(ctx context.Context, gopts GlobalOptions, opts KeyPasswdOption
}
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)
pw, err := getNewPassword(ctx, gopts, opts.NewPasswordFile, opts.InsecureNoPassword)
if err != nil {
return err
}

View file

@ -12,7 +12,7 @@ import (
"github.com/spf13/cobra"
)
func newKeyRemoveCommand() *cobra.Command {
func newKeyRemoveCommand(globalOptions *GlobalOptions) *cobra.Command {
cmd := &cobra.Command{
Use: "remove [ID]",
Short: "Remove key ID (password) from the repository.",
@ -31,9 +31,7 @@ Exit status is 12 if the password is incorrect.
`,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
term, cancel := setupTermstatus()
defer cancel()
return runKeyRemove(cmd.Context(), globalOptions, args, term)
return runKeyRemove(cmd.Context(), *globalOptions, args, globalOptions.term)
},
}
return cmd
@ -44,7 +42,7 @@ func runKeyRemove(ctx context.Context, gopts GlobalOptions, args []string, term
return fmt.Errorf("key remove expects one argument as the key id")
}
printer := newTerminalProgressPrinter(gopts.JSON, gopts.verbosity, term)
printer := ui.NewProgressPrinter(gopts.JSON, gopts.verbosity, term)
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, false, printer)
if err != nil {
return err

View file

@ -12,7 +12,7 @@ import (
"github.com/spf13/cobra"
)
func newListCommand() *cobra.Command {
func newListCommand(globalOptions *GlobalOptions) *cobra.Command {
var listAllowedArgs = []string{"blobs", "packs", "index", "snapshots", "keys", "locks"}
var listAllowedArgsUseString = strings.Join(listAllowedArgs, "|")
@ -34,9 +34,7 @@ Exit status is 12 if the password is incorrect.
DisableAutoGenTag: true,
GroupID: cmdGroupDefault,
RunE: func(cmd *cobra.Command, args []string) error {
term, cancel := setupTermstatus()
defer cancel()
return runList(cmd.Context(), globalOptions, args, term)
return runList(cmd.Context(), *globalOptions, args, globalOptions.term)
},
ValidArgs: listAllowedArgs,
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
@ -45,7 +43,7 @@ Exit status is 12 if the password is incorrect.
}
func runList(ctx context.Context, gopts GlobalOptions, args []string, term ui.Terminal) error {
printer := newTerminalProgressPrinter(false, gopts.verbosity, term)
printer := ui.NewProgressPrinter(false, gopts.verbosity, term)
if len(args) != 1 {
return errors.Fatal("type not specified")

View file

@ -8,14 +8,11 @@ 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, 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)
})
func testRunList(t testing.TB, gopts GlobalOptions, tpe string) restic.IDs {
buf, err := withCaptureStdout(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
return runList(ctx, gopts, []string{tpe}, gopts.term)
})
rtest.OK(t, err)
return parseIDsFromReader(t, buf)
@ -39,9 +36,9 @@ func parseIDsFromReader(t testing.TB, rd io.Reader) restic.IDs {
return IDs
}
func testListSnapshots(t testing.TB, opts GlobalOptions, expected int) restic.IDs {
func testListSnapshots(t testing.TB, gopts GlobalOptions, expected int) restic.IDs {
t.Helper()
snapshotIDs := testRunList(t, opts, "snapshots")
snapshotIDs := testRunList(t, gopts, "snapshots")
rtest.Assert(t, len(snapshotIDs) == expected, "expected %v snapshot, got %v", expected, snapshotIDs)
return snapshotIDs
}

View file

@ -22,7 +22,7 @@ import (
"github.com/restic/restic/internal/walker"
)
func newLsCommand() *cobra.Command {
func newLsCommand(globalOptions *GlobalOptions) *cobra.Command {
var opts LsOptions
cmd := &cobra.Command{
@ -60,9 +60,7 @@ Exit status is 12 if the password is incorrect.
DisableAutoGenTag: true,
GroupID: cmdGroupDefault,
RunE: func(cmd *cobra.Command, args []string) error {
term, cancel := setupTermstatus()
defer cancel()
return runLs(cmd.Context(), opts, globalOptions, args, term)
return runLs(cmd.Context(), opts, *globalOptions, args, globalOptions.term)
},
}
opts.AddFlags(cmd.Flags())
@ -304,7 +302,7 @@ type toSortOutput struct {
}
func runLs(ctx context.Context, opts LsOptions, gopts GlobalOptions, args []string, term ui.Terminal) error {
termPrinter := newTerminalProgressPrinter(gopts.JSON, gopts.verbosity, term)
termPrinter := ui.NewProgressPrinter(gopts.JSON, gopts.verbosity, term)
if len(args) == 0 {
return errors.Fatal("no snapshot ID specified, specify snapshot ID or use special ID 'latest'")
@ -375,8 +373,7 @@ func runLs(ctx context.Context, opts LsOptions, gopts GlobalOptions, args []stri
return err
}
bar := newIndexTerminalProgress(termPrinter)
if err = repo.LoadIndex(ctx, bar); err != nil {
if err = repo.LoadIndex(ctx, termPrinter); err != nil {
return err
}
@ -384,11 +381,11 @@ func runLs(ctx context.Context, opts LsOptions, gopts GlobalOptions, args []stri
if gopts.JSON {
printer = &jsonLsPrinter{
enc: json.NewEncoder(globalOptions.stdout),
enc: json.NewEncoder(gopts.term.OutputWriter()),
}
} else if opts.Ncdu {
printer = &ncduLsPrinter{
out: globalOptions.stdout,
out: gopts.term.OutputWriter(),
}
} else {
printer = &textLsPrinter{

View file

@ -10,15 +10,12 @@ 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(gopts, func(gopts GlobalOptions) error {
buf, err := withCaptureStdout(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
gopts.Quiet = true
return withTermStatus(gopts, func(ctx context.Context, term ui.Terminal) error {
return runLs(context.TODO(), opts, gopts, args, term)
})
return runLs(context.TODO(), opts, gopts, args, gopts.term)
})
rtest.OK(t, err)
return buf.Bytes()

View file

@ -12,7 +12,7 @@ import (
"github.com/spf13/pflag"
)
func newMigrateCommand() *cobra.Command {
func newMigrateCommand(globalOptions *GlobalOptions) *cobra.Command {
var opts MigrateOptions
cmd := &cobra.Command{
@ -35,9 +35,7 @@ Exit status is 12 if the password is incorrect.
DisableAutoGenTag: true,
GroupID: cmdGroupDefault,
RunE: func(cmd *cobra.Command, args []string) error {
term, cancel := setupTermstatus()
defer cancel()
return runMigrate(cmd.Context(), opts, globalOptions, args, term)
return runMigrate(cmd.Context(), opts, *globalOptions, args, globalOptions.term)
},
}
@ -136,7 +134,7 @@ func applyMigrations(ctx context.Context, opts MigrateOptions, gopts GlobalOptio
}
func runMigrate(ctx context.Context, opts MigrateOptions, gopts GlobalOptions, args []string, term ui.Terminal) error {
printer := newTerminalProgressPrinter(false, gopts.verbosity, term)
printer := ui.NewProgressPrinter(false, gopts.verbosity, term)
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, false, printer)
if err != nil {

View file

@ -24,11 +24,11 @@ import (
"github.com/anacrolix/fuse/fs"
)
func registerMountCommand(cmdRoot *cobra.Command) {
cmdRoot.AddCommand(newMountCommand())
func registerMountCommand(cmdRoot *cobra.Command, globalOptions *GlobalOptions) {
cmdRoot.AddCommand(newMountCommand(globalOptions))
}
func newMountCommand() *cobra.Command {
func newMountCommand(globalOptions *GlobalOptions) *cobra.Command {
var opts MountOptions
cmd := &cobra.Command{
@ -82,9 +82,7 @@ Exit status is 12 if the password is incorrect.
DisableAutoGenTag: true,
GroupID: cmdGroupDefault,
RunE: func(cmd *cobra.Command, args []string) error {
term, cancel := setupTermstatus()
defer cancel()
return runMount(cmd.Context(), opts, globalOptions, args, term)
return runMount(cmd.Context(), opts, *globalOptions, args, globalOptions.term)
},
}
@ -116,7 +114,7 @@ func (opts *MountOptions) AddFlags(f *pflag.FlagSet) {
}
func runMount(ctx context.Context, opts MountOptions, gopts GlobalOptions, args []string, term ui.Terminal) error {
printer := newTerminalProgressPrinter(false, gopts.verbosity, term)
printer := ui.NewProgressPrinter(false, gopts.verbosity, term)
if opts.TimeTemplate == "" {
return errors.Fatal("time template string cannot be empty")
@ -148,8 +146,7 @@ func runMount(ctx context.Context, opts MountOptions, gopts GlobalOptions, args
}
defer unlock()
bar := newIndexTerminalProgress(printer)
err = repo.LoadIndex(ctx, bar)
err = repo.LoadIndex(ctx, printer)
if err != nil {
return err
}

View file

@ -5,6 +5,6 @@ package main
import "github.com/spf13/cobra"
func registerMountCommand(_ *cobra.Command) {
func registerMountCommand(_ *cobra.Command, _ *GlobalOptions) {
// Mount command not supported on these platforms
}

View file

@ -62,8 +62,8 @@ func testRunMount(t testing.TB, gopts GlobalOptions, dir string, wg *sync.WaitGr
opts := MountOptions{
TimeTemplate: time.RFC3339,
}
rtest.OK(t, withTermStatus(gopts, func(ctx context.Context, term ui.Terminal) error {
return runMount(context.TODO(), opts, gopts, []string{dir}, term)
rtest.OK(t, withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
return runMount(context.TODO(), opts, gopts, []string{dir}, gopts.term)
}))
}
@ -128,8 +128,8 @@ func checkSnapshots(t testing.TB, gopts GlobalOptions, mountpoint string, snapsh
}
}
err := withTermStatus(gopts, func(ctx context.Context, term ui.Terminal) error {
printer := newTerminalProgressPrinter(gopts.JSON, gopts.verbosity, term)
err := withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
printer := ui.NewProgressPrinter(gopts.JSON, gopts.verbosity, gopts.term)
_, repo, unlock, err := openWithReadLock(ctx, gopts, false, printer)
if err != nil {
return err

View file

@ -8,7 +8,7 @@ import (
"github.com/spf13/cobra"
)
func newOptionsCommand() *cobra.Command {
func newOptionsCommand(globalOptions *GlobalOptions) *cobra.Command {
cmd := &cobra.Command{
Use: "options",
Short: "Print list of extended options",
@ -24,7 +24,7 @@ Exit status is 1 if there was any error.
GroupID: cmdGroupAdvanced,
DisableAutoGenTag: true,
Run: func(_ *cobra.Command, _ []string) {
fmt.Printf("All Extended Options:\n")
globalOptions.term.Print("All Extended Options:")
var maxLen int
for _, opt := range options.List() {
if l := len(opt.Namespace + "." + opt.Name); l > maxLen {
@ -32,7 +32,7 @@ Exit status is 1 if there was any error.
}
}
for _, opt := range options.List() {
fmt.Printf(" %*s %s\n", -maxLen, opt.Namespace+"."+opt.Name, opt.Text)
globalOptions.term.Print(fmt.Sprintf(" %*s %s", -maxLen, opt.Namespace+"."+opt.Name, opt.Text))
}
},
}

View file

@ -18,7 +18,7 @@ import (
"github.com/spf13/pflag"
)
func newPruneCommand() *cobra.Command {
func newPruneCommand(globalOptions *GlobalOptions) *cobra.Command {
var opts PruneOptions
cmd := &cobra.Command{
@ -40,9 +40,7 @@ Exit status is 12 if the password is incorrect.
GroupID: cmdGroupDefault,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, _ []string) error {
term, cancel := setupTermstatus()
defer cancel()
return runPrune(cmd.Context(), opts, globalOptions, term)
return runPrune(cmd.Context(), opts, *globalOptions, globalOptions.term)
},
}
@ -168,7 +166,7 @@ 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")
}
printer := newTerminalProgressPrinter(false, gopts.verbosity, term)
printer := ui.NewProgressPrinter(false, gopts.verbosity, term)
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, opts.DryRun && gopts.NoLock, printer)
if err != nil {
return err
@ -192,8 +190,7 @@ func runPruneWithRepo(ctx context.Context, opts PruneOptions, repo *repository.R
}
// loading the index before the snapshots is ok, as we use an exclusive lock here
bar := newIndexTerminalProgress(printer)
err := repo.LoadIndex(ctx, bar)
err := repo.LoadIndex(ctx, printer)
if err != nil {
return err
}

View file

@ -9,28 +9,27 @@ 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"
)
func testRunPrune(t testing.TB, gopts GlobalOptions, opts PruneOptions) {
t.Helper()
rtest.OK(t, testRunPruneOutput(gopts, opts))
rtest.OK(t, testRunPruneOutput(t, gopts, opts))
}
func testRunPruneMustFail(t testing.TB, gopts GlobalOptions, opts PruneOptions) {
t.Helper()
err := testRunPruneOutput(gopts, opts)
err := testRunPruneOutput(t, gopts, opts)
rtest.Assert(t, err != nil, "expected non nil error")
}
func testRunPruneOutput(gopts GlobalOptions, opts PruneOptions) error {
func testRunPruneOutput(t testing.TB, gopts GlobalOptions, opts PruneOptions) error {
oldHook := gopts.backendTestHook
gopts.backendTestHook = func(r backend.Backend) (backend.Backend, error) { return newListOnceBackend(r), nil }
defer func() {
gopts.backendTestHook = oldHook
}()
return withTermStatus(gopts, func(ctx context.Context, term ui.Terminal) error {
return runPrune(context.TODO(), opts, gopts, term)
return withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
return runPrune(context.TODO(), opts, gopts, gopts.term)
})
}
@ -90,7 +89,7 @@ func createPrunableRepo(t *testing.T, env *testEnvironment) {
}
func testRunForgetJSON(t testing.TB, gopts GlobalOptions, args ...string) {
buf, err := withCaptureStdout(gopts, func(gopts GlobalOptions) error {
buf, err := withCaptureStdout(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
gopts.JSON = true
opts := ForgetOptions{
DryRun: true,
@ -99,9 +98,7 @@ func testRunForgetJSON(t testing.TB, gopts GlobalOptions, args ...string) {
pruneOpts := PruneOptions{
MaxUnused: "5%",
}
return withTermStatus(gopts, func(ctx context.Context, term ui.Terminal) error {
return runForget(context.TODO(), opts, pruneOpts, gopts, term, args)
})
return runForget(context.TODO(), opts, pruneOpts, gopts, gopts.term, args)
})
rtest.OK(t, err)
@ -122,8 +119,8 @@ 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 ui.Terminal) error {
_, err := runCheck(context.TODO(), checkOpts, env.gopts, nil, term)
rtest.OK(t, withTermStatus(t, env.gopts, func(ctx context.Context, gopts GlobalOptions) error {
_, err := runCheck(context.TODO(), checkOpts, gopts, nil, gopts.term)
return err
}))
}
@ -158,8 +155,8 @@ 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 ui.Terminal) error {
return runPrune(context.TODO(), pruneDefaultOptions, env.gopts, term)
rtest.Equals(t, repository.ErrPacksMissing, withTermStatus(t, env.gopts, func(ctx context.Context, gopts GlobalOptions) error {
return runPrune(context.TODO(), pruneDefaultOptions, gopts, gopts.term)
}), "prune should have reported index not complete error")
}
@ -231,8 +228,8 @@ 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 ui.Terminal) error {
_, err := runCheck(context.TODO(), optionsCheck, env.gopts, nil, term)
rtest.Assert(t, withTermStatus(t, env.gopts, func(ctx context.Context, gopts GlobalOptions) error {
_, err := runCheck(context.TODO(), optionsCheck, gopts, nil, gopts.term)
return err
}) != nil,
"check should have reported an error")
@ -242,8 +239,8 @@ 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 ui.Terminal) error {
return runPrune(context.TODO(), optionsPrune, env.gopts, term)
rtest.Assert(t, withTermStatus(t, env.gopts, func(ctx context.Context, gopts GlobalOptions) error {
return runPrune(context.TODO(), optionsPrune, gopts, gopts.term)
}) != nil,
"prune should have reported an error")
}

View file

@ -14,7 +14,7 @@ import (
"golang.org/x/sync/errgroup"
)
func newRecoverCommand() *cobra.Command {
func newRecoverCommand(globalOptions *GlobalOptions) *cobra.Command {
cmd := &cobra.Command{
Use: "recover [flags]",
Short: "Recover data from the repository not referenced by snapshots",
@ -35,9 +35,7 @@ Exit status is 12 if the password is incorrect.
GroupID: cmdGroupDefault,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, _ []string) error {
term, cancel := setupTermstatus()
defer cancel()
return runRecover(cmd.Context(), globalOptions, term)
return runRecover(cmd.Context(), *globalOptions, globalOptions.term)
},
}
return cmd
@ -49,7 +47,7 @@ func runRecover(ctx context.Context, gopts GlobalOptions, term ui.Terminal) erro
return err
}
printer := newTerminalProgressPrinter(false, gopts.verbosity, term)
printer := ui.NewProgressPrinter(false, gopts.verbosity, term)
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, false, printer)
if err != nil {
return err
@ -68,8 +66,7 @@ func runRecover(ctx context.Context, gopts GlobalOptions, term ui.Terminal) erro
}
printer.P("load index files\n")
bar := newIndexTerminalProgress(printer)
if err = repo.LoadIndex(ctx, bar); err != nil {
if err = repo.LoadIndex(ctx, printer); err != nil {
return err
}
@ -87,7 +84,7 @@ func runRecover(ctx context.Context, gopts GlobalOptions, term ui.Terminal) erro
}
printer.P("load %d trees\n", len(trees))
bar = printer.NewCounter("trees loaded")
bar := printer.NewCounter("trees loaded")
bar.SetMax(uint64(len(trees)))
for id := range trees {
tree, err := restic.LoadTree(ctx, repo, id)

View file

@ -5,12 +5,11 @@ import (
"testing"
rtest "github.com/restic/restic/internal/test"
"github.com/restic/restic/internal/ui"
)
func testRunRecover(t testing.TB, gopts GlobalOptions) {
rtest.OK(t, withTermStatus(gopts, func(ctx context.Context, term ui.Terminal) error {
return runRecover(context.TODO(), gopts, term)
rtest.OK(t, withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
return runRecover(context.TODO(), gopts, gopts.term)
}))
}
@ -33,7 +32,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, 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)
rtest.OK(t, withTermStatus(t, env.gopts, func(ctx context.Context, gopts GlobalOptions) error {
return runCat(context.TODO(), gopts, []string{"tree", ids[0].String() + ":" + sn.Tree.Str()}, gopts.term)
}))
}

View file

@ -4,7 +4,7 @@ import (
"github.com/spf13/cobra"
)
func newRepairCommand() *cobra.Command {
func newRepairCommand(globalOptions *GlobalOptions) *cobra.Command {
cmd := &cobra.Command{
Use: "repair",
Short: "Repair the repository",
@ -13,9 +13,9 @@ func newRepairCommand() *cobra.Command {
}
cmd.AddCommand(
newRepairIndexCommand(),
newRepairPacksCommand(),
newRepairSnapshotsCommand(),
newRepairIndexCommand(globalOptions),
newRepairPacksCommand(globalOptions),
newRepairSnapshotsCommand(globalOptions),
)
return cmd
}

View file

@ -9,7 +9,7 @@ import (
"github.com/spf13/pflag"
)
func newRepairIndexCommand() *cobra.Command {
func newRepairIndexCommand(globalOptions *GlobalOptions) *cobra.Command {
var opts RepairIndexOptions
cmd := &cobra.Command{
@ -30,9 +30,7 @@ Exit status is 12 if the password is incorrect.
`,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, _ []string) error {
term, cancel := setupTermstatus()
defer cancel()
return runRebuildIndex(cmd.Context(), opts, globalOptions, term)
return runRebuildIndex(cmd.Context(), opts, *globalOptions, globalOptions.term)
},
}
@ -49,10 +47,10 @@ func (opts *RepairIndexOptions) AddFlags(f *pflag.FlagSet) {
f.BoolVar(&opts.ReadAllPacks, "read-all-packs", false, "read all pack files to generate new index from scratch")
}
func newRebuildIndexCommand() *cobra.Command {
func newRebuildIndexCommand(globalOptions *GlobalOptions) *cobra.Command {
var opts RepairIndexOptions
replacement := newRepairIndexCommand()
replacement := newRepairIndexCommand(globalOptions)
cmd := &cobra.Command{
Use: "rebuild-index [flags]",
Short: replacement.Short,
@ -62,9 +60,7 @@ func newRebuildIndexCommand() *cobra.Command {
// must create a new instance of the run function as it captures opts
// by reference
RunE: func(cmd *cobra.Command, _ []string) error {
term, cancel := setupTermstatus()
defer cancel()
return runRebuildIndex(cmd.Context(), opts, globalOptions, term)
return runRebuildIndex(cmd.Context(), opts, *globalOptions, globalOptions.term)
},
}
@ -73,7 +69,7 @@ func newRebuildIndexCommand() *cobra.Command {
}
func runRebuildIndex(ctx context.Context, opts RepairIndexOptions, gopts GlobalOptions, term ui.Terminal) error {
printer := newTerminalProgressPrinter(false, gopts.verbosity, term)
printer := ui.NewProgressPrinter(false, gopts.verbosity, term)
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, false, printer)
if err != nil {

View file

@ -13,15 +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"
)
func testRunRebuildIndex(t testing.TB, gopts GlobalOptions) {
rtest.OK(t, withRestoreGlobalOptions(func() error {
return withTermStatus(gopts, func(ctx context.Context, term ui.Terminal) error {
globalOptions.stdout = io.Discard
return runRebuildIndex(context.TODO(), RepairIndexOptions{}, gopts, term)
})
rtest.OK(t, withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
gopts.Quiet = true
return runRebuildIndex(context.TODO(), RepairIndexOptions{}, gopts, gopts.term)
}))
}
@ -32,7 +29,7 @@ func testRebuildIndex(t *testing.T, backendTestHook backendWrapper) {
datafile := filepath.Join("..", "..", "internal", "checker", "testdata", "duplicate-packs-in-index-test-repo.tar.gz")
rtest.SetupTarTestFixture(t, env.base, datafile)
out, err := testRunCheckOutput(env.gopts, false)
out, err := testRunCheckOutput(t, env.gopts, false)
if !strings.Contains(out, "contained in several indexes") {
t.Fatalf("did not find checker hint for packs in several indexes")
}
@ -49,7 +46,7 @@ func testRebuildIndex(t *testing.T, backendTestHook backendWrapper) {
testRunRebuildIndex(t, env.gopts)
env.gopts.backendTestHook = nil
out, err = testRunCheckOutput(env.gopts, false)
out, err = testRunCheckOutput(t, env.gopts, false)
if len(out) != 0 {
t.Fatalf("expected no output from the checker, got: %v", out)
}
@ -128,14 +125,12 @@ func TestRebuildIndexFailsOnAppendOnly(t *testing.T) {
datafile := filepath.Join("..", "..", "internal", "checker", "testdata", "duplicate-packs-in-index-test-repo.tar.gz")
rtest.SetupTarTestFixture(t, env.base, datafile)
err := withRestoreGlobalOptions(func() error {
env.gopts.backendTestHook = func(r backend.Backend) (backend.Backend, error) {
return &appendOnlyBackend{r}, nil
}
return withTermStatus(env.gopts, func(ctx context.Context, term ui.Terminal) error {
globalOptions.stdout = io.Discard
return runRebuildIndex(context.TODO(), RepairIndexOptions{}, env.gopts, term)
})
env.gopts.backendTestHook = func(r backend.Backend) (backend.Backend, error) {
return &appendOnlyBackend{r}, nil
}
err := withTermStatus(t, env.gopts, func(ctx context.Context, gopts GlobalOptions) error {
gopts.Quiet = true
return runRebuildIndex(context.TODO(), RepairIndexOptions{}, gopts, gopts.term)
})
if err == nil {

View file

@ -13,7 +13,7 @@ import (
"github.com/spf13/cobra"
)
func newRepairPacksCommand() *cobra.Command {
func newRepairPacksCommand(globalOptions *GlobalOptions) *cobra.Command {
cmd := &cobra.Command{
Use: "packs [packIDs...]",
Short: "Salvage damaged pack files",
@ -32,9 +32,7 @@ Exit status is 12 if the password is incorrect.
`,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
term, cancel := setupTermstatus()
defer cancel()
return runRepairPacks(cmd.Context(), globalOptions, term, args)
return runRepairPacks(cmd.Context(), *globalOptions, globalOptions.term, args)
},
}
return cmd
@ -53,7 +51,7 @@ func runRepairPacks(ctx context.Context, gopts GlobalOptions, term ui.Terminal,
return errors.Fatal("no ids specified")
}
printer := newTerminalProgressPrinter(false, gopts.verbosity, term)
printer := ui.NewProgressPrinter(false, gopts.verbosity, term)
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, false, printer)
if err != nil {
@ -61,8 +59,7 @@ func runRepairPacks(ctx context.Context, gopts GlobalOptions, term ui.Terminal,
}
defer unlock()
bar := newIndexTerminalProgress(printer)
err = repo.LoadIndex(ctx, bar)
err = repo.LoadIndex(ctx, printer)
if err != nil {
return errors.Fatalf("%s", err)
}

View file

@ -12,7 +12,7 @@ import (
"github.com/spf13/pflag"
)
func newRepairSnapshotsCommand() *cobra.Command {
func newRepairSnapshotsCommand(globalOptions *GlobalOptions) *cobra.Command {
var opts RepairOptions
cmd := &cobra.Command{
@ -50,9 +50,7 @@ Exit status is 12 if the password is incorrect.
`,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
term, cancel := setupTermstatus()
defer cancel()
return runRepairSnapshots(cmd.Context(), globalOptions, opts, args, term)
return runRepairSnapshots(cmd.Context(), *globalOptions, opts, args, globalOptions.term)
},
}
@ -76,7 +74,7 @@ func (opts *RepairOptions) AddFlags(f *pflag.FlagSet) {
}
func runRepairSnapshots(ctx context.Context, gopts GlobalOptions, opts RepairOptions, args []string, term ui.Terminal) error {
printer := newTerminalProgressPrinter(false, gopts.verbosity, term)
printer := ui.NewProgressPrinter(false, gopts.verbosity, term)
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, opts.DryRun, printer)
if err != nil {
@ -89,8 +87,7 @@ func runRepairSnapshots(ctx context.Context, gopts GlobalOptions, opts RepairOpt
return err
}
bar := newIndexTerminalProgress(printer)
if err := repo.LoadIndex(ctx, bar); err != nil {
if err := repo.LoadIndex(ctx, printer); err != nil {
return err
}

View file

@ -12,7 +12,6 @@ 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) {
@ -20,8 +19,8 @@ func testRunRepairSnapshot(t testing.TB, gopts GlobalOptions, forget bool) {
Forget: forget,
}
rtest.OK(t, withTermStatus(gopts, func(ctx context.Context, term ui.Terminal) error {
return runRepairSnapshots(context.TODO(), gopts, opts, nil, term)
rtest.OK(t, withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
return runRepairSnapshots(context.TODO(), gopts, opts, nil, gopts.term)
}))
}
@ -67,7 +66,7 @@ func TestRepairSnapshotsWithLostData(t *testing.T) {
// repository must be ok after removing the broken snapshots
testRunForget(t, env.gopts, ForgetOptions{}, snapshotIDs[0].String(), snapshotIDs[1].String())
testListSnapshots(t, env.gopts, 2)
_, err := testRunCheckOutput(env.gopts, false)
_, err := testRunCheckOutput(t, env.gopts, false)
rtest.OK(t, err)
}
@ -96,7 +95,7 @@ func TestRepairSnapshotsWithLostTree(t *testing.T) {
testRunRebuildIndex(t, env.gopts)
testRunRepairSnapshot(t, env.gopts, true)
testListSnapshots(t, env.gopts, 1)
_, err := testRunCheckOutput(env.gopts, false)
_, err := testRunCheckOutput(t, env.gopts, false)
rtest.OK(t, err)
}
@ -119,7 +118,7 @@ func TestRepairSnapshotsWithLostRootTree(t *testing.T) {
testRunRebuildIndex(t, env.gopts)
testRunRepairSnapshot(t, env.gopts, true)
testListSnapshots(t, env.gopts, 0)
_, err := testRunCheckOutput(env.gopts, false)
_, err := testRunCheckOutput(t, env.gopts, false)
rtest.OK(t, err)
}

View file

@ -18,7 +18,7 @@ import (
"github.com/spf13/pflag"
)
func newRestoreCommand() *cobra.Command {
func newRestoreCommand(globalOptions *GlobalOptions) *cobra.Command {
var opts RestoreOptions
cmd := &cobra.Command{
@ -46,9 +46,7 @@ Exit status is 12 if the password is incorrect.
GroupID: cmdGroupDefault,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
term, cancel := setupTermstatus()
defer cancel()
return runRestore(cmd.Context(), opts, globalOptions, term, args)
return runRestore(cmd.Context(), opts, *globalOptions, globalOptions.term, args)
},
}
@ -91,13 +89,19 @@ func (opts *RestoreOptions) AddFlags(f *pflag.FlagSet) {
func runRestore(ctx context.Context, opts RestoreOptions, gopts GlobalOptions,
term ui.Terminal, args []string) error {
msg := newTerminalProgressPrinter(gopts.JSON, gopts.verbosity, term)
excludePatternFns, err := opts.ExcludePatternOptions.CollectPatterns(msg.E)
var printer restoreui.ProgressPrinter
if gopts.JSON {
printer = restoreui.NewJSONProgress(term, gopts.verbosity)
} else {
printer = restoreui.NewTextProgress(term, gopts.verbosity)
}
excludePatternFns, err := opts.ExcludePatternOptions.CollectPatterns(printer.E)
if err != nil {
return err
}
includePatternFns, err := opts.IncludePatternOptions.CollectPatterns(msg.E)
includePatternFns, err := opts.IncludePatternOptions.CollectPatterns(printer.E)
if err != nil {
return err
}
@ -132,7 +136,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, msg)
ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock, printer)
if err != nil {
return err
}
@ -147,8 +151,7 @@ func runRestore(ctx context.Context, opts RestoreOptions, gopts GlobalOptions,
return errors.Fatalf("failed to find snapshot: %v", err)
}
bar := newIndexTerminalProgress(msg)
err = repo.LoadIndex(ctx, bar)
err = repo.LoadIndex(ctx, printer)
if err != nil {
return err
}
@ -158,14 +161,7 @@ func runRestore(ctx context.Context, opts RestoreOptions, gopts GlobalOptions,
return err
}
var printer restoreui.ProgressPrinter
if gopts.JSON {
printer = restoreui.NewJSONProgress(term, gopts.verbosity)
} else {
printer = restoreui.NewTextProgress(term, gopts.verbosity)
}
progress := restoreui.NewProgress(printer, calculateProgressInterval(!gopts.Quiet, gopts.JSON))
progress := restoreui.NewProgress(printer, ui.CalculateProgressInterval(!gopts.Quiet, gopts.JSON, term.CanUpdateStatus()))
res := restorer.NewRestorer(repo, sn, restorer.Options{
DryRun: opts.DryRun,
Sparse: opts.Sparse,
@ -180,13 +176,13 @@ func runRestore(ctx context.Context, opts RestoreOptions, gopts GlobalOptions,
return progress.Error(location, err)
}
res.Warn = func(message string) {
msg.E("Warning: %s\n", message)
printer.E("Warning: %s\n", message)
}
res.Info = func(message string) {
if gopts.JSON {
return
}
msg.P("Info: %s\n", message)
printer.P("Info: %s\n", message)
}
selectExcludeFilter := func(item string, isDir bool) (selectedForRestore bool, childMayBeSelected bool) {
@ -234,13 +230,13 @@ func runRestore(ctx context.Context, opts RestoreOptions, gopts GlobalOptions,
res.SelectFilter = selectIncludeFilter
}
res.XattrSelectFilter, err = getXattrSelectFilter(opts, msg)
res.XattrSelectFilter, err = getXattrSelectFilter(opts, printer)
if err != nil {
return err
}
if !gopts.JSON {
msg.P("restoring %s to %s\n", res.Snapshot(), opts.Target)
printer.P("restoring %s to %s\n", res.Snapshot(), opts.Target)
}
countRestoredFiles, err := res.RestoreTo(ctx, opts.Target)
@ -256,11 +252,11 @@ func runRestore(ctx context.Context, opts RestoreOptions, gopts GlobalOptions,
if opts.Verify {
if !gopts.JSON {
msg.P("verifying files in %s\n", opts.Target)
printer.P("verifying files in %s\n", opts.Target)
}
var count int
t0 := time.Now()
bar := msg.NewCounterTerminalOnly("files verified")
bar := printer.NewCounterTerminalOnly("files verified")
count, err = res.VerifyFiles(ctx, opts.Target, countRestoredFiles, bar)
if err != nil {
return err
@ -270,7 +266,7 @@ func runRestore(ctx context.Context, opts RestoreOptions, gopts GlobalOptions,
}
if !gopts.JSON {
msg.P("finished verifying %d files in %s (took %s)\n", count, opts.Target,
printer.P("finished verifying %d files in %s (took %s)\n", count, opts.Target,
time.Since(t0).Round(time.Millisecond))
}
}

View file

@ -3,7 +3,6 @@ package main
import (
"context"
"fmt"
"io"
"math/rand"
"os"
"path/filepath"
@ -14,11 +13,10 @@ import (
"github.com/restic/restic/internal/restic"
rtest "github.com/restic/restic/internal/test"
"github.com/restic/restic/internal/ui"
)
func testRunRestore(t testing.TB, opts GlobalOptions, dir string, snapshotID string) {
testRunRestoreExcludes(t, opts, dir, snapshotID, nil)
func testRunRestore(t testing.TB, gopts GlobalOptions, dir string, snapshotID string) {
testRunRestoreExcludes(t, gopts, dir, snapshotID, nil)
}
func testRunRestoreExcludes(t testing.TB, gopts GlobalOptions, dir string, snapshotID string, excludes []string) {
@ -27,12 +25,12 @@ func testRunRestoreExcludes(t testing.TB, gopts GlobalOptions, dir string, snaps
}
opts.Excludes = excludes
rtest.OK(t, testRunRestoreAssumeFailure(snapshotID, opts, gopts))
rtest.OK(t, testRunRestoreAssumeFailure(t, snapshotID, opts, gopts))
}
func testRunRestoreAssumeFailure(snapshotID string, opts RestoreOptions, gopts GlobalOptions) error {
return withTermStatus(gopts, func(ctx context.Context, term ui.Terminal) error {
return runRestore(ctx, opts, gopts, term, []string{snapshotID})
func testRunRestoreAssumeFailure(t testing.TB, snapshotID string, opts RestoreOptions, gopts GlobalOptions) error {
return withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
return runRestore(ctx, opts, gopts, gopts.term, []string{snapshotID})
})
}
@ -45,7 +43,7 @@ func testRunRestoreLatest(t testing.TB, gopts GlobalOptions, dir string, paths [
},
}
rtest.OK(t, testRunRestoreAssumeFailure("latest", opts, gopts))
rtest.OK(t, testRunRestoreAssumeFailure(t, "latest", opts, gopts))
}
func testRunRestoreIncludes(t testing.TB, gopts GlobalOptions, dir string, snapshotID restic.ID, includes []string) {
@ -54,7 +52,7 @@ func testRunRestoreIncludes(t testing.TB, gopts GlobalOptions, dir string, snaps
}
opts.Includes = includes
rtest.OK(t, testRunRestoreAssumeFailure(snapshotID.String(), opts, gopts))
rtest.OK(t, testRunRestoreAssumeFailure(t, snapshotID.String(), opts, gopts))
}
func testRunRestoreIncludesFromFile(t testing.TB, gopts GlobalOptions, dir string, snapshotID restic.ID, includesFile string) {
@ -63,7 +61,7 @@ func testRunRestoreIncludesFromFile(t testing.TB, gopts GlobalOptions, dir strin
}
opts.IncludeFiles = []string{includesFile}
rtest.OK(t, testRunRestoreAssumeFailure(snapshotID.String(), opts, gopts))
rtest.OK(t, testRunRestoreAssumeFailure(t, snapshotID.String(), opts, gopts))
}
func testRunRestoreExcludesFromFile(t testing.TB, gopts GlobalOptions, dir string, snapshotID restic.ID, excludesFile string) {
@ -72,7 +70,7 @@ func testRunRestoreExcludesFromFile(t testing.TB, gopts GlobalOptions, dir strin
}
opts.ExcludeFiles = []string{excludesFile}
rtest.OK(t, testRunRestoreAssumeFailure(snapshotID.String(), opts, gopts))
rtest.OK(t, testRunRestoreAssumeFailure(t, snapshotID.String(), opts, gopts))
}
func TestRestoreMustFailWhenUsingBothIncludesAndExcludes(t *testing.T) {
@ -93,7 +91,7 @@ func TestRestoreMustFailWhenUsingBothIncludesAndExcludes(t *testing.T) {
restoreOpts.Includes = includePatterns
restoreOpts.Excludes = excludePatterns
err := testRunRestoreAssumeFailure("latest", restoreOpts, env.gopts)
err := testRunRestoreAssumeFailure(t, "latest", restoreOpts, env.gopts)
rtest.Assert(t, err != nil && strings.Contains(err.Error(), "exclude and include patterns are mutually exclusive"),
"expected: %s error, got %v", "exclude and include patterns are mutually exclusive", err)
}
@ -257,7 +255,7 @@ func TestRestore(t *testing.T) {
restoredir := filepath.Join(env.base, "restore")
testRunRestoreLatest(t, env.gopts, restoredir, nil, nil)
diff := directoriesContentsDiff(env.testdata, filepath.Join(restoredir, filepath.Base(env.testdata)))
diff := directoriesContentsDiff(t, env.testdata, filepath.Join(restoredir, filepath.Base(env.testdata)))
rtest.Assert(t, diff == "", "directories are not equal %v", diff)
}
@ -337,11 +335,7 @@ func TestRestoreWithPermissionFailure(t *testing.T) {
snapshots := testListSnapshots(t, env.gopts, 1)
_ = withRestoreGlobalOptions(func() error {
globalOptions.stderr = io.Discard
testRunRestore(t, env.gopts, filepath.Join(env.base, "restore"), snapshots[0].String())
return nil
})
testRunRestore(t, env.gopts, filepath.Join(env.base, "restore"), snapshots[0].String())
// make sure that all files have been restored, regardless of any
// permission errors

View file

@ -18,7 +18,7 @@ import (
"github.com/restic/restic/internal/walker"
)
func newRewriteCommand() *cobra.Command {
func newRewriteCommand(globalOptions *GlobalOptions) *cobra.Command {
var opts RewriteOptions
cmd := &cobra.Command{
@ -60,9 +60,7 @@ Exit status is 12 if the password is incorrect.
GroupID: cmdGroupDefault,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
term, cancel := setupTermstatus()
defer cancel()
return runRewrite(cmd.Context(), opts, globalOptions, args, term)
return runRewrite(cmd.Context(), opts, *globalOptions, args, globalOptions.term)
},
}
@ -296,7 +294,7 @@ func runRewrite(ctx context.Context, opts RewriteOptions, gopts GlobalOptions, a
return errors.Fatal("Nothing to do: no excludes provided and no new metadata provided")
}
printer := newTerminalProgressPrinter(false, gopts.verbosity, term)
printer := ui.NewProgressPrinter(false, gopts.verbosity, term)
var (
repo *repository.Repository
@ -320,8 +318,7 @@ func runRewrite(ctx context.Context, opts RewriteOptions, gopts GlobalOptions, a
return err
}
bar := newIndexTerminalProgress(printer)
if err = repo.LoadIndex(ctx, bar); err != nil {
if err = repo.LoadIndex(ctx, printer); err != nil {
return err
}

View file

@ -20,8 +20,8 @@ func testRunRewriteExclude(t testing.TB, gopts GlobalOptions, excludes []string,
Metadata: metadata,
}
rtest.OK(t, withTermStatus(gopts, func(ctx context.Context, term ui.Terminal) error {
return runRewrite(context.TODO(), opts, gopts, nil, term)
rtest.OK(t, withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
return runRewrite(context.TODO(), opts, gopts, nil, gopts.term)
}))
}
@ -41,9 +41,9 @@ func getSnapshot(t testing.TB, snapshotID restic.ID, env *testEnvironment) *rest
t.Helper()
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)
err := withTermStatus(t, env.gopts, func(ctx context.Context, gopts GlobalOptions) error {
printer := ui.NewProgressPrinter(gopts.JSON, gopts.verbosity, gopts.term)
ctx, repo, unlock, err := openWithReadLock(ctx, gopts, false, printer)
rtest.OK(t, err)
defer unlock()
@ -117,9 +117,9 @@ func testRewriteMetadata(t *testing.T, metadata snapshotMetadataArgs) {
testRunRewriteExclude(t, env.gopts, []string{}, true, metadata)
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)
err := withTermStatus(t, env.gopts, func(ctx context.Context, gopts GlobalOptions) error {
printer := ui.NewProgressPrinter(gopts.JSON, gopts.verbosity, gopts.term)
ctx, repo, unlock, err := openWithReadLock(ctx, gopts, false, printer)
rtest.OK(t, err)
defer unlock()
@ -157,17 +157,17 @@ func TestRewriteSnaphotSummary(t *testing.T) {
defer cleanup()
createBasicRewriteRepo(t, env)
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)
rtest.OK(t, withTermStatus(t, env.gopts, func(ctx context.Context, gopts GlobalOptions) error {
return runRewrite(context.TODO(), RewriteOptions{SnapshotSummary: true}, gopts, []string{}, gopts.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
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)
err := withTermStatus(t, env.gopts, func(ctx context.Context, gopts GlobalOptions) error {
printer := ui.NewProgressPrinter(gopts.JSON, gopts.verbosity, gopts.term)
_, repo, unlock, err := openWithExclusiveLock(ctx, gopts, false, printer)
rtest.OK(t, err)
defer unlock()
@ -182,8 +182,8 @@ func TestRewriteSnaphotSummary(t *testing.T) {
rtest.OK(t, err)
// rewrite snapshot and lookup ID of new snapshot
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)
rtest.OK(t, withTermStatus(t, env.gopts, func(ctx context.Context, gopts GlobalOptions) error {
return runRewrite(context.TODO(), RewriteOptions{SnapshotSummary: true}, gopts, []string{}, gopts.term)
}))
newSnapshots := testListSnapshots(t, env.gopts, 2)
newSnapshot := restic.NewIDSet(newSnapshots...).Sub(restic.NewIDSet(snapshots...)).List()[0]

View file

@ -14,13 +14,13 @@ import (
"github.com/spf13/pflag"
)
func registerSelfUpdateCommand(cmd *cobra.Command) {
func registerSelfUpdateCommand(cmd *cobra.Command, globalOptions *GlobalOptions) {
cmd.AddCommand(
newSelfUpdateCommand(),
newSelfUpdateCommand(globalOptions),
)
}
func newSelfUpdateCommand() *cobra.Command {
func newSelfUpdateCommand(globalOptions *GlobalOptions) *cobra.Command {
var opts SelfUpdateOptions
cmd := &cobra.Command{
@ -43,9 +43,7 @@ Exit status is 12 if the password is incorrect.
`,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
term, cancel := setupTermstatus()
defer cancel()
return runSelfUpdate(cmd.Context(), opts, globalOptions, args, term)
return runSelfUpdate(cmd.Context(), opts, *globalOptions, args, globalOptions.term)
},
}
@ -88,7 +86,7 @@ func runSelfUpdate(ctx context.Context, opts SelfUpdateOptions, gopts GlobalOpti
}
}
printer := newTerminalProgressPrinter(false, gopts.verbosity, term)
printer := ui.NewProgressPrinter(false, gopts.verbosity, term)
printer.P("writing restic to %v", opts.Output)
v, err := selfupdate.DownloadLatestStableRelease(ctx, opts.Output, version, printer.P)

View file

@ -4,6 +4,6 @@ package main
import "github.com/spf13/cobra"
func registerSelfUpdateCommand(_ *cobra.Command) {
func registerSelfUpdateCommand(_ *cobra.Command, _ *GlobalOptions) {
// No commands to register in non-selfupdate mode
}

View file

@ -15,7 +15,7 @@ import (
"github.com/spf13/pflag"
)
func newSnapshotsCommand() *cobra.Command {
func newSnapshotsCommand(globalOptions *GlobalOptions) *cobra.Command {
var opts SnapshotOptions
cmd := &cobra.Command{
@ -36,9 +36,7 @@ Exit status is 12 if the password is incorrect.
GroupID: cmdGroupDefault,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
term, cancel := setupTermstatus()
defer cancel()
return runSnapshots(cmd.Context(), opts, globalOptions, args, term)
return runSnapshots(cmd.Context(), opts, *globalOptions, args, globalOptions.term)
},
}
@ -69,7 +67,7 @@ func (opts *SnapshotOptions) AddFlags(f *pflag.FlagSet) {
}
func runSnapshots(ctx context.Context, opts SnapshotOptions, gopts GlobalOptions, args []string, term ui.Terminal) error {
printer := newTerminalProgressPrinter(gopts.JSON, gopts.verbosity, term)
printer := ui.NewProgressPrinter(gopts.JSON, gopts.verbosity, term)
ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock, printer)
if err != nil {
return err
@ -105,7 +103,7 @@ func runSnapshots(ctx context.Context, opts SnapshotOptions, gopts GlobalOptions
}
if gopts.JSON {
err := printSnapshotGroupJSON(globalOptions.stdout, snapshotGroups, grouped)
err := printSnapshotGroupJSON(gopts.term.OutputWriter(), snapshotGroups, grouped)
if err != nil {
printer.E("error printing snapshots: %v", err)
}
@ -118,12 +116,12 @@ func runSnapshots(ctx context.Context, opts SnapshotOptions, gopts GlobalOptions
}
if grouped {
err := PrintSnapshotGroupHeader(globalOptions.stdout, k)
err := PrintSnapshotGroupHeader(gopts.term.OutputWriter(), k)
if err != nil {
return err
}
}
err := PrintSnapshots(globalOptions.stdout, list, nil, opts.Compact)
err := PrintSnapshots(gopts.term.OutputWriter(), list, nil, opts.Compact)
if err != nil {
return err
}
@ -301,9 +299,7 @@ func PrintSnapshotGroupHeader(stdout io.Writer, groupKeyJSON string) error {
}
// Info
if _, err := fmt.Fprintf(stdout, "snapshots"); err != nil {
return err
}
header := "snapshots"
var infoStrings []string
if key.Hostname != "" {
infoStrings = append(infoStrings, "host ["+key.Hostname+"]")
@ -315,12 +311,10 @@ func PrintSnapshotGroupHeader(stdout io.Writer, groupKeyJSON string) error {
infoStrings = append(infoStrings, "paths ["+strings.Join(key.Paths, ", ")+"]")
}
if infoStrings != nil {
if _, err := fmt.Fprintf(stdout, " for (%s)", strings.Join(infoStrings, ", ")); err != nil {
return err
}
header += " for (" + strings.Join(infoStrings, ", ") + ")"
}
_, err = fmt.Fprintf(stdout, ":\n")
header += ":\n"
_, err = stdout.Write([]byte(header))
return err
}

View file

@ -7,17 +7,14 @@ 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(gopts, func(gopts GlobalOptions) error {
buf, err := withCaptureStdout(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
gopts.JSON = true
opts := SnapshotOptions{}
return withTermStatus(gopts, func(ctx context.Context, term ui.Terminal) error {
return runSnapshots(ctx, opts, gopts, []string{}, term)
})
return runSnapshots(ctx, opts, gopts, []string{}, gopts.term)
})
rtest.OK(t, err)

View file

@ -22,7 +22,7 @@ import (
"github.com/spf13/pflag"
)
func newStatsCommand() *cobra.Command {
func newStatsCommand(globalOptions *GlobalOptions) *cobra.Command {
var opts StatsOptions
cmd := &cobra.Command{
@ -63,9 +63,7 @@ Exit status is 12 if the password is incorrect.
GroupID: cmdGroupDefault,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
term, cancel := setupTermstatus()
defer cancel()
return runStats(cmd.Context(), opts, globalOptions, args, term)
return runStats(cmd.Context(), opts, *globalOptions, args, globalOptions.term)
},
}
@ -101,7 +99,7 @@ func runStats(ctx context.Context, opts StatsOptions, gopts GlobalOptions, args
return err
}
printer := newTerminalProgressPrinter(gopts.JSON, gopts.verbosity, term)
printer := ui.NewProgressPrinter(gopts.JSON, gopts.verbosity, term)
ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock, printer)
if err != nil {
@ -113,8 +111,7 @@ func runStats(ctx context.Context, opts StatsOptions, gopts GlobalOptions, args
if err != nil {
return err
}
bar := newIndexTerminalProgress(printer)
if err = repo.LoadIndex(ctx, bar); err != nil {
if err = repo.LoadIndex(ctx, printer); err != nil {
return err
}
@ -171,7 +168,7 @@ func runStats(ctx context.Context, opts StatsOptions, gopts GlobalOptions, args
}
if gopts.JSON {
err = json.NewEncoder(globalOptions.stdout).Encode(stats)
err = json.NewEncoder(gopts.term.OutputWriter()).Encode(stats)
if err != nil {
return fmt.Errorf("encoding output: %v", err)
}

View file

@ -13,7 +13,7 @@ import (
"github.com/restic/restic/internal/ui"
)
func newTagCommand() *cobra.Command {
func newTagCommand(globalOptions *GlobalOptions) *cobra.Command {
var opts TagOptions
cmd := &cobra.Command{
@ -39,9 +39,7 @@ Exit status is 12 if the password is incorrect.
GroupID: cmdGroupDefault,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
term, cancel := setupTermstatus()
defer cancel()
return runTag(cmd.Context(), opts, globalOptions, term, args)
return runTag(cmd.Context(), opts, *globalOptions, globalOptions.term, args)
},
}
@ -119,7 +117,7 @@ func changeTags(ctx context.Context, repo *repository.Repository, sn *restic.Sna
}
func runTag(ctx context.Context, opts TagOptions, gopts GlobalOptions, term ui.Terminal, args []string) error {
printer := newTerminalProgressPrinter(gopts.JSON, gopts.verbosity, term)
printer := ui.NewProgressPrinter(gopts.JSON, gopts.verbosity, term)
if len(opts.SetTags) == 0 && len(opts.AddTags) == 0 && len(opts.RemoveTags) == 0 {
return errors.Fatal("nothing to do!")

View file

@ -9,7 +9,9 @@ import (
)
func testRunTag(t testing.TB, opts TagOptions, gopts GlobalOptions) {
rtest.OK(t, runTag(context.TODO(), opts, gopts, nil, []string{}))
rtest.OK(t, withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
return runTag(context.TODO(), opts, gopts, gopts.term, []string{})
}))
}
func TestTag(t *testing.T) {

View file

@ -9,7 +9,7 @@ import (
"github.com/spf13/pflag"
)
func newUnlockCommand() *cobra.Command {
func newUnlockCommand(globalOptions *GlobalOptions) *cobra.Command {
var opts UnlockOptions
cmd := &cobra.Command{
@ -27,9 +27,7 @@ Exit status is 1 if there was any error.
GroupID: cmdGroupDefault,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, _ []string) error {
term, cancel := setupTermstatus()
defer cancel()
return runUnlock(cmd.Context(), opts, globalOptions, term)
return runUnlock(cmd.Context(), opts, *globalOptions, globalOptions.term)
},
}
opts.AddFlags(cmd.Flags())
@ -46,7 +44,7 @@ func (opts *UnlockOptions) AddFlags(f *pflag.FlagSet) {
}
func runUnlock(ctx context.Context, opts UnlockOptions, gopts GlobalOptions, term ui.Terminal) error {
printer := newTerminalProgressPrinter(gopts.JSON, gopts.verbosity, term)
printer := ui.NewProgressPrinter(gopts.JSON, gopts.verbosity, term)
repo, err := OpenRepository(ctx, gopts, printer)
if err != nil {
return err

View file

@ -4,10 +4,11 @@ import (
"encoding/json"
"runtime"
"github.com/restic/restic/internal/ui"
"github.com/spf13/cobra"
)
func newVersionCommand() *cobra.Command {
func newVersionCommand(globalOptions *GlobalOptions) *cobra.Command {
cmd := &cobra.Command{
Use: "version",
Short: "Print version information",
@ -23,9 +24,7 @@ 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)
printer := ui.NewProgressPrinter(globalOptions.JSON, globalOptions.verbosity, globalOptions.term)
if globalOptions.JSON {
type jsonVersion struct {
@ -44,7 +43,7 @@ Exit status is 1 if there was any error.
GoArch: runtime.GOARCH,
}
err := json.NewEncoder(globalOptions.stdout).Encode(jsonS)
err := json.NewEncoder(globalOptions.term.OutputWriter()).Encode(jsonS)
if err != nil {
printer.E("JSON encode failed: %v\n", err)
return

View file

@ -8,7 +8,7 @@ import (
// TestFlags checks for double defined flags, the commands will panic on
// ParseFlags() when a shorthand flag is defined twice.
func TestFlags(t *testing.T) {
for _, cmd := range newRootCommand().Commands() {
for _, cmd := range newRootCommand(&GlobalOptions{}).Commands() {
t.Run(cmd.Name(), func(t *testing.T) {
cmd.Flags().SetOutput(io.Discard)
err := cmd.ParseFlags([]string{"--help"})

View file

@ -1,10 +1,8 @@
package main
import (
"bufio"
"context"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
@ -32,8 +30,8 @@ import (
"github.com/restic/restic/internal/options"
"github.com/restic/restic/internal/repository"
"github.com/restic/restic/internal/restic"
"github.com/restic/restic/internal/terminal"
"github.com/restic/restic/internal/textfile"
"github.com/restic/restic/internal/ui"
"github.com/restic/restic/internal/ui/progress"
"github.com/spf13/pflag"
@ -75,8 +73,7 @@ type GlobalOptions struct {
limiter.Limits
password string
stdout io.Writer
stderr io.Writer
term ui.Terminal
backends *location.Registry
backendTestHook, backendInnerTestHook backendWrapper
@ -177,12 +174,6 @@ func (opts *GlobalOptions) PreRun(needsPassword bool) error {
return nil
}
var globalOptions = GlobalOptions{
stdout: os.Stdout,
stderr: os.Stderr,
backends: collectBackends(),
}
func collectBackends() *location.Registry {
backends := location.NewRegistry()
backends.Register(azure.NewFactory())
@ -236,45 +227,24 @@ func loadPasswordFromFile(pwdFile string) (string, error) {
return strings.TrimSpace(string(s)), errors.Wrap(err, "Readfile")
}
// readPassword reads the password from the given reader directly.
func readPassword(in io.Reader) (password string, err error) {
sc := bufio.NewScanner(in)
sc.Scan()
return sc.Text(), errors.WithStack(sc.Err())
}
// 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, printer progress.Printer) (string, error) {
if opts.InsecureNoPassword {
if opts.password != "" {
func ReadPassword(ctx context.Context, gopts GlobalOptions, prompt string) (string, error) {
if gopts.InsecureNoPassword {
if gopts.password != "" {
return "", errors.Fatal("--insecure-no-password must not be specified together with providing a password via a cli option or environment variable")
}
return "", nil
}
if opts.password != "" {
return opts.password, nil
}
var (
password string
err error
)
if terminal.StdinIsTerminal() {
password, err = terminal.ReadPassword(ctx, os.Stdin, os.Stderr, prompt)
} else {
if terminal.StdoutIsTerminal() {
printer.P("reading repository password from stdin")
}
password, err = readPassword(os.Stdin)
if gopts.password != "" {
return gopts.password, nil
}
password, err := gopts.term.ReadPassword(ctx, prompt)
if err != nil {
return "", errors.Wrap(err, "unable to read password")
return "", fmt.Errorf("unable to read password: %w", err)
}
if len(password) == 0 {
@ -287,13 +257,13 @@ func ReadPassword(ctx context.Context, opts GlobalOptions, prompt string, printe
// 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, printer progress.Printer) (string, error) {
pw1, err := ReadPassword(ctx, gopts, prompt1, printer)
func ReadPasswordTwice(ctx context.Context, gopts GlobalOptions, prompt1, prompt2 string) (string, error) {
pw1, err := ReadPassword(ctx, gopts, prompt1)
if err != nil {
return "", err
}
if terminal.StdinIsTerminal() {
pw2, err := ReadPassword(ctx, gopts, prompt2, printer)
if gopts.term.InputIsTerminal() {
pw2, err := ReadPassword(ctx, gopts, prompt2)
if err != nil {
return "", err
}
@ -306,20 +276,20 @@ func ReadPasswordTwice(ctx context.Context, gopts GlobalOptions, prompt1, prompt
return pw1, nil
}
func ReadRepo(opts GlobalOptions) (string, error) {
if opts.Repo == "" && opts.RepositoryFile == "" {
func ReadRepo(gopts GlobalOptions) (string, error) {
if gopts.Repo == "" && gopts.RepositoryFile == "" {
return "", errors.Fatal("Please specify repository location (-r or --repository-file)")
}
repo := opts.Repo
if opts.RepositoryFile != "" {
repo := gopts.Repo
if gopts.RepositoryFile != "" {
if repo != "" {
return "", errors.Fatal("Options -r and --repository-file are mutually exclusive, please specify only one")
}
s, err := textfile.Read(opts.RepositoryFile)
s, err := textfile.Read(gopts.RepositoryFile)
if errors.Is(err, os.ErrNotExist) {
return "", errors.Fatalf("%s does not exist", opts.RepositoryFile)
return "", errors.Fatalf("%s does not exist", gopts.RepositoryFile)
}
if err != nil {
return "", err
@ -334,47 +304,47 @@ func ReadRepo(opts GlobalOptions) (string, error) {
const maxKeys = 20
// OpenRepository reads the password and opens the repository.
func OpenRepository(ctx context.Context, opts GlobalOptions, printer progress.Printer) (*repository.Repository, error) {
repo, err := ReadRepo(opts)
func OpenRepository(ctx context.Context, gopts GlobalOptions, printer progress.Printer) (*repository.Repository, error) {
repo, err := ReadRepo(gopts)
if err != nil {
return nil, err
}
be, err := open(ctx, repo, opts, opts.extended, printer)
be, err := open(ctx, repo, gopts, gopts.extended, printer)
if err != nil {
return nil, err
}
s, err := repository.New(be, repository.Options{
Compression: opts.Compression,
PackSize: opts.PackSize * 1024 * 1024,
NoExtraVerify: opts.NoExtraVerify,
Compression: gopts.Compression,
PackSize: gopts.PackSize * 1024 * 1024,
NoExtraVerify: gopts.NoExtraVerify,
})
if err != nil {
return nil, errors.Fatalf("%s", err)
}
passwordTriesLeft := 1
if terminal.StdinIsTerminal() && opts.password == "" && !opts.InsecureNoPassword {
if gopts.term.InputIsTerminal() && gopts.password == "" && !gopts.InsecureNoPassword {
passwordTriesLeft = 3
}
for ; passwordTriesLeft > 0; passwordTriesLeft-- {
opts.password, err = ReadPassword(ctx, opts, "enter password for repository: ", printer)
gopts.password, err = ReadPassword(ctx, gopts, "enter password for repository: ")
if ctx.Err() != nil {
return nil, ctx.Err()
}
if err != nil && passwordTriesLeft > 1 {
opts.password = ""
gopts.password = ""
printer.E("%s. Try again", err)
}
if err != nil {
continue
}
err = s.SearchKey(ctx, opts.password, maxKeys, opts.KeyHint)
err = s.SearchKey(ctx, gopts.password, maxKeys, gopts.KeyHint)
if err != nil && passwordTriesLeft > 1 {
opts.password = ""
gopts.password = ""
printer.E("%s. Try again", err)
}
}
@ -385,36 +355,32 @@ func OpenRepository(ctx context.Context, opts GlobalOptions, printer progress.Pr
return nil, errors.Fatalf("%s", err)
}
if terminal.StdoutIsTerminal() && !opts.JSON {
id := s.Config().ID
if len(id) > 8 {
id = id[:8]
}
if !opts.JSON {
extra := ""
if s.Config().Version >= 2 {
extra = ", compression level " + opts.Compression.String()
}
printer.P("repository %v opened (version %v%s)", id, s.Config().Version, extra)
}
id := s.Config().ID
if len(id) > 8 {
id = id[:8]
}
extra := ""
if s.Config().Version >= 2 {
extra = ", compression level " + gopts.Compression.String()
}
printer.PT("repository %v opened (version %v%s)", id, s.Config().Version, extra)
if opts.NoCache {
if gopts.NoCache {
return s, nil
}
c, err := cache.New(s.Config().ID, opts.CacheDir)
c, err := cache.New(s.Config().ID, gopts.CacheDir)
if err != nil {
printer.E("unable to open cache: %v", err)
return s, nil
}
if c.Created && !opts.JSON && terminal.StdoutIsTerminal() {
printer.P("created new cache in %v", c.Base)
if c.Created {
printer.PT("created new cache in %v", c.Base)
}
// start using the cache
s.UseCache(c)
s.UseCache(c, printer.E)
oldCacheDirs, err := cache.Old(c.Base)
if err != nil {
@ -427,10 +393,8 @@ func OpenRepository(ctx context.Context, opts GlobalOptions, printer progress.Pr
}
// cleanup old cache dirs if instructed to do so
if opts.CleanupCache {
if terminal.StdoutIsTerminal() && !opts.JSON {
printer.P("removing %d old cache dirs from %v", len(oldCacheDirs), c.Base)
}
if gopts.CleanupCache {
printer.PT("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)
@ -439,10 +403,8 @@ func OpenRepository(ctx context.Context, opts GlobalOptions, printer progress.Pr
}
}
} else {
if terminal.StdoutIsTerminal() {
printer.P("found %d old cache directories in %v, run `restic cache --cleanup` to remove them",
len(oldCacheDirs), c.Base)
}
printer.PT("found %d old cache directories in %v, run `restic cache --cleanup` to remove them",
len(oldCacheDirs), c.Base)
}
return s, nil
@ -476,7 +438,7 @@ func innerOpen(ctx context.Context, s string, gopts GlobalOptions, opts options.
return nil, err
}
rt, err := backend.Transport(globalOptions.TransportOptions)
rt, err := backend.Transport(gopts.TransportOptions)
if err != nil {
return nil, errors.Fatalf("%s", err)
}
@ -492,9 +454,9 @@ func innerOpen(ctx context.Context, s string, gopts GlobalOptions, opts options.
var be backend.Backend
if create {
be, err = factory.Create(ctx, cfg, rt, lim)
be, err = factory.Create(ctx, cfg, rt, lim, printer.E)
} else {
be, err = factory.Open(ctx, cfg, rt, lim)
be, err = factory.Open(ctx, cfg, rt, lim, printer.E)
}
if errors.Is(err, backend.ErrNoRepository) {

View file

@ -5,9 +5,9 @@ package main
import (
"fmt"
"io"
"net/http"
_ "net/http/pprof"
"os"
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/repository"
@ -17,7 +17,7 @@ import (
"github.com/pkg/profile"
)
func registerProfiling(cmd *cobra.Command) {
func registerProfiling(cmd *cobra.Command, stderr io.Writer) {
var profiler profiler
origPreRun := cmd.PersistentPreRunE
@ -27,7 +27,7 @@ func registerProfiling(cmd *cobra.Command) {
return err
}
}
return profiler.Start(profiler.opts)
return profiler.Start(profiler.opts, stderr)
}
// Once https://github.com/spf13/cobra/issues/1893 is fixed,
@ -65,19 +65,21 @@ func (opts *ProfileOptions) AddFlags(f *pflag.FlagSet) {
f.BoolVar(&opts.insecure, "insecure-kdf", false, "use insecure KDF settings")
}
type fakeTestingTB struct{}
func (fakeTestingTB) Logf(msg string, args ...interface{}) {
fmt.Fprintf(os.Stderr, msg, args...)
type fakeTestingTB struct {
stderr io.Writer
}
func (p *profiler) Start(profileOpts ProfileOptions) error {
func (t fakeTestingTB) Logf(msg string, args ...interface{}) {
fmt.Fprintf(t.stderr, msg, args...)
}
func (p *profiler) Start(profileOpts ProfileOptions, stderr io.Writer) error {
if profileOpts.listen != "" {
fmt.Fprintf(os.Stderr, "running profile HTTP server on %v\n", profileOpts.listen)
fmt.Fprintf(stderr, "running profile HTTP server on %v\n", profileOpts.listen)
go func() {
err := http.ListenAndServe(profileOpts.listen, nil)
if err != nil {
fmt.Fprintf(os.Stderr, "profile HTTP server listen failed: %v\n", err)
fmt.Fprintf(stderr, "profile HTTP server listen failed: %v\n", err)
}
}()
}
@ -111,7 +113,7 @@ func (p *profiler) Start(profileOpts ProfileOptions) error {
}
if profileOpts.insecure {
repository.TestUseLowSecurityKDFParameters(fakeTestingTB{})
repository.TestUseLowSecurityKDFParameters(fakeTestingTB{stderr})
}
return nil

View file

@ -3,8 +3,12 @@
package main
import "github.com/spf13/cobra"
import (
"io"
func registerProfiling(_ *cobra.Command) {
"github.com/spf13/cobra"
)
func registerProfiling(_ *cobra.Command, _ io.Writer) {
// No profiling in release mode
}

View file

@ -7,28 +7,16 @@ import (
"strings"
"testing"
"github.com/restic/restic/internal/errors"
rtest "github.com/restic/restic/internal/test"
"github.com/restic/restic/internal/ui/progress"
)
type errorReader struct{ err error }
func (r *errorReader) Read([]byte) (int, error) { return 0, r.err }
func TestReadPassword(t *testing.T) {
want := errors.New("foo")
_, err := readPassword(&errorReader{want})
rtest.Assert(t, errors.Is(err, want), "wrong error %v", err)
}
func TestReadRepo(t *testing.T) {
tempDir := rtest.TempDir(t)
// test --repo option
var opts GlobalOptions
opts.Repo = tempDir
repo, err := ReadRepo(opts)
var gopts GlobalOptions
gopts.Repo = tempDir
repo, err := ReadRepo(gopts)
rtest.OK(t, err)
rtest.Equals(t, tempDir, repo)
@ -37,15 +25,15 @@ func TestReadRepo(t *testing.T) {
err = os.WriteFile(foo, []byte(tempDir+"\n"), 0666)
rtest.OK(t, err)
var opts2 GlobalOptions
opts2.RepositoryFile = foo
repo, err = ReadRepo(opts2)
var gopts2 GlobalOptions
gopts2.RepositoryFile = foo
repo, err = ReadRepo(gopts2)
rtest.OK(t, err)
rtest.Equals(t, tempDir, repo)
var opts3 GlobalOptions
opts3.RepositoryFile = foo + "-invalid"
_, err = ReadRepo(opts3)
var gopts3 GlobalOptions
gopts3.RepositoryFile = foo + "-invalid"
_, err = ReadRepo(gopts3)
if err == nil {
t.Fatal("must not read repository path from invalid file path")
}
@ -53,11 +41,11 @@ func TestReadRepo(t *testing.T) {
func TestReadEmptyPassword(t *testing.T) {
opts := GlobalOptions{InsecureNoPassword: true}
password, err := ReadPassword(context.TODO(), opts, "test", &progress.NoopPrinter{})
password, err := ReadPassword(context.TODO(), opts, "test")
rtest.OK(t, err)
rtest.Equals(t, "", password, "got unexpected password")
opts.password = "invalid"
_, err = ReadPassword(context.TODO(), opts, "test", &progress.NoopPrinter{})
_, err = ReadPassword(context.TODO(), opts, "test")
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

@ -71,28 +71,28 @@ func TestRestoreFailsWhenUsingInvalidPatterns(t *testing.T) {
var err error
// Test --exclude
err = testRunRestoreAssumeFailure("latest", RestoreOptions{ExcludePatternOptions: filter.ExcludePatternOptions{Excludes: []string{"*[._]log[.-][0-9]", "!*[._]log[.-][0-9]"}}}, env.gopts)
err = testRunRestoreAssumeFailure(t, "latest", RestoreOptions{ExcludePatternOptions: filter.ExcludePatternOptions{Excludes: []string{"*[._]log[.-][0-9]", "!*[._]log[.-][0-9]"}}}, env.gopts)
rtest.Equals(t, `Fatal: --exclude: invalid pattern(s) provided:
*[._]log[.-][0-9]
!*[._]log[.-][0-9]`, err.Error())
// Test --iexclude
err = testRunRestoreAssumeFailure("latest", RestoreOptions{ExcludePatternOptions: filter.ExcludePatternOptions{InsensitiveExcludes: []string{"*[._]log[.-][0-9]", "!*[._]log[.-][0-9]"}}}, env.gopts)
err = testRunRestoreAssumeFailure(t, "latest", RestoreOptions{ExcludePatternOptions: filter.ExcludePatternOptions{InsensitiveExcludes: []string{"*[._]log[.-][0-9]", "!*[._]log[.-][0-9]"}}}, env.gopts)
rtest.Equals(t, `Fatal: --iexclude: invalid pattern(s) provided:
*[._]log[.-][0-9]
!*[._]log[.-][0-9]`, err.Error())
// Test --include
err = testRunRestoreAssumeFailure("latest", RestoreOptions{IncludePatternOptions: filter.IncludePatternOptions{Includes: []string{"*[._]log[.-][0-9]", "!*[._]log[.-][0-9]"}}}, env.gopts)
err = testRunRestoreAssumeFailure(t, "latest", RestoreOptions{IncludePatternOptions: filter.IncludePatternOptions{Includes: []string{"*[._]log[.-][0-9]", "!*[._]log[.-][0-9]"}}}, env.gopts)
rtest.Equals(t, `Fatal: --include: invalid pattern(s) provided:
*[._]log[.-][0-9]
!*[._]log[.-][0-9]`, err.Error())
// Test --iinclude
err = testRunRestoreAssumeFailure("latest", RestoreOptions{IncludePatternOptions: filter.IncludePatternOptions{InsensitiveIncludes: []string{"*[._]log[.-][0-9]", "!*[._]log[.-][0-9]"}}}, env.gopts)
err = testRunRestoreAssumeFailure(t, "latest", RestoreOptions{IncludePatternOptions: filter.IncludePatternOptions{InsensitiveIncludes: []string{"*[._]log[.-][0-9]", "!*[._]log[.-][0-9]"}}}, env.gopts)
rtest.Equals(t, `Fatal: --iinclude: invalid pattern(s) provided:
*[._]log[.-][0-9]
@ -112,22 +112,22 @@ func TestRestoreFailsWhenUsingInvalidPatternsFromFile(t *testing.T) {
t.Fatalf("Could not write include file: %v", fileErr)
}
err := testRunRestoreAssumeFailure("latest", RestoreOptions{IncludePatternOptions: filter.IncludePatternOptions{IncludeFiles: []string{patternsFile}}}, env.gopts)
err := testRunRestoreAssumeFailure(t, "latest", RestoreOptions{IncludePatternOptions: filter.IncludePatternOptions{IncludeFiles: []string{patternsFile}}}, env.gopts)
rtest.Equals(t, `Fatal: --include-file: invalid pattern(s) provided:
*[._]log[.-][0-9]
!*[._]log[.-][0-9]`, err.Error())
err = testRunRestoreAssumeFailure("latest", RestoreOptions{ExcludePatternOptions: filter.ExcludePatternOptions{ExcludeFiles: []string{patternsFile}}}, env.gopts)
err = testRunRestoreAssumeFailure(t, "latest", RestoreOptions{ExcludePatternOptions: filter.ExcludePatternOptions{ExcludeFiles: []string{patternsFile}}}, env.gopts)
rtest.Equals(t, `Fatal: --exclude-file: invalid pattern(s) provided:
*[._]log[.-][0-9]
!*[._]log[.-][0-9]`, err.Error())
err = testRunRestoreAssumeFailure("latest", RestoreOptions{IncludePatternOptions: filter.IncludePatternOptions{InsensitiveIncludeFiles: []string{patternsFile}}}, env.gopts)
err = testRunRestoreAssumeFailure(t, "latest", RestoreOptions{IncludePatternOptions: filter.IncludePatternOptions{InsensitiveIncludeFiles: []string{patternsFile}}}, env.gopts)
rtest.Equals(t, `Fatal: --iinclude-file: invalid pattern(s) provided:
*[._]log[.-][0-9]
!*[._]log[.-][0-9]`, err.Error())
err = testRunRestoreAssumeFailure("latest", RestoreOptions{ExcludePatternOptions: filter.ExcludePatternOptions{InsensitiveExcludeFiles: []string{patternsFile}}}, env.gopts)
err = testRunRestoreAssumeFailure(t, "latest", RestoreOptions{ExcludePatternOptions: filter.ExcludePatternOptions{InsensitiveExcludeFiles: []string{patternsFile}}}, env.gopts)
rtest.Equals(t, `Fatal: --iexclude-file: invalid pattern(s) provided:
*[._]log[.-][0-9]
!*[._]log[.-][0-9]`, err.Error())

View file

@ -30,19 +30,19 @@ type dirEntry struct {
link uint64
}
func walkDir(dir string) <-chan *dirEntry {
func walkDir(t testing.TB, dir string) <-chan *dirEntry {
ch := make(chan *dirEntry, 100)
go func() {
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
t.Logf("error: %v\n", err)
return nil
}
name, err := filepath.Rel(dir, path)
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
t.Logf("error: %v\n", err)
return nil
}
@ -56,7 +56,7 @@ func walkDir(dir string) <-chan *dirEntry {
})
if err != nil {
fmt.Fprintf(os.Stderr, "Walk() error: %v\n", err)
t.Logf("Walk() error: %v\n", err)
}
close(ch)
@ -86,10 +86,10 @@ func sameModTime(fi1, fi2 os.FileInfo) bool {
// directoriesContentsDiff returns a diff between both directories. If these
// contain exactly the same contents, then the diff is an empty string.
func directoriesContentsDiff(dir1, dir2 string) string {
func directoriesContentsDiff(t testing.TB, dir1, dir2 string) string {
var out bytes.Buffer
ch1 := walkDir(dir1)
ch2 := walkDir(dir2)
ch1 := walkDir(t, dir1)
ch2 := walkDir(t, dir2)
var a, b *dirEntry
for {
@ -146,8 +146,8 @@ func isFile(fi os.FileInfo) bool {
}
// dirStats walks dir and collects stats.
func dirStats(dir string) (stat dirStat) {
for entry := range walkDir(dir) {
func dirStats(t testing.TB, dir string) (stat dirStat) {
for entry := range walkDir(t, dir) {
if isFile(entry.fi) {
stat.files++
stat.size += uint64(entry.fi.Size())
@ -212,22 +212,14 @@ func withTestEnvironment(t testing.TB) (env *testEnvironment, cleanup func()) {
Quiet: true,
CacheDir: env.cache,
password: rtest.TestPassword,
// stdout and stderr are written to by Warnf etc. That is the written data
// usually consists of one or multiple lines and therefore can be handled well
// by t.Log.
stdout: &logOutputter{t},
stderr: &logOutputter{t},
extended: make(options.Options),
// replace this hook with "nil" if listing a filetype more than once is necessary
backendTestHook: func(r backend.Backend) (backend.Backend, error) { return newOrderedListOnceBackend(r), nil },
// start with default set of backends
backends: globalOptions.backends,
backends: collectBackends(),
}
// always overwrite global options
globalOptions = env.gopts
cleanup = func() {
if !rtest.TestCleanupTempDirs {
t.Logf("leaving temporary directory %v used for test", tempdir)
@ -248,8 +240,8 @@ func testSetupBackupData(t testing.TB, env *testEnvironment) string {
func listPacks(gopts GlobalOptions, t *testing.T) restic.IDSet {
var packs restic.IDSet
err := withTermStatus(gopts, func(ctx context.Context, term ui.Terminal) error {
printer := newTerminalProgressPrinter(gopts.JSON, gopts.verbosity, term)
err := withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
printer := ui.NewProgressPrinter(gopts.JSON, gopts.verbosity, gopts.term)
ctx, r, unlock, err := openWithReadLock(ctx, gopts, false, printer)
rtest.OK(t, err)
defer unlock()
@ -267,8 +259,8 @@ func listPacks(gopts GlobalOptions, t *testing.T) restic.IDSet {
func listTreePacks(gopts GlobalOptions, t *testing.T) restic.IDSet {
var treePacks restic.IDSet
err := withTermStatus(gopts, func(ctx context.Context, term ui.Terminal) error {
printer := newTerminalProgressPrinter(gopts.JSON, gopts.verbosity, term)
err := withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
printer := ui.NewProgressPrinter(gopts.JSON, gopts.verbosity, gopts.term)
ctx, r, unlock, err := openWithReadLock(ctx, gopts, false, printer)
rtest.OK(t, err)
defer unlock()
@ -298,8 +290,8 @@ func captureBackend(gopts *GlobalOptions) func() backend.Backend {
func removePacks(gopts GlobalOptions, t testing.TB, remove restic.IDSet) {
be := captureBackend(&gopts)
err := withTermStatus(gopts, func(ctx context.Context, term ui.Terminal) error {
printer := newTerminalProgressPrinter(gopts.JSON, gopts.verbosity, term)
err := withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
printer := ui.NewProgressPrinter(gopts.JSON, gopts.verbosity, gopts.term)
ctx, _, unlock, err := openWithExclusiveLock(ctx, gopts, false, printer)
rtest.OK(t, err)
defer unlock()
@ -314,8 +306,8 @@ func removePacks(gopts GlobalOptions, t testing.TB, remove restic.IDSet) {
func removePacksExcept(gopts GlobalOptions, t testing.TB, keep restic.IDSet, removeTreePacks bool) {
be := captureBackend(&gopts)
err := withTermStatus(gopts, func(ctx context.Context, term ui.Terminal) error {
printer := newTerminalProgressPrinter(gopts.JSON, gopts.verbosity, term)
err := withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
printer := ui.NewProgressPrinter(gopts.JSON, gopts.verbosity, gopts.term)
ctx, r, unlock, err := openWithExclusiveLock(ctx, gopts, false, printer)
rtest.OK(t, err)
defer unlock()
@ -375,8 +367,8 @@ func lastSnapshot(old, new map[string]struct{}) (map[string]struct{}, string) {
func testLoadSnapshot(t testing.TB, gopts GlobalOptions, id restic.ID) *restic.Snapshot {
var snapshot *restic.Snapshot
err := withTermStatus(gopts, func(ctx context.Context, term ui.Terminal) error {
printer := newTerminalProgressPrinter(gopts.JSON, gopts.verbosity, term)
err := withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error {
printer := ui.NewProgressPrinter(gopts.JSON, gopts.verbosity, gopts.term)
_, repo, unlock, err := openWithReadLock(ctx, gopts, false, printer)
rtest.OK(t, err)
defer unlock()
@ -391,19 +383,16 @@ func testLoadSnapshot(t testing.TB, gopts GlobalOptions, id restic.ID) *restic.S
func appendRandomData(filename string, bytes uint) error {
f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE, 0666)
if err != nil {
fmt.Fprint(os.Stderr, err)
return err
}
_, err = f.Seek(0, 2)
if err != nil {
fmt.Fprint(os.Stderr, err)
return err
}
_, err = io.Copy(f, io.LimitReader(rand.Reader, int64(bytes)))
if err != nil {
fmt.Fprint(os.Stderr, err)
return err
}
@ -423,30 +412,25 @@ func testFileSize(filename string, size int64) error {
return nil
}
func withRestoreGlobalOptions(inner func() error) error {
gopts := globalOptions
defer func() {
globalOptions = gopts
}()
return inner()
}
func withCaptureStdout(gopts GlobalOptions, inner func(gopts GlobalOptions) error) (*bytes.Buffer, error) {
func withCaptureStdout(t testing.TB, gopts GlobalOptions, callback func(ctx context.Context, gopts GlobalOptions) error) (*bytes.Buffer, error) {
buf := bytes.NewBuffer(nil)
err := withRestoreGlobalOptions(func() error {
globalOptions.stdout = buf
gopts.stdout = buf
return inner(gopts)
})
err := withTermStatusRaw(os.Stdin, buf, &logOutputter{t: t}, gopts, callback)
return buf, err
}
func withTermStatus(gopts GlobalOptions, callback func(ctx context.Context, term ui.Terminal) error) error {
func withTermStatus(t testing.TB, gopts GlobalOptions, callback func(ctx context.Context, gopts GlobalOptions) error) error {
// stdout and stderr are written to by printer functions etc. That is the written data
// usually consists of one or multiple lines and therefore can be handled well
// by t.Log.
return withTermStatusRaw(os.Stdin, &logOutputter{t: t}, &logOutputter{t: t}, gopts, callback)
}
func withTermStatusRaw(stdin io.ReadCloser, stdout, stderr io.Writer, gopts GlobalOptions, callback func(ctx context.Context, gopts GlobalOptions) error) error {
ctx, cancel := context.WithCancel(context.TODO())
var wg sync.WaitGroup
term := termstatus.New(gopts.stdout, gopts.stderr, gopts.Quiet)
term := termstatus.New(stdin, stdout, stderr, gopts.Quiet)
gopts.term = term
wg.Add(1)
go func() {
defer wg.Done()
@ -456,5 +440,5 @@ func withTermStatus(gopts GlobalOptions, callback func(ctx context.Context, term
defer wg.Wait()
defer cancel()
return callback(ctx, term)
return callback(ctx, gopts)
}

View file

@ -87,15 +87,15 @@ func TestListOnce(t *testing.T) {
createPrunableRepo(t, env)
testRunPrune(t, env.gopts, pruneOpts)
rtest.OK(t, withTermStatus(env.gopts, func(ctx context.Context, term ui.Terminal) error {
_, err := runCheck(context.TODO(), checkOpts, env.gopts, nil, term)
rtest.OK(t, withTermStatus(t, env.gopts, func(ctx context.Context, gopts GlobalOptions) error {
_, err := runCheck(context.TODO(), checkOpts, gopts, nil, gopts.term)
return err
}))
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(t, env.gopts, func(ctx context.Context, gopts GlobalOptions) error {
return runRebuildIndex(context.TODO(), RepairIndexOptions{}, gopts, gopts.term)
}))
rtest.OK(t, withTermStatus(env.gopts, func(ctx context.Context, term ui.Terminal) error {
return runRebuildIndex(context.TODO(), RepairIndexOptions{ReadAllPacks: true}, env.gopts, term)
rtest.OK(t, withTermStatus(t, env.gopts, func(ctx context.Context, gopts GlobalOptions) error {
return runRebuildIndex(context.TODO(), RepairIndexOptions{ReadAllPacks: true}, gopts, gopts.term)
}))
}
@ -162,9 +162,9 @@ func TestFindListOnce(t *testing.T) {
thirdSnapshot := restic.NewIDSet(testListSnapshots(t, env.gopts, 3)...)
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, withTermStatus(t, env.gopts, func(ctx context.Context, gopts GlobalOptions) error {
printer := ui.NewProgressPrinter(gopts.JSON, gopts.verbosity, gopts.term)
ctx, repo, unlock, err := openWithReadLock(ctx, gopts, false, printer)
rtest.OK(t, err)
defer unlock()

View file

@ -19,6 +19,7 @@ import (
"github.com/restic/restic/internal/feature"
"github.com/restic/restic/internal/repository"
"github.com/restic/restic/internal/restic"
"github.com/restic/restic/internal/ui/termstatus"
)
func init() {
@ -31,7 +32,7 @@ var ErrOK = errors.New("ok")
var cmdGroupDefault = "default"
var cmdGroupAdvanced = "advanced"
func newRootCommand() *cobra.Command {
func newRootCommand(globalOptions *GlobalOptions) *cobra.Command {
cmd := &cobra.Command{
Use: "restic",
Short: "Backup and restore files",
@ -66,41 +67,42 @@ The full documentation can be found at https://restic.readthedocs.io/ .
// Use our "generate" command instead of the cobra provided "completion" command
cmd.CompletionOptions.DisableDefaultCmd = true
// globalOptions is passed to commands by reference to allow PersistentPreRunE to modify it
cmd.AddCommand(
newBackupCommand(),
newCacheCommand(),
newCatCommand(),
newCheckCommand(),
newCopyCommand(),
newDiffCommand(),
newDumpCommand(),
newFeaturesCommand(),
newFindCommand(),
newForgetCommand(),
newGenerateCommand(),
newInitCommand(),
newKeyCommand(),
newListCommand(),
newLsCommand(),
newMigrateCommand(),
newOptionsCommand(),
newPruneCommand(),
newRebuildIndexCommand(),
newRecoverCommand(),
newRepairCommand(),
newRestoreCommand(),
newRewriteCommand(),
newSnapshotsCommand(),
newStatsCommand(),
newTagCommand(),
newUnlockCommand(),
newVersionCommand(),
newBackupCommand(globalOptions),
newCacheCommand(globalOptions),
newCatCommand(globalOptions),
newCheckCommand(globalOptions),
newCopyCommand(globalOptions),
newDiffCommand(globalOptions),
newDumpCommand(globalOptions),
newFeaturesCommand(globalOptions),
newFindCommand(globalOptions),
newForgetCommand(globalOptions),
newGenerateCommand(globalOptions),
newInitCommand(globalOptions),
newKeyCommand(globalOptions),
newListCommand(globalOptions),
newLsCommand(globalOptions),
newMigrateCommand(globalOptions),
newOptionsCommand(globalOptions),
newPruneCommand(globalOptions),
newRebuildIndexCommand(globalOptions),
newRecoverCommand(globalOptions),
newRepairCommand(globalOptions),
newRestoreCommand(globalOptions),
newRewriteCommand(globalOptions),
newSnapshotsCommand(globalOptions),
newStatsCommand(globalOptions),
newTagCommand(globalOptions),
newUnlockCommand(globalOptions),
newVersionCommand(globalOptions),
)
registerDebugCommand(cmd)
registerMountCommand(cmd)
registerSelfUpdateCommand(cmd)
registerProfiling(cmd)
registerDebugCommand(cmd, globalOptions)
registerMountCommand(cmd, globalOptions)
registerSelfUpdateCommand(cmd, globalOptions)
registerProfiling(cmd, os.Stderr)
return cmd
}
@ -125,7 +127,7 @@ func tweakGoGC() {
}
}
func printExitError(code int, message string) {
func printExitError(globalOptions GlobalOptions, code int, message string) {
if globalOptions.JSON {
type jsonExitError struct {
MessageType string `json:"message_type"` // exit_error
@ -139,7 +141,7 @@ func printExitError(code int, message string) {
Message: message,
}
err := json.NewEncoder(globalOptions.stderr).Encode(jsonS)
err := json.NewEncoder(os.Stderr).Encode(jsonS)
if err != nil {
// ignore error as there's no good way to handle it
_, _ = fmt.Fprintf(os.Stderr, "JSON encode failed: %v\n", err)
@ -147,7 +149,7 @@ func printExitError(code int, message string) {
return
}
} else {
_, _ = fmt.Fprintf(globalOptions.stderr, "%v\n", message)
_, _ = fmt.Fprintf(os.Stderr, "%v\n", message)
}
}
@ -170,16 +172,23 @@ func main() {
debug.Log("restic %s compiled with %v on %v/%v",
version, runtime.Version(), runtime.GOOS, runtime.GOARCH)
ctx := createGlobalContext()
err = newRootCommand().ExecuteContext(ctx)
switch err {
case nil:
err = ctx.Err()
case ErrOK:
// ErrOK overwrites context cancellation errors
err = nil
globalOptions := GlobalOptions{
backends: collectBackends(),
}
func() {
term, cancel := termstatus.Setup(os.Stdin, os.Stdout, os.Stderr, globalOptions.Quiet)
defer cancel()
globalOptions.term = term
ctx := createGlobalContext(os.Stderr)
err = newRootCommand(&globalOptions).ExecuteContext(ctx)
switch err {
case nil:
err = ctx.Err()
case ErrOK:
// ErrOK overwrites context cancellation errors
err = nil
}
}()
var exitMessage string
switch {
@ -224,7 +233,7 @@ func main() {
}
if exitCode != 0 {
printExitError(exitCode, exitMessage)
printExitError(globalOptions, exitCode, exitMessage)
}
Exit(exitCode)
}

View file

@ -1,84 +0,0 @@
package main
import (
"fmt"
"os"
"strconv"
"time"
"github.com/restic/restic/internal/terminal"
"github.com/restic/restic/internal/ui"
"github.com/restic/restic/internal/ui/progress"
)
// calculateProgressInterval returns the interval configured via RESTIC_PROGRESS_FPS
// or if unset returns an interval for 60fps on interactive terminals and 0 (=disabled)
// for non-interactive terminals or when run using the --quiet flag
func calculateProgressInterval(show bool, json bool) time.Duration {
interval := time.Second / 60
fps, err := strconv.ParseFloat(os.Getenv("RESTIC_PROGRESS_FPS"), 64)
if err == nil && fps > 0 {
if fps > 60 {
fps = 60
}
interval = time.Duration(float64(time.Second) / fps)
} else if !json && !terminal.StdoutCanUpdateStatus() || !show {
interval = 0
}
return interval
}
// 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
}
interval := calculateProgressInterval(show, false)
return progress.NewCounter(interval, max, func(v uint64, max uint64, d time.Duration, final bool) {
var status string
if max == 0 {
status = fmt.Sprintf("[%s] %d %s",
ui.FormatDuration(d), v, description)
} else {
status = fmt.Sprintf("[%s] %s %d / %d %s",
ui.FormatDuration(d), ui.FormatPercent(v, max), v, max, description)
}
if final {
term.SetStatus(nil)
term.Print(status)
} else {
term.SetStatus([]string{status})
}
})
}
type terminalProgressPrinter struct {
term ui.Terminal
ui.Message
show bool
}
func (t *terminalProgressPrinter) NewCounter(description string) *progress.Counter {
return newTerminalProgressMax(t.show, 0, description, t.term)
}
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,7 +5,6 @@ import (
"os"
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/ui/progress"
"github.com/spf13/pflag"
)
@ -60,7 +59,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, printer progress.Printer) (GlobalOptions, bool, error) {
func fillSecondaryGlobalOpts(ctx context.Context, opts secondaryRepoOptions, gopts GlobalOptions, repoPrefix string) (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)")
}
@ -116,7 +115,7 @@ func fillSecondaryGlobalOpts(ctx context.Context, opts secondaryRepoOptions, gop
return GlobalOptions{}, false, err
}
}
dstGopts.password, err = ReadPassword(ctx, dstGopts, "enter password for "+repoPrefix+" repository: ", printer)
dstGopts.password, err = ReadPassword(ctx, dstGopts, "enter password for "+repoPrefix+" repository: ")
if err != nil {
return GlobalOptions{}, false, err
}

View file

@ -7,7 +7,6 @@ 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
@ -131,12 +130,6 @@ func TestFillSecondaryGlobalOpts(t *testing.T) {
PasswordCommand: "notEmpty",
},
},
{
// Test must fail as no password is given.
Opts: secondaryRepoOptions{
Repo: "backupDst",
},
},
{
// Test must fail as current and legacy options are mixed
Opts: secondaryRepoOptions{
@ -172,7 +165,7 @@ func TestFillSecondaryGlobalOpts(t *testing.T) {
// Test all valid cases
for _, testCase := range validSecondaryRepoTestCases {
DstGOpts, isFromRepo, err := fillSecondaryGlobalOpts(context.TODO(), testCase.Opts, gOpts, "destination", &progress.NoopPrinter{})
DstGOpts, isFromRepo, err := fillSecondaryGlobalOpts(context.TODO(), testCase.Opts, gOpts, "destination")
rtest.OK(t, err)
rtest.Equals(t, DstGOpts, testCase.DstGOpts)
rtest.Equals(t, isFromRepo, testCase.FromRepo)
@ -180,7 +173,7 @@ func TestFillSecondaryGlobalOpts(t *testing.T) {
// Test all invalid cases
for _, testCase := range invalidSecondaryRepoTestCases {
_, _, err := fillSecondaryGlobalOpts(context.TODO(), testCase.Opts, gOpts, "destination", &progress.NoopPrinter{})
_, _, err := fillSecondaryGlobalOpts(context.TODO(), testCase.Opts, gOpts, "destination")
rtest.Assert(t, err != nil, "Expected error, but function did not return an error")
}
}

View file

@ -1,41 +0,0 @@
package main
import (
"context"
"sync"
"github.com/restic/restic/internal/ui/termstatus"
)
// setupTermstatus creates a new termstatus and reroutes globalOptions.{stdout,stderr} to it
// The returned function must be called to shut down the termstatus,
//
// Expected usage:
// ```
// term, cancel := setupTermstatus()
// defer cancel()
// // do stuff
// ```
func setupTermstatus() (*termstatus.Terminal, func()) {
var wg sync.WaitGroup
// only shutdown once cancel is called to ensure that no output is lost
cancelCtx, cancel := context.WithCancel(context.Background())
term := termstatus.New(globalOptions.stdout, globalOptions.stderr, globalOptions.Quiet)
wg.Add(1)
go func() {
defer wg.Done()
term.Run(cancelCtx)
}()
// use the termstatus for stdout/stderr
prevStdout, prevStderr := globalOptions.stdout, globalOptions.stderr
globalOptions.stdout, globalOptions.stderr = termstatus.WrapStdio(term)
return term, func() {
// shutdown termstatus
globalOptions.stdout, globalOptions.stderr = prevStdout, prevStderr
cancel()
wg.Wait()
}
}

View file

@ -68,7 +68,7 @@ func (s *ItemStats) Add(other ItemStats) {
// ToNoder returns a restic.Node for a File.
type ToNoder interface {
ToNode(ignoreXattrListError bool) (*restic.Node, error)
ToNode(ignoreXattrListError bool, warnf func(format string, args ...any)) (*restic.Node, error)
}
type archiverRepo interface {
@ -263,7 +263,9 @@ func (arch *Archiver) trackItem(item string, previous, current *restic.Node, s I
// nodeFromFileInfo returns the restic node from an os.FileInfo.
func (arch *Archiver) nodeFromFileInfo(snPath, filename string, meta ToNoder, ignoreXattrListError bool) (*restic.Node, error) {
node, err := meta.ToNode(ignoreXattrListError)
node, err := meta.ToNode(ignoreXattrListError, func(format string, args ...any) {
_ = arch.error(filename, fmt.Errorf(format, args...))
})
// node does not exist. This prevents all further processing for this file.
// If an error and a node are returned, then preserve as much data as possible (see below).
if err != nil && node == nil {

View file

@ -551,7 +551,7 @@ func rename(t testing.TB, oldname, newname string) {
func nodeFromFile(t testing.TB, localFs fs.FS, filename string) *restic.Node {
meta, err := localFs.OpenFile(filename, fs.O_NOFOLLOW, true)
rtest.OK(t, err)
node, err := meta.ToNode(false)
node, err := meta.ToNode(false, t.Logf)
rtest.OK(t, err)
rtest.OK(t, meta.Close())
@ -2287,9 +2287,9 @@ func (f overrideFile) MakeReadable() error {
return f.File.MakeReadable()
}
func (f overrideFile) ToNode(ignoreXattrListError bool) (*restic.Node, error) {
func (f overrideFile) ToNode(ignoreXattrListError bool, warnf func(format string, args ...any)) (*restic.Node, error) {
if f.ofs.overrideNode == nil {
return f.File.ToNode(ignoreXattrListError)
return f.File.ToNode(ignoreXattrListError, warnf)
}
return f.ofs.overrideNode, f.ofs.overrideErr
}
@ -2321,7 +2321,7 @@ func TestMetadataChanged(t *testing.T) {
localFS := &fs.Local{}
meta, err := localFS.OpenFile("testfile", fs.O_NOFOLLOW, true)
rtest.OK(t, err)
want, err := meta.ToNode(false)
want, err := meta.ToNode(false, t.Logf)
rtest.OK(t, err)
rtest.OK(t, meta.Close())
@ -2455,7 +2455,7 @@ type mockToNoder struct {
err error
}
func (m *mockToNoder) ToNode(_ bool) (*restic.Node, error) {
func (m *mockToNoder) ToNode(_ bool, _ func(format string, args ...any)) (*restic.Node, error) {
return m.node, m.err
}

View file

@ -50,7 +50,7 @@ func startFileSaver(ctx context.Context, t testing.TB, _ fs.FS) (*fileSaver, con
s := newFileSaver(ctx, wg, saveBlob, pol, workers, workers)
s.NodeFromFileInfo = func(snPath, filename string, meta ToNoder, ignoreXattrListError bool) (*restic.Node, error) {
return meta.ToNode(ignoreXattrListError)
return meta.ToNode(ignoreXattrListError, t.Logf)
}
return s, ctx, wg

View file

@ -159,13 +159,13 @@ func supportedAccessTiers() []blob.AccessTier {
}
// Open opens the Azure backend at specified container.
func Open(_ context.Context, cfg Config, rt http.RoundTripper) (*Backend, error) {
func Open(_ context.Context, cfg Config, rt http.RoundTripper, _ func(string, ...interface{})) (*Backend, error) {
return open(cfg, rt)
}
// Create opens the Azure backend at specified container and creates the container if
// it does not exist yet.
func Create(ctx context.Context, cfg Config, rt http.RoundTripper) (*Backend, error) {
func Create(ctx context.Context, cfg Config, rt http.RoundTripper, _ func(string, ...interface{})) (*Backend, error) {
be, err := open(cfg, rt)
if err != nil {

View file

@ -116,7 +116,7 @@ func TestBackendAzureAccountToken(t *testing.T) {
t.Fatal(err)
}
_, err = azure.Create(ctx, *cfg, tr)
_, err = azure.Create(ctx, *cfg, tr, t.Logf)
if err != nil {
t.Fatal(err)
}
@ -159,7 +159,7 @@ func TestBackendAzureContainerToken(t *testing.T) {
t.Fatal(err)
}
_, err = azure.Create(ctx, *cfg, tr)
_, err = azure.Create(ctx, *cfg, tr, t.Logf)
if err != nil {
t.Fatal(err)
}
@ -193,7 +193,7 @@ func TestUploadLargeFile(t *testing.T) {
t.Fatal(err)
}
be, err := azure.Create(ctx, *cfg, tr)
be, err := azure.Create(ctx, *cfg, tr, t.Logf)
if err != nil {
t.Fatal(err)
}

View file

@ -88,7 +88,7 @@ func newClient(ctx context.Context, cfg Config, rt http.RoundTripper) (*b2.Clien
}
// Open opens a connection to the B2 service.
func Open(ctx context.Context, cfg Config, rt http.RoundTripper) (backend.Backend, error) {
func Open(ctx context.Context, cfg Config, rt http.RoundTripper, _ func(string, ...interface{})) (backend.Backend, error) {
debug.Log("cfg %#v", cfg)
ctx, cancel := context.WithCancel(ctx)
@ -120,7 +120,7 @@ func Open(ctx context.Context, cfg Config, rt http.RoundTripper) (backend.Backen
// Create opens a connection to the B2 service. If the bucket does not exist yet,
// it is created.
func Create(ctx context.Context, cfg Config, rt http.RoundTripper) (backend.Backend, error) {
func Create(ctx context.Context, cfg Config, rt http.RoundTripper, _ func(string, ...interface{})) (backend.Backend, error) {
debug.Log("cfg %#v", cfg)
ctx, cancel := context.WithCancel(ctx)

View file

@ -2,9 +2,7 @@ package cache
import (
"context"
"fmt"
"io"
"os"
"sync"
"github.com/restic/restic/internal/backend"
@ -22,16 +20,18 @@ type Backend struct {
// is finished.
inProgressMutex sync.Mutex
inProgress map[backend.Handle]chan struct{}
errorLog func(string, ...interface{})
}
// ensure Backend implements backend.Backend
var _ backend.Backend = &Backend{}
func newBackend(be backend.Backend, c *Cache) *Backend {
func newBackend(be backend.Backend, c *Cache, errorLog func(string, ...interface{})) *Backend {
return &Backend{
Backend: be,
Cache: c,
inProgress: make(map[backend.Handle]chan struct{}),
errorLog: errorLog,
}
}
@ -253,7 +253,7 @@ func (b *Backend) List(ctx context.Context, t backend.FileType, fn func(f backen
// clear the cache for files that are not in the repo anymore, ignore errors
err = b.Cache.Clear(t, ids)
if err != nil {
fmt.Fprintf(os.Stderr, "error clearing %s files in cache: %v\n", t.String(), err)
b.errorLog("error clearing %s files in cache: %v\n", t.String(), err)
}
return nil

View file

@ -67,7 +67,7 @@ func list(t testing.TB, be backend.Backend, fn func(backend.FileInfo) error) {
func TestBackend(t *testing.T) {
be := mem.New()
c := TestNewCache(t)
wbe := c.Wrap(be)
wbe := c.Wrap(be, t.Logf)
h, data := randomData(5234142)
@ -135,7 +135,7 @@ func (l *loadCountingBackend) Load(ctx context.Context, h backend.Handle, length
func TestOutOfBoundsAccess(t *testing.T) {
be := &loadCountingBackend{Backend: mem.New()}
c := TestNewCache(t)
wbe := c.Wrap(be)
wbe := c.Wrap(be, t.Logf)
h, data := randomData(50)
save(t, be, h, data)
@ -164,7 +164,7 @@ func TestOutOfBoundsAccess(t *testing.T) {
func TestForget(t *testing.T) {
be := &loadCountingBackend{Backend: mem.New()}
c := TestNewCache(t)
wbe := c.Wrap(be)
wbe := c.Wrap(be, t.Logf)
h, data := randomData(50)
save(t, be, h, data)
@ -236,7 +236,7 @@ func TestErrorBackend(t *testing.T) {
time.Sleep(time.Millisecond)
}
wrappedBE := c.Wrap(errBackend)
wrappedBE := c.Wrap(errBackend, t.Logf)
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
@ -249,7 +249,7 @@ func TestErrorBackend(t *testing.T) {
func TestAutomaticCacheClear(t *testing.T) {
be := mem.New()
c := TestNewCache(t)
wbe := c.Wrap(be)
wbe := c.Wrap(be, t.Logf)
// add two handles h1 and h2
h1, data := randomData(2000)
@ -308,7 +308,7 @@ func TestAutomaticCacheClearInvalidFilename(t *testing.T) {
}
save(t, be, h, data)
wbe := c.Wrap(be)
wbe := c.Wrap(be, t.Logf)
// list all files in the backend
list(t, wbe, func(_ backend.FileInfo) error { return nil })

View file

@ -237,8 +237,8 @@ func IsOld(t time.Time, maxAge time.Duration) bool {
}
// Wrap returns a backend with a cache.
func (c *Cache) Wrap(be backend.Backend) backend.Backend {
return newBackend(be, c)
func (c *Cache) Wrap(be backend.Backend, errorLog func(string, ...interface{})) backend.Backend {
return newBackend(be, c, errorLog)
}
// BaseDir returns the base directory.

View file

@ -120,7 +120,7 @@ func open(cfg Config, rt http.RoundTripper) (*Backend, error) {
}
// Open opens the gs backend at the specified bucket.
func Open(_ context.Context, cfg Config, rt http.RoundTripper) (backend.Backend, error) {
func Open(_ context.Context, cfg Config, rt http.RoundTripper, _ func(string, ...interface{})) (backend.Backend, error) {
return open(cfg, rt)
}
@ -129,7 +129,7 @@ func Open(_ context.Context, cfg Config, rt http.RoundTripper) (backend.Backend,
//
// The service account must have the "storage.buckets.create" permission to
// create a bucket the does not yet exist.
func Create(ctx context.Context, cfg Config, rt http.RoundTripper) (backend.Backend, error) {
func Create(ctx context.Context, cfg Config, rt http.RoundTripper, _ func(string, ...interface{})) (backend.Backend, error) {
be, err := open(cfg, rt)
if err != nil {
return nil, err

View file

@ -7,10 +7,10 @@ import (
"github.com/restic/restic/internal/backend"
)
func WrapBackendConstructor[B backend.Backend, C any](constructor func(ctx context.Context, cfg C) (B, error)) func(ctx context.Context, cfg C, lim Limiter) (backend.Backend, error) {
return func(ctx context.Context, cfg C, lim Limiter) (backend.Backend, error) {
func WrapBackendConstructor[B backend.Backend, C any](constructor func(ctx context.Context, cfg C, errorLog func(string, ...interface{})) (B, error)) func(ctx context.Context, cfg C, lim Limiter, errorLog func(string, ...interface{})) (backend.Backend, error) {
return func(ctx context.Context, cfg C, lim Limiter, errorLog func(string, ...interface{})) (backend.Backend, error) {
var be backend.Backend
be, err := constructor(ctx, cfg)
be, err := constructor(ctx, cfg, errorLog)
if err != nil {
return nil, err
}

View file

@ -32,7 +32,7 @@ func TestLayout(t *testing.T) {
be, err := Open(context.TODO(), Config{
Path: repo,
Connections: 2,
})
}, t.Logf)
if err != nil {
t.Fatal(err)
}

View file

@ -52,14 +52,14 @@ func open(cfg Config) (*Local, error) {
}
// Open opens the local backend as specified by config.
func Open(_ context.Context, cfg Config) (*Local, error) {
func Open(_ context.Context, cfg Config, _ func(string, ...interface{})) (*Local, error) {
debug.Log("open local backend at %v", cfg.Path)
return open(cfg)
}
// Create creates all the necessary files and directories for a new local
// backend at dir. Afterwards a new config blob should be created.
func Create(_ context.Context, cfg Config) (*Local, error) {
func Create(_ context.Context, cfg Config, _ func(string, ...interface{})) (*Local, error) {
debug.Log("create local backend at %v", cfg.Path)
be, err := open(cfg)

View file

@ -26,7 +26,7 @@ func TestNoSpacePermanent(t *testing.T) {
dir := rtest.TempDir(t)
be, err := Open(context.Background(), Config{Path: dir, Connections: 2})
be, err := Open(context.Background(), Config{Path: dir, Connections: 2}, t.Logf)
rtest.OK(t, err)
defer func() {
rtest.OK(t, be.Close())

View file

@ -66,7 +66,7 @@ func empty(t testing.TB, dir string) {
func openclose(t testing.TB, dir string) {
cfg := local.Config{Path: dir}
be, err := local.Open(context.TODO(), cfg)
be, err := local.Open(context.TODO(), cfg, t.Logf)
if err != nil {
t.Logf("Open returned error %v", err)
}

View file

@ -33,16 +33,16 @@ type Factory interface {
Scheme() string
ParseConfig(s string) (interface{}, error)
StripPassword(s string) string
Create(ctx context.Context, cfg interface{}, rt http.RoundTripper, lim limiter.Limiter) (backend.Backend, error)
Open(ctx context.Context, cfg interface{}, rt http.RoundTripper, lim limiter.Limiter) (backend.Backend, error)
Create(ctx context.Context, cfg interface{}, rt http.RoundTripper, lim limiter.Limiter, errorLog func(string, ...interface{})) (backend.Backend, error)
Open(ctx context.Context, cfg interface{}, rt http.RoundTripper, lim limiter.Limiter, errorLog func(string, ...interface{})) (backend.Backend, error)
}
type genericBackendFactory[C any, T backend.Backend] struct {
scheme string
parseConfigFn func(s string) (*C, error)
stripPasswordFn func(s string) string
createFn func(ctx context.Context, cfg C, rt http.RoundTripper, lim limiter.Limiter) (T, error)
openFn func(ctx context.Context, cfg C, rt http.RoundTripper, lim limiter.Limiter) (T, error)
createFn func(ctx context.Context, cfg C, rt http.RoundTripper, lim limiter.Limiter, errorLog func(string, ...interface{})) (T, error)
openFn func(ctx context.Context, cfg C, rt http.RoundTripper, lim limiter.Limiter, errorLog func(string, ...interface{})) (T, error)
}
func (f *genericBackendFactory[C, T]) Scheme() string {
@ -58,29 +58,29 @@ func (f *genericBackendFactory[C, T]) StripPassword(s string) string {
}
return s
}
func (f *genericBackendFactory[C, T]) Create(ctx context.Context, cfg interface{}, rt http.RoundTripper, lim limiter.Limiter) (backend.Backend, error) {
return f.createFn(ctx, *cfg.(*C), rt, lim)
func (f *genericBackendFactory[C, T]) Create(ctx context.Context, cfg interface{}, rt http.RoundTripper, lim limiter.Limiter, errorLog func(string, ...interface{})) (backend.Backend, error) {
return f.createFn(ctx, *cfg.(*C), rt, lim, errorLog)
}
func (f *genericBackendFactory[C, T]) Open(ctx context.Context, cfg interface{}, rt http.RoundTripper, lim limiter.Limiter) (backend.Backend, error) {
return f.openFn(ctx, *cfg.(*C), rt, lim)
func (f *genericBackendFactory[C, T]) Open(ctx context.Context, cfg interface{}, rt http.RoundTripper, lim limiter.Limiter, errorLog func(string, ...interface{})) (backend.Backend, error) {
return f.openFn(ctx, *cfg.(*C), rt, lim, errorLog)
}
func NewHTTPBackendFactory[C any, T backend.Backend](
scheme string,
parseConfigFn func(s string) (*C, error),
stripPasswordFn func(s string) string,
createFn func(ctx context.Context, cfg C, rt http.RoundTripper) (T, error),
openFn func(ctx context.Context, cfg C, rt http.RoundTripper) (T, error)) Factory {
createFn func(ctx context.Context, cfg C, rt http.RoundTripper, errorLog func(string, ...interface{})) (T, error),
openFn func(ctx context.Context, cfg C, rt http.RoundTripper, errorLog func(string, ...interface{})) (T, error)) Factory {
return &genericBackendFactory[C, T]{
scheme: scheme,
parseConfigFn: parseConfigFn,
stripPasswordFn: stripPasswordFn,
createFn: func(ctx context.Context, cfg C, rt http.RoundTripper, _ limiter.Limiter) (T, error) {
return createFn(ctx, cfg, rt)
createFn: func(ctx context.Context, cfg C, rt http.RoundTripper, _ limiter.Limiter, errorLog func(string, ...interface{})) (T, error) {
return createFn(ctx, cfg, rt, errorLog)
},
openFn: func(ctx context.Context, cfg C, rt http.RoundTripper, _ limiter.Limiter) (T, error) {
return openFn(ctx, cfg, rt)
openFn: func(ctx context.Context, cfg C, rt http.RoundTripper, _ limiter.Limiter, errorLog func(string, ...interface{})) (T, error) {
return openFn(ctx, cfg, rt, errorLog)
},
}
}
@ -89,18 +89,18 @@ func NewLimitedBackendFactory[C any, T backend.Backend](
scheme string,
parseConfigFn func(s string) (*C, error),
stripPasswordFn func(s string) string,
createFn func(ctx context.Context, cfg C, lim limiter.Limiter) (T, error),
openFn func(ctx context.Context, cfg C, lim limiter.Limiter) (T, error)) Factory {
createFn func(ctx context.Context, cfg C, lim limiter.Limiter, errorLog func(string, ...interface{})) (T, error),
openFn func(ctx context.Context, cfg C, lim limiter.Limiter, errorLog func(string, ...interface{})) (T, error)) Factory {
return &genericBackendFactory[C, T]{
scheme: scheme,
parseConfigFn: parseConfigFn,
stripPasswordFn: stripPasswordFn,
createFn: func(ctx context.Context, cfg C, _ http.RoundTripper, lim limiter.Limiter) (T, error) {
return createFn(ctx, cfg, lim)
createFn: func(ctx context.Context, cfg C, _ http.RoundTripper, lim limiter.Limiter, errorLog func(string, ...interface{})) (T, error) {
return createFn(ctx, cfg, lim, errorLog)
},
openFn: func(ctx context.Context, cfg C, _ http.RoundTripper, lim limiter.Limiter) (T, error) {
return openFn(ctx, cfg, lim)
openFn: func(ctx context.Context, cfg C, _ http.RoundTripper, lim limiter.Limiter, errorLog func(string, ...interface{})) (T, error) {
return openFn(ctx, cfg, lim, errorLog)
},
}
}

View file

@ -33,10 +33,10 @@ func NewFactory() location.Factory {
return &struct{}{}, nil
},
location.NoPassword,
func(_ context.Context, _ struct{}, _ http.RoundTripper) (*MemoryBackend, error) {
func(_ context.Context, _ struct{}, _ http.RoundTripper, _ func(string, ...interface{})) (*MemoryBackend, error) {
return be, nil
},
func(_ context.Context, _ struct{}, _ http.RoundTripper) (*MemoryBackend, error) {
func(_ context.Context, _ struct{}, _ http.RoundTripper, _ func(string, ...interface{})) (*MemoryBackend, error) {
return be, nil
},
)

View file

@ -43,7 +43,7 @@ func NewFactory() location.Factory {
}
// run starts command with args and initializes the StdioConn.
func run(command string, args ...string) (*StdioConn, *sync.WaitGroup, chan struct{}, func() error, error) {
func run(errorLog func(string, ...interface{}), command string, args ...string) (*StdioConn, *sync.WaitGroup, chan struct{}, func() error, error) {
cmd := exec.Command(command, args...)
p, err := cmd.StderrPipe()
@ -61,7 +61,7 @@ func run(command string, args ...string) (*StdioConn, *sync.WaitGroup, chan stru
defer close(waitCh)
sc := bufio.NewScanner(p)
for sc.Scan() {
fmt.Fprintf(os.Stderr, "rclone: %v\n", sc.Text())
errorLog("rclone: %v\n", sc.Text())
}
debug.Log("command has exited, closing waitCh")
}()
@ -140,7 +140,7 @@ func wrapConn(c *StdioConn, lim limiter.Limiter) *wrappedConn {
}
// New initializes a Backend and starts the process.
func newBackend(ctx context.Context, cfg Config, lim limiter.Limiter) (*Backend, error) {
func newBackend(ctx context.Context, cfg Config, lim limiter.Limiter, errorLog func(string, ...interface{})) (*Backend, error) {
var (
args []string
err error
@ -170,7 +170,7 @@ func newBackend(ctx context.Context, cfg Config, lim limiter.Limiter) (*Backend,
arg0, args := args[0], args[1:]
debug.Log("running command: %v %v", arg0, args)
stdioConn, wg, waitCh, bg, err := run(arg0, args...)
stdioConn, wg, waitCh, bg, err := run(errorLog, arg0, args...)
if err != nil {
return nil, err
}
@ -263,8 +263,8 @@ func newBackend(ctx context.Context, cfg Config, lim limiter.Limiter) (*Backend,
}
// Open starts an rclone process with the given config.
func Open(ctx context.Context, cfg Config, lim limiter.Limiter) (*Backend, error) {
be, err := newBackend(ctx, cfg, lim)
func Open(ctx context.Context, cfg Config, lim limiter.Limiter, errorLog func(string, ...interface{})) (*Backend, error) {
be, err := newBackend(ctx, cfg, lim, errorLog)
if err != nil {
return nil, err
}
@ -279,7 +279,7 @@ func Open(ctx context.Context, cfg Config, lim limiter.Limiter) (*Backend, error
URL: url,
}
restBackend, err := rest.Open(ctx, restConfig, debug.RoundTripper(be.tr))
restBackend, err := rest.Open(ctx, restConfig, debug.RoundTripper(be.tr), errorLog)
if err != nil {
_ = be.Close()
return nil, err
@ -290,8 +290,8 @@ func Open(ctx context.Context, cfg Config, lim limiter.Limiter) (*Backend, error
}
// Create initializes a new restic repo with rclone.
func Create(ctx context.Context, cfg Config, lim limiter.Limiter) (*Backend, error) {
be, err := newBackend(ctx, cfg, lim)
func Create(ctx context.Context, cfg Config, lim limiter.Limiter, errorLog func(string, ...interface{})) (*Backend, error) {
be, err := newBackend(ctx, cfg, lim, errorLog)
if err != nil {
return nil, err
}
@ -308,7 +308,7 @@ func Create(ctx context.Context, cfg Config, lim limiter.Limiter) (*Backend, err
URL: url,
}
restBackend, err := rest.Create(ctx, restConfig, debug.RoundTripper(be.tr))
restBackend, err := rest.Create(ctx, restConfig, debug.RoundTripper(be.tr), errorLog)
if err != nil {
_ = be.Close()
return nil, err

View file

@ -15,7 +15,7 @@ func TestRcloneExit(t *testing.T) {
dir := rtest.TempDir(t)
cfg := NewConfig()
cfg.Remote = dir
be, err := Open(context.TODO(), cfg, nil)
be, err := Open(context.TODO(), cfg, nil, t.Logf)
var e *exec.Error
if errors.As(err, &e) && e.Err == exec.ErrNotFound {
t.Skipf("program %q not found", e.Name)
@ -45,7 +45,7 @@ func TestRcloneFailedStart(t *testing.T) {
cfg := NewConfig()
// exits with exit code 1
cfg.Program = "false"
_, err := Open(context.TODO(), cfg, nil)
_, err := Open(context.TODO(), cfg, nil, t.Logf)
var e *exec.ExitError
if !errors.As(err, &e) {
// unexpected error

View file

@ -55,7 +55,7 @@ const (
)
// Open opens the REST backend with the given config.
func Open(_ context.Context, cfg Config, rt http.RoundTripper) (*Backend, error) {
func Open(_ context.Context, cfg Config, rt http.RoundTripper, _ func(string, ...interface{})) (*Backend, error) {
// use url without trailing slash for layout
url := cfg.URL.String()
if url[len(url)-1] == '/' {
@ -84,8 +84,8 @@ func drainAndClose(resp *http.Response) error {
}
// Create creates a new REST on server configured in config.
func Create(ctx context.Context, cfg Config, rt http.RoundTripper) (*Backend, error) {
be, err := Open(ctx, cfg, rt)
func Create(ctx context.Context, cfg Config, rt http.RoundTripper, errorLog func(string, ...interface{})) (*Backend, error) {
be, err := Open(ctx, cfg, rt, errorLog)
if err != nil {
return nil, err
}

View file

@ -117,7 +117,7 @@ func TestListAPI(t *testing.T) {
URL: srvURL,
}
be, err := rest.Open(context.TODO(), cfg, http.DefaultTransport)
be, err := rest.Open(context.TODO(), cfg, http.DefaultTransport, t.Logf)
if err != nil {
t.Fatal(err)
}

View file

@ -106,7 +106,7 @@ func runRESTServer(ctx context.Context, t testing.TB, dir, reqListenAddr string)
matched = true
}
}
_, _ = fmt.Fprintln(os.Stdout, line) // print all output to console
t.Log(line)
}
}()

View file

@ -195,13 +195,13 @@ func getCredentials(cfg Config, tr http.RoundTripper) (*credentials.Credentials,
// Open opens the S3 backend at bucket and region. The bucket is created if it
// does not exist yet.
func Open(_ context.Context, cfg Config, rt http.RoundTripper) (backend.Backend, error) {
func Open(_ context.Context, cfg Config, rt http.RoundTripper, _ func(string, ...interface{})) (backend.Backend, error) {
return open(cfg, rt)
}
// Create opens the S3 backend at bucket and region and creates the bucket if
// it does not exist yet.
func Create(ctx context.Context, cfg Config, rt http.RoundTripper) (backend.Backend, error) {
func Create(ctx context.Context, cfg Config, rt http.RoundTripper, _ func(string, ...interface{})) (backend.Backend, error) {
be, err := open(cfg, rt)
if err != nil {
return nil, errors.Wrap(err, "open")

View file

@ -117,9 +117,9 @@ func newMinioTestSuite(t testing.TB) (*test.Suite[s3.Config], func()) {
return &cfg, nil
},
Factory: location.NewHTTPBackendFactory("s3", s3.ParseConfig, location.NoPassword, func(ctx context.Context, cfg s3.Config, rt http.RoundTripper) (be backend.Backend, err error) {
Factory: location.NewHTTPBackendFactory("s3", s3.ParseConfig, location.NoPassword, func(ctx context.Context, cfg s3.Config, rt http.RoundTripper, errorLog func(string, ...interface{})) (be backend.Backend, err error) {
for i := 0; i < 50; i++ {
be, err = s3.Create(ctx, cfg, rt)
be, err = s3.Create(ctx, cfg, rt, errorLog)
if err != nil {
t.Logf("s3 open: try %d: error %v", i, err)
time.Sleep(500 * time.Millisecond)

View file

@ -39,7 +39,7 @@ func TestLayout(t *testing.T) {
Command: fmt.Sprintf("%q -e", sftpServer),
Path: repo,
Connections: 5,
})
}, t.Logf)
if err != nil {
t.Fatal(err)
}

Some files were not shown because too many files have changed in this diff Show more