From 6bd85d24126dff5779985af654fddfbc09a21279 Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Sun, 14 Sep 2025 14:52:02 +0200 Subject: [PATCH 01/25] reduce usages of globalOptions variable --- cmd/restic/cmd_backup.go | 2 +- cmd/restic/cmd_backup_integration_test.go | 23 ++++++++----------- cmd/restic/cmd_cache.go | 2 +- cmd/restic/cmd_check_integration_test.go | 19 ++++++++------- cmd/restic/cmd_debug.go | 10 ++++---- cmd/restic/cmd_diff.go | 4 ++-- cmd/restic/cmd_forget.go | 8 +++---- cmd/restic/cmd_generate.go | 12 +++++----- cmd/restic/cmd_init.go | 2 +- cmd/restic/cmd_key_list.go | 4 ++-- cmd/restic/cmd_ls.go | 4 ++-- .../cmd_repair_index_integration_test.go | 4 ++-- cmd/restic/cmd_restore_integration_test.go | 2 +- cmd/restic/cmd_snapshots.go | 6 ++--- cmd/restic/cmd_stats.go | 2 +- cmd/restic/global.go | 2 +- internal/fs/fs_reader_command.go | 4 ++-- internal/fs/fs_reader_command_test.go | 10 ++++---- 18 files changed, 58 insertions(+), 62 deletions(-) diff --git a/cmd/restic/cmd_backup.go b/cmd/restic/cmd_backup.go index 997e702e8..fe3ace448 100644 --- a/cmd/restic/cmd_backup.go +++ b/cmd/restic/cmd_backup.go @@ -586,7 +586,7 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, ter filename := path.Join("/", opts.StdinFilename) var source io.ReadCloser = os.Stdin if opts.StdinCommand { - source, err = fs.NewCommandReader(ctx, args, globalOptions.stderr) + source, err = fs.NewCommandReader(ctx, args, msg.E) if err != nil { return err } diff --git a/cmd/restic/cmd_backup_integration_test.go b/cmd/restic/cmd_backup_integration_test.go index 74a979024..ee9c495c1 100644 --- a/cmd/restic/cmd_backup_integration_test.go +++ b/cmd/restic/cmd_backup_integration_test.go @@ -262,22 +262,19 @@ func TestBackupNonExistingFile(t *testing.T) { testSetupBackupData(t, env) - _ = withRestoreGlobalOptions(func() error { - globalOptions.stderr = io.Discard + env.gopts.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) { diff --git a/cmd/restic/cmd_cache.go b/cmd/restic/cmd_cache.go index f94c23747..f3f82954d 100644 --- a/cmd/restic/cmd_cache.go +++ b/cmd/restic/cmd_cache.go @@ -163,7 +163,7 @@ func runCache(opts CacheOptions, gopts GlobalOptions, args []string, term ui.Ter }) } - _ = tab.Write(globalOptions.stdout) + _ = tab.Write(gopts.stdout) printer.S("%d cache dirs in %s", len(dirs), cachedir) return nil diff --git a/cmd/restic/cmd_check_integration_test.go b/cmd/restic/cmd_check_integration_test.go index 59a0f7498..004f54b6b 100644 --- a/cmd/restic/cmd_check_integration_test.go +++ b/cmd/restic/cmd_check_integration_test.go @@ -1,7 +1,6 @@ package main import ( - "bytes" "context" "testing" @@ -25,15 +24,15 @@ func testRunCheckMustFail(t testing.TB, gopts GlobalOptions) { } func testRunCheckOutput(gopts GlobalOptions, checkUnused bool) (string, error) { - buf := bytes.NewBuffer(nil) - gopts.stdout = buf - err := withTermStatus(gopts, func(ctx context.Context, term ui.Terminal) error { - opts := CheckOptions{ - ReadData: true, - CheckUnused: checkUnused, - } - _, err := runCheck(context.TODO(), opts, gopts, nil, term) - return err + buf, err := withCaptureStdout(gopts, func(gopts GlobalOptions) error { + return withTermStatus(gopts, func(ctx context.Context, term ui.Terminal) error { + opts := CheckOptions{ + ReadData: true, + CheckUnused: checkUnused, + } + _, err := runCheck(context.TODO(), opts, gopts, nil, term) + return err + }) }) return buf.String(), err } diff --git a/cmd/restic/cmd_debug.go b/cmd/restic/cmd_debug.go index 8dcc3cc8d..a41a0d087 100644 --- a/cmd/restic/cmd_debug.go +++ b/cmd/restic/cmd_debug.go @@ -204,20 +204,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.stdout, printer) case "snapshots": - return debugPrintSnapshots(ctx, repo, globalOptions.stdout) + return debugPrintSnapshots(ctx, repo, gopts.stdout) case "packs": - return printPacks(ctx, repo, globalOptions.stdout, printer) + return printPacks(ctx, repo, gopts.stdout, printer) case "all": printer.S("snapshots:") - err := debugPrintSnapshots(ctx, repo, globalOptions.stdout) + err := debugPrintSnapshots(ctx, repo, gopts.stdout) if err != nil { return err } printer.S("indexes:") - err = dumpIndexes(ctx, repo, globalOptions.stdout, printer) + err = dumpIndexes(ctx, repo, gopts.stdout, printer) if err != nil { return err } diff --git a/cmd/restic/cmd_diff.go b/cmd/restic/cmd_diff.go index 2aa328773..7650a225f 100644 --- a/cmd/restic/cmd_diff.go +++ b/cmd/restic/cmd_diff.go @@ -426,7 +426,7 @@ func runDiff(ctx context.Context, opts DiffOptions, gopts GlobalOptions, args [] } if gopts.JSON { - enc := json.NewEncoder(globalOptions.stdout) + enc := json.NewEncoder(gopts.stdout) c.printChange = func(change *Change) { err := enc.Encode(change) if err != nil { @@ -460,7 +460,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.stdout).Encode(stats) if err != nil { printer.E("JSON encode failed: %v", err) } diff --git a/cmd/restic/cmd_forget.go b/cmd/restic/cmd_forget.go index 8523b3b2d..42739cdf0 100644 --- a/cmd/restic/cmd_forget.go +++ b/cmd/restic/cmd_forget.go @@ -253,7 +253,7 @@ func runForget(ctx context.Context, opts ForgetOptions, pruneOptions PruneOption } if gopts.Verbose >= 1 && !gopts.JSON { - err = PrintSnapshotGroupHeader(globalOptions.stdout, k) + err = PrintSnapshotGroupHeader(gopts.stdout, k) if err != nil { return err } @@ -276,7 +276,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.stdout, keep, reasons, opts.Compact); err != nil { return err } printer.P("\n") @@ -285,7 +285,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.stdout, remove, nil, opts.Compact); err != nil { return err } printer.P("\n") @@ -330,7 +330,7 @@ func runForget(ctx context.Context, opts ForgetOptions, pruneOptions PruneOption } if gopts.JSON && len(jsonGroups) > 0 { - err = printJSONForget(globalOptions.stdout, jsonGroups) + err = printJSONForget(gopts.stdout, jsonGroups) if err != nil { return err } diff --git a/cmd/restic/cmd_generate.go b/cmd/restic/cmd_generate.go index cf7b3c1ea..328b937ad 100644 --- a/cmd/restic/cmd_generate.go +++ b/cmd/restic/cmd_generate.go @@ -75,7 +75,7 @@ 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) { +func writeCompletion(filename string, shell string, generate func(w io.Writer) error, printer progress.Printer, gopts GlobalOptions) (err error) { if terminal.StdoutIsTerminal() { printer.P("writing %s completion file to %v", shell, filename) } @@ -89,7 +89,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.stdout } err = generate(outWriter) @@ -136,28 +136,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 } diff --git a/cmd/restic/cmd_init.go b/cmd/restic/cmd_init.go index b1bfc637e..8e8488355 100644 --- a/cmd/restic/cmd_init.go +++ b/cmd/restic/cmd_init.go @@ -134,7 +134,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.stdout).Encode(status) } return nil diff --git a/cmd/restic/cmd_key_list.go b/cmd/restic/cmd_key_list.go index 897601675..bdc0a7f82 100644 --- a/cmd/restic/cmd_key_list.go +++ b/cmd/restic/cmd_key_list.go @@ -97,7 +97,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.stdout).Encode(keys) } tab := table.New() @@ -110,5 +110,5 @@ func listKeys(ctx context.Context, s *repository.Repository, gopts GlobalOptions tab.AddRow(key) } - return tab.Write(globalOptions.stdout) + return tab.Write(gopts.stdout) } diff --git a/cmd/restic/cmd_ls.go b/cmd/restic/cmd_ls.go index 532d2f4dd..b56f0b6e3 100644 --- a/cmd/restic/cmd_ls.go +++ b/cmd/restic/cmd_ls.go @@ -384,11 +384,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.stdout), } } else if opts.Ncdu { printer = &ncduLsPrinter{ - out: globalOptions.stdout, + out: gopts.stdout, } } else { printer = &textLsPrinter{ diff --git a/cmd/restic/cmd_repair_index_integration_test.go b/cmd/restic/cmd_repair_index_integration_test.go index 6d1e81eee..c03c8f3d0 100644 --- a/cmd/restic/cmd_repair_index_integration_test.go +++ b/cmd/restic/cmd_repair_index_integration_test.go @@ -19,7 +19,7 @@ import ( 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 + gopts.stdout = io.Discard return runRebuildIndex(context.TODO(), RepairIndexOptions{}, gopts, term) }) })) @@ -133,7 +133,7 @@ func TestRebuildIndexFailsOnAppendOnly(t *testing.T) { return &appendOnlyBackend{r}, nil } return withTermStatus(env.gopts, func(ctx context.Context, term ui.Terminal) error { - globalOptions.stdout = io.Discard + env.gopts.stdout = io.Discard return runRebuildIndex(context.TODO(), RepairIndexOptions{}, env.gopts, term) }) }) diff --git a/cmd/restic/cmd_restore_integration_test.go b/cmd/restic/cmd_restore_integration_test.go index 0f02ec385..0b6443529 100644 --- a/cmd/restic/cmd_restore_integration_test.go +++ b/cmd/restic/cmd_restore_integration_test.go @@ -338,7 +338,7 @@ func TestRestoreWithPermissionFailure(t *testing.T) { snapshots := testListSnapshots(t, env.gopts, 1) _ = withRestoreGlobalOptions(func() error { - globalOptions.stderr = io.Discard + env.gopts.stderr = io.Discard testRunRestore(t, env.gopts, filepath.Join(env.base, "restore"), snapshots[0].String()) return nil }) diff --git a/cmd/restic/cmd_snapshots.go b/cmd/restic/cmd_snapshots.go index 5ce82c996..8d921194e 100644 --- a/cmd/restic/cmd_snapshots.go +++ b/cmd/restic/cmd_snapshots.go @@ -105,7 +105,7 @@ func runSnapshots(ctx context.Context, opts SnapshotOptions, gopts GlobalOptions } if gopts.JSON { - err := printSnapshotGroupJSON(globalOptions.stdout, snapshotGroups, grouped) + err := printSnapshotGroupJSON(gopts.stdout, snapshotGroups, grouped) if err != nil { printer.E("error printing snapshots: %v", err) } @@ -118,12 +118,12 @@ func runSnapshots(ctx context.Context, opts SnapshotOptions, gopts GlobalOptions } if grouped { - err := PrintSnapshotGroupHeader(globalOptions.stdout, k) + err := PrintSnapshotGroupHeader(gopts.stdout, k) if err != nil { return err } } - err := PrintSnapshots(globalOptions.stdout, list, nil, opts.Compact) + err := PrintSnapshots(gopts.stdout, list, nil, opts.Compact) if err != nil { return err } diff --git a/cmd/restic/cmd_stats.go b/cmd/restic/cmd_stats.go index 00f885ffa..f25ed39bb 100644 --- a/cmd/restic/cmd_stats.go +++ b/cmd/restic/cmd_stats.go @@ -171,7 +171,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.stdout).Encode(stats) if err != nil { return fmt.Errorf("encoding output: %v", err) } diff --git a/cmd/restic/global.go b/cmd/restic/global.go index 6838b1227..b218eacde 100644 --- a/cmd/restic/global.go +++ b/cmd/restic/global.go @@ -476,7 +476,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) } diff --git a/internal/fs/fs_reader_command.go b/internal/fs/fs_reader_command.go index 9e6674e8f..a1371fd1e 100644 --- a/internal/fs/fs_reader_command.go +++ b/internal/fs/fs_reader_command.go @@ -28,7 +28,7 @@ type CommandReader struct { alreadyClosedReadErr error } -func NewCommandReader(ctx context.Context, args []string, logOutput io.Writer) (*CommandReader, error) { +func NewCommandReader(ctx context.Context, args []string, errorOutput func(msg string, args ...interface{})) (*CommandReader, error) { if len(args) == 0 { return nil, fmt.Errorf("no command was specified as argument") } @@ -48,7 +48,7 @@ func NewCommandReader(ctx context.Context, args []string, logOutput io.Writer) ( go func() { sc := bufio.NewScanner(stderr) for sc.Scan() { - _, _ = fmt.Fprintf(logOutput, "subprocess %v: %v\n", command.Args[0], sc.Text()) + errorOutput("subprocess %v: %v", command.Args[0], sc.Text()) } }() diff --git a/internal/fs/fs_reader_command_test.go b/internal/fs/fs_reader_command_test.go index 8f0d17b1e..5f27e9590 100644 --- a/internal/fs/fs_reader_command_test.go +++ b/internal/fs/fs_reader_command_test.go @@ -12,7 +12,7 @@ import ( ) func TestCommandReaderSuccess(t *testing.T) { - reader, err := fs.NewCommandReader(context.TODO(), []string{"true"}, io.Discard) + reader, err := fs.NewCommandReader(context.TODO(), []string{"true"}, func(msg string, args ...interface{}) {}) test.OK(t, err) _, err = io.Copy(io.Discard, reader) @@ -22,7 +22,7 @@ func TestCommandReaderSuccess(t *testing.T) { } func TestCommandReaderFail(t *testing.T) { - reader, err := fs.NewCommandReader(context.TODO(), []string{"false"}, io.Discard) + reader, err := fs.NewCommandReader(context.TODO(), []string{"false"}, func(msg string, args ...interface{}) {}) test.OK(t, err) _, err = io.Copy(io.Discard, reader) @@ -30,17 +30,17 @@ func TestCommandReaderFail(t *testing.T) { } func TestCommandReaderInvalid(t *testing.T) { - _, err := fs.NewCommandReader(context.TODO(), []string{"w54fy098hj7fy5twijouytfrj098y645wr"}, io.Discard) + _, err := fs.NewCommandReader(context.TODO(), []string{"w54fy098hj7fy5twijouytfrj098y645wr"}, func(msg string, args ...interface{}) {}) test.Assert(t, err != nil, "missing error") } func TestCommandReaderEmptyArgs(t *testing.T) { - _, err := fs.NewCommandReader(context.TODO(), []string{}, io.Discard) + _, err := fs.NewCommandReader(context.TODO(), []string{}, func(msg string, args ...interface{}) {}) test.Assert(t, err != nil, "missing error") } func TestCommandReaderOutput(t *testing.T) { - reader, err := fs.NewCommandReader(context.TODO(), []string{"echo", "hello world"}, io.Discard) + reader, err := fs.NewCommandReader(context.TODO(), []string{"echo", "hello world"}, func(msg string, args ...interface{}) {}) test.OK(t, err) var buf bytes.Buffer From 3e1632c412bfe9a19e744ce444d5222c570b0cef Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Sun, 14 Sep 2025 16:09:44 +0200 Subject: [PATCH 02/25] reduce os.stdout / os.stderr usage in tests --- cmd/restic/cmd_backup_integration_test.go | 16 ++++++++-------- cmd/restic/cmd_copy_integration_test.go | 6 +++--- cmd/restic/cmd_restore_integration_test.go | 2 +- cmd/restic/integration_helpers_test.go | 21 +++++++++------------ 4 files changed, 21 insertions(+), 24 deletions(-) diff --git a/cmd/restic/cmd_backup_integration_test.go b/cmd/restic/cmd_backup_integration_test.go index ee9c495c1..ff4998991 100644 --- a/cmd/restic/cmd_backup_integration_test.go +++ b/cmd/restic/cmd_backup_integration_test.go @@ -56,13 +56,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 +74,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 +85,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) } @@ -435,13 +435,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) } @@ -451,7 +451,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) } @@ -562,7 +562,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")) diff --git a/cmd/restic/cmd_copy_integration_test.go b/cmd/restic/cmd_copy_integration_test.go index 2dc27050f..27f67bc73 100644 --- a/cmd/restic/cmd_copy_integration_test.go +++ b/cmd/restic/cmd_copy_integration_test.go @@ -48,8 +48,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 +72,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 diff --git a/cmd/restic/cmd_restore_integration_test.go b/cmd/restic/cmd_restore_integration_test.go index 0b6443529..09d5f4d9e 100644 --- a/cmd/restic/cmd_restore_integration_test.go +++ b/cmd/restic/cmd_restore_integration_test.go @@ -257,7 +257,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) } diff --git a/cmd/restic/integration_helpers_test.go b/cmd/restic/integration_helpers_test.go index 35eb5c253..7367bbe70 100644 --- a/cmd/restic/integration_helpers_test.go +++ b/cmd/restic/integration_helpers_test.go @@ -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()) @@ -391,19 +391,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 } From 13f743e26b492c3dfb5f5a2781a37e9f88e26e23 Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Sun, 14 Sep 2025 16:10:35 +0200 Subject: [PATCH 03/25] profiling: inject os.Stderr instead of directly using it --- cmd/restic/global_debug.go | 24 +++++++++++++----------- cmd/restic/global_release.go | 8 ++++++-- cmd/restic/main.go | 2 +- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/cmd/restic/global_debug.go b/cmd/restic/global_debug.go index e536bffea..ff1ac3a57 100644 --- a/cmd/restic/global_debug.go +++ b/cmd/restic/global_debug.go @@ -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 diff --git a/cmd/restic/global_release.go b/cmd/restic/global_release.go index 2c4f28b13..1e1e2147c 100644 --- a/cmd/restic/global_release.go +++ b/cmd/restic/global_release.go @@ -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 } diff --git a/cmd/restic/main.go b/cmd/restic/main.go index a82d92af3..f373a418d 100644 --- a/cmd/restic/main.go +++ b/cmd/restic/main.go @@ -100,7 +100,7 @@ The full documentation can be found at https://restic.readthedocs.io/ . registerDebugCommand(cmd) registerMountCommand(cmd) registerSelfUpdateCommand(cmd) - registerProfiling(cmd) + registerProfiling(cmd, os.Stderr) return cmd } From 4dc71f24c578fcff31a08f5dfb0b86798976bea8 Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Sun, 14 Sep 2025 16:13:21 +0200 Subject: [PATCH 04/25] backends: pass error logger to backends --- cmd/restic/global.go | 6 +-- internal/backend/azure/azure.go | 4 +- internal/backend/azure/azure_test.go | 6 +-- internal/backend/b2/b2.go | 4 +- internal/backend/cache/backend.go | 8 ++-- internal/backend/cache/backend_test.go | 12 +++--- internal/backend/cache/cache.go | 4 +- internal/backend/gs/gs.go | 4 +- internal/backend/limiter/limiter_backend.go | 6 +-- internal/backend/local/layout_test.go | 2 +- internal/backend/local/local.go | 4 +- internal/backend/local/local_internal_test.go | 2 +- internal/backend/local/local_test.go | 2 +- internal/backend/location/registry.go | 40 +++++++++---------- internal/backend/mem/mem_backend.go | 4 +- internal/backend/rclone/backend.go | 20 +++++----- internal/backend/rclone/internal_test.go | 4 +- internal/backend/rest/rest.go | 6 +-- internal/backend/rest/rest_int_test.go | 2 +- internal/backend/rest/rest_test.go | 2 +- internal/backend/s3/s3.go | 4 +- internal/backend/s3/s3_test.go | 4 +- internal/backend/sftp/layout_test.go | 2 +- internal/backend/sftp/sftp.go | 12 +++--- internal/backend/swift/swift.go | 2 +- internal/backend/test/suite.go | 8 ++-- internal/backend/test/tests.go | 2 +- internal/repository/raw_test.go | 2 +- internal/repository/repository.go | 4 +- internal/repository/repository_test.go | 6 +-- internal/repository/testing.go | 4 +- 31 files changed, 96 insertions(+), 96 deletions(-) diff --git a/cmd/restic/global.go b/cmd/restic/global.go index b218eacde..391e1edf6 100644 --- a/cmd/restic/global.go +++ b/cmd/restic/global.go @@ -414,7 +414,7 @@ func OpenRepository(ctx context.Context, opts GlobalOptions, printer progress.Pr } // start using the cache - s.UseCache(c) + s.UseCache(c, printer.E) oldCacheDirs, err := cache.Old(c.Base) if err != nil { @@ -492,9 +492,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) { diff --git a/internal/backend/azure/azure.go b/internal/backend/azure/azure.go index c2dc336cd..46df66113 100644 --- a/internal/backend/azure/azure.go +++ b/internal/backend/azure/azure.go @@ -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 { diff --git a/internal/backend/azure/azure_test.go b/internal/backend/azure/azure_test.go index adafb6b03..f755b21d2 100644 --- a/internal/backend/azure/azure_test.go +++ b/internal/backend/azure/azure_test.go @@ -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) } diff --git a/internal/backend/b2/b2.go b/internal/backend/b2/b2.go index 6ebba570e..6e751479d 100644 --- a/internal/backend/b2/b2.go +++ b/internal/backend/b2/b2.go @@ -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) diff --git a/internal/backend/cache/backend.go b/internal/backend/cache/backend.go index f323b1663..8a561c411 100644 --- a/internal/backend/cache/backend.go +++ b/internal/backend/cache/backend.go @@ -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 diff --git a/internal/backend/cache/backend_test.go b/internal/backend/cache/backend_test.go index 7f83e40cb..12d4fa028 100644 --- a/internal/backend/cache/backend_test.go +++ b/internal/backend/cache/backend_test.go @@ -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 }) diff --git a/internal/backend/cache/cache.go b/internal/backend/cache/cache.go index 2893df501..d1bfa47a0 100644 --- a/internal/backend/cache/cache.go +++ b/internal/backend/cache/cache.go @@ -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. diff --git a/internal/backend/gs/gs.go b/internal/backend/gs/gs.go index 9ea5fca2b..aaf1ae33c 100644 --- a/internal/backend/gs/gs.go +++ b/internal/backend/gs/gs.go @@ -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 diff --git a/internal/backend/limiter/limiter_backend.go b/internal/backend/limiter/limiter_backend.go index ac1a4188a..945777688 100644 --- a/internal/backend/limiter/limiter_backend.go +++ b/internal/backend/limiter/limiter_backend.go @@ -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 } diff --git a/internal/backend/local/layout_test.go b/internal/backend/local/layout_test.go index cac89e552..56219f608 100644 --- a/internal/backend/local/layout_test.go +++ b/internal/backend/local/layout_test.go @@ -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) } diff --git a/internal/backend/local/local.go b/internal/backend/local/local.go index 861371b3e..1c1a13470 100644 --- a/internal/backend/local/local.go +++ b/internal/backend/local/local.go @@ -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) diff --git a/internal/backend/local/local_internal_test.go b/internal/backend/local/local_internal_test.go index 6cad26d0a..6a7616031 100644 --- a/internal/backend/local/local_internal_test.go +++ b/internal/backend/local/local_internal_test.go @@ -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()) diff --git a/internal/backend/local/local_test.go b/internal/backend/local/local_test.go index 2a8b626d4..f47f27920 100644 --- a/internal/backend/local/local_test.go +++ b/internal/backend/local/local_test.go @@ -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) } diff --git a/internal/backend/location/registry.go b/internal/backend/location/registry.go index b50371add..c0761e76b 100644 --- a/internal/backend/location/registry.go +++ b/internal/backend/location/registry.go @@ -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) }, } } diff --git a/internal/backend/mem/mem_backend.go b/internal/backend/mem/mem_backend.go index 3064a3b88..6ff38867c 100644 --- a/internal/backend/mem/mem_backend.go +++ b/internal/backend/mem/mem_backend.go @@ -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 }, ) diff --git a/internal/backend/rclone/backend.go b/internal/backend/rclone/backend.go index 5ee330a69..afc516355 100644 --- a/internal/backend/rclone/backend.go +++ b/internal/backend/rclone/backend.go @@ -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 diff --git a/internal/backend/rclone/internal_test.go b/internal/backend/rclone/internal_test.go index 34d52885e..adc251557 100644 --- a/internal/backend/rclone/internal_test.go +++ b/internal/backend/rclone/internal_test.go @@ -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 diff --git a/internal/backend/rest/rest.go b/internal/backend/rest/rest.go index 5776f284f..d0158ab58 100644 --- a/internal/backend/rest/rest.go +++ b/internal/backend/rest/rest.go @@ -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 } diff --git a/internal/backend/rest/rest_int_test.go b/internal/backend/rest/rest_int_test.go index 926b30ed4..e7b9eb965 100644 --- a/internal/backend/rest/rest_int_test.go +++ b/internal/backend/rest/rest_int_test.go @@ -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) } diff --git a/internal/backend/rest/rest_test.go b/internal/backend/rest/rest_test.go index 50560f66d..109259b92 100644 --- a/internal/backend/rest/rest_test.go +++ b/internal/backend/rest/rest_test.go @@ -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) } }() diff --git a/internal/backend/s3/s3.go b/internal/backend/s3/s3.go index 3653c827c..cac72144d 100644 --- a/internal/backend/s3/s3.go +++ b/internal/backend/s3/s3.go @@ -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") diff --git a/internal/backend/s3/s3_test.go b/internal/backend/s3/s3_test.go index 40f7cb0b3..b026ce427 100644 --- a/internal/backend/s3/s3_test.go +++ b/internal/backend/s3/s3_test.go @@ -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) diff --git a/internal/backend/sftp/layout_test.go b/internal/backend/sftp/layout_test.go index 9e143d4fd..c87e3a9d3 100644 --- a/internal/backend/sftp/layout_test.go +++ b/internal/backend/sftp/layout_test.go @@ -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) } diff --git a/internal/backend/sftp/sftp.go b/internal/backend/sftp/sftp.go index 174877b26..269a9f41a 100644 --- a/internal/backend/sftp/sftp.go +++ b/internal/backend/sftp/sftp.go @@ -51,7 +51,7 @@ func NewFactory() location.Factory { return location.NewLimitedBackendFactory("sftp", ParseConfig, location.NoPassword, limiter.WrapBackendConstructor(Create), limiter.WrapBackendConstructor(Open)) } -func startClient(cfg Config) (*SFTP, error) { +func startClient(cfg Config, errorLog func(string, ...interface{})) (*SFTP, error) { program, args, err := buildSSHCommand(cfg) if err != nil { return nil, err @@ -71,7 +71,7 @@ func startClient(cfg Config) (*SFTP, error) { go func() { sc := bufio.NewScanner(stderr) for sc.Scan() { - fmt.Fprintf(os.Stderr, "subprocess %v: %v\n", program, sc.Text()) + errorLog("subprocess %v: %v\n", program, sc.Text()) } }() @@ -144,10 +144,10 @@ func (r *SFTP) clientError() error { // Open opens an sftp backend as described by the config by running // "ssh" with the appropriate arguments (or cfg.Command, if set). -func Open(_ context.Context, cfg Config) (*SFTP, error) { +func Open(_ context.Context, cfg Config, errorLog func(string, ...interface{})) (*SFTP, error) { debug.Log("open backend with config %#v", cfg) - sftp, err := startClient(cfg) + sftp, err := startClient(cfg, errorLog) if err != nil { debug.Log("unable to start program: %v", err) return nil, err @@ -240,8 +240,8 @@ func buildSSHCommand(cfg Config) (cmd string, args []string, err error) { // Create creates an sftp backend as described by the config by running "ssh" // with the appropriate arguments (or cfg.Command, if set). -func Create(ctx context.Context, cfg Config) (*SFTP, error) { - sftp, err := startClient(cfg) +func Create(ctx context.Context, cfg Config, errorLog func(string, ...interface{})) (*SFTP, error) { + sftp, err := startClient(cfg, errorLog) if err != nil { debug.Log("unable to start program: %v", err) return nil, err diff --git a/internal/backend/swift/swift.go b/internal/backend/swift/swift.go index ae6a13462..155b5041e 100644 --- a/internal/backend/swift/swift.go +++ b/internal/backend/swift/swift.go @@ -42,7 +42,7 @@ func NewFactory() location.Factory { // Open opens the swift backend at a container in region. The container is // created if it does not exist yet. -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("config %#v", cfg) be := &beSwift{ diff --git a/internal/backend/test/suite.go b/internal/backend/test/suite.go index ad8eb4c5d..86acc5cb5 100644 --- a/internal/backend/test/suite.go +++ b/internal/backend/test/suite.go @@ -155,13 +155,13 @@ func (s *Suite[C]) RunBenchmarks(b *testing.B) { s.cleanup(b) } -func (s *Suite[C]) createOrError() (backend.Backend, error) { +func (s *Suite[C]) createOrError(t testing.TB) (backend.Backend, error) { tr, err := backend.Transport(backend.TransportOptions{}) if err != nil { return nil, fmt.Errorf("cannot create transport for tests: %v", err) } - be, err := s.Factory.Create(context.TODO(), s.Config, tr, nil) + be, err := s.Factory.Create(context.TODO(), s.Config, tr, nil, t.Logf) if err != nil { return nil, err } @@ -179,7 +179,7 @@ func (s *Suite[C]) createOrError() (backend.Backend, error) { } func (s *Suite[C]) create(t testing.TB) backend.Backend { - be, err := s.createOrError() + be, err := s.createOrError(t) if err != nil { t.Fatal(err) } @@ -192,7 +192,7 @@ func (s *Suite[C]) open(t testing.TB) backend.Backend { t.Fatalf("cannot create transport for tests: %v", err) } - be, err := s.Factory.Open(context.TODO(), s.Config, tr, nil) + be, err := s.Factory.Open(context.TODO(), s.Config, tr, nil, func(string, ...interface{}) {}) if err != nil { t.Fatal(err) } diff --git a/internal/backend/test/tests.go b/internal/backend/test/tests.go index b3deba8da..5c223412b 100644 --- a/internal/backend/test/tests.go +++ b/internal/backend/test/tests.go @@ -79,7 +79,7 @@ func (s *Suite[C]) TestCreateWithConfig(t *testing.T) { store(t, b, backend.ConfigFile, []byte("test config")) // now create the backend again, this must fail - _, err = s.createOrError() + _, err = s.createOrError(t) if err == nil { t.Fatalf("expected error not found for creating a backend with an existing config file") } diff --git a/internal/repository/raw_test.go b/internal/repository/raw_test.go index ac65a8dc8..81fc25250 100644 --- a/internal/repository/raw_test.go +++ b/internal/repository/raw_test.go @@ -87,7 +87,7 @@ func TestLoadRawBrokenWithCache(t *testing.T) { c := cache.TestNewCache(t) repo, err := repository.New(b, repository.Options{}) rtest.OK(t, err) - repo.UseCache(c) + repo.UseCache(c, t.Logf) data := rtest.Random(23, 10*KiB) id := restic.Hash(data) diff --git a/internal/repository/repository.go b/internal/repository/repository.go index bd2a0b316..39cb5e9c3 100644 --- a/internal/repository/repository.go +++ b/internal/repository/repository.go @@ -161,13 +161,13 @@ func (r *Repository) packSize() uint { } // UseCache replaces the backend with the wrapped cache. -func (r *Repository) UseCache(c *cache.Cache) { +func (r *Repository) UseCache(c *cache.Cache, errorLog func(string, ...interface{})) { if c == nil { return } debug.Log("using cache") r.cache = c - r.be = c.Wrap(r.be) + r.be = c.Wrap(r.be, errorLog) } func (r *Repository) Cache() *cache.Cache { diff --git a/internal/repository/repository_test.go b/internal/repository/repository_test.go index 2f2d3d1ac..3496b9a2d 100644 --- a/internal/repository/repository_test.go +++ b/internal/repository/repository_test.go @@ -207,7 +207,7 @@ func TestLoadBlobBroken(t *testing.T) { // setup cache after saving the blob to make sure that the damageOnceBackend damages the cached data c := cache.TestNewCache(t) - repo.UseCache(c) + repo.UseCache(c, t.Logf) data, err := repo.LoadBlob(context.TODO(), restic.TreeBlob, id, nil) rtest.OK(t, err) @@ -355,7 +355,7 @@ func TestRepositoryLoadUnpackedRetryBroken(t *testing.T) { repodir, cleanup := rtest.Env(t, repoFixture) defer cleanup() - be, err := local.Open(context.TODO(), local.Config{Path: repodir, Connections: 2}) + be, err := local.Open(context.TODO(), local.Config{Path: repodir, Connections: 2}, t.Logf) rtest.OK(t, err) repo := repository.TestOpenBackend(t, &damageOnceBackend{Backend: be}) @@ -446,7 +446,7 @@ func TestListPack(t *testing.T) { // setup cache after saving the blob to make sure that the damageOnceBackend damages the cached data c := cache.TestNewCache(t) - repo.UseCache(c) + repo.UseCache(c, t.Logf) // Forcibly cache pack file packID := repo.LookupBlob(restic.TreeBlob, id)[0].PackID diff --git a/internal/repository/testing.go b/internal/repository/testing.go index 5a464e44f..988daba19 100644 --- a/internal/repository/testing.go +++ b/internal/repository/testing.go @@ -91,7 +91,7 @@ func TestRepositoryWithVersion(t testing.TB, version uint) (*Repository, restic. if dir != "" { _, err := os.Stat(dir) if err != nil { - lbe, err := local.Create(context.TODO(), local.Config{Path: dir}) + lbe, err := local.Create(context.TODO(), local.Config{Path: dir}, t.Logf) if err != nil { t.Fatalf("error creating local backend at %v: %v", dir, err) } @@ -115,7 +115,7 @@ func TestFromFixture(t testing.TB, repoFixture string) (*Repository, backend.Bac // TestOpenLocal opens a local repository. func TestOpenLocal(t testing.TB, dir string) (*Repository, backend.Backend) { var be backend.Backend - be, err := local.Open(context.TODO(), local.Config{Path: dir, Connections: 2}) + be, err := local.Open(context.TODO(), local.Config{Path: dir, Connections: 2}, t.Logf) if err != nil { t.Fatal(err) } From b6c50662dad1b3c628a1db3d4d3ef8663204f442 Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Sun, 14 Sep 2025 16:15:43 +0200 Subject: [PATCH 05/25] repository: don't ignore cache clearing error --- internal/repository/repository.go | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/internal/repository/repository.go b/internal/repository/repository.go index 39cb5e9c3..d5da0ca5f 100644 --- a/internal/repository/repository.go +++ b/internal/repository/repository.go @@ -6,7 +6,6 @@ import ( "fmt" "io" "math" - "os" "runtime" "sort" "sync" @@ -755,12 +754,7 @@ func (r *Repository) prepareCache() error { packs := r.idx.Packs(restic.NewIDSet()) // clear old packs - err := r.cache.Clear(restic.PackFile, packs) - if err != nil { - fmt.Fprintf(os.Stderr, "error clearing pack files in cache: %v\n", err) - } - - return nil + return r.cache.Clear(restic.PackFile, packs) } // SearchKey finds a key with the supplied password, afterwards the config is From c745e4221eadd3b1d55de2545fa2a9aec1acaa73 Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Sun, 14 Sep 2025 16:16:15 +0200 Subject: [PATCH 06/25] termstatus: use errWriter instead of os.Stderr --- internal/ui/termstatus/status.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/internal/ui/termstatus/status.go b/internal/ui/termstatus/status.go index b54ee6d80..dc47c880a 100644 --- a/internal/ui/termstatus/status.go +++ b/internal/ui/termstatus/status.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "io" - "os" "strings" "github.com/restic/restic/internal/terminal" @@ -128,7 +127,7 @@ func (t *Terminal) run(ctx context.Context) { } if _, err := io.WriteString(dst, msg.line); err != nil { - fmt.Fprintf(os.Stderr, "write failed: %v\n", err) + _, _ = fmt.Fprintf(t.errWriter, "write failed: %v\n", err) continue } @@ -164,7 +163,7 @@ func (t *Terminal) writeStatus(status []string) { _, err := t.wr.Write([]byte(line)) if err != nil { - fmt.Fprintf(os.Stderr, "write failed: %v\n", err) + _, _ = fmt.Fprintf(t.errWriter, "write failed: %v\n", err) } } @@ -190,14 +189,14 @@ func (t *Terminal) runWithoutStatus(ctx context.Context) { } if _, err := io.WriteString(dst, msg.line); err != nil { - _, _ = fmt.Fprintf(os.Stderr, "write failed: %v\n", err) + _, _ = fmt.Fprintf(t.errWriter, "write failed: %v\n", err) } case stat := <-t.status: for _, line := range stat.lines { // Ensure that each message ends with exactly one newline. if _, err := fmt.Fprintln(t.wr, strings.TrimRight(line, "\n")); err != nil { - _, _ = fmt.Fprintf(os.Stderr, "write failed: %v\n", err) + _, _ = fmt.Fprintf(t.errWriter, "write failed: %v\n", err) } } } From 1ae2d08d1b92cb10cbc235d27d2440706cc9a80c Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Sun, 14 Sep 2025 17:58:52 +0200 Subject: [PATCH 07/25] termstatus: centralize OutputIsTerminal checks --- cmd/restic/cmd_backup.go | 2 +- cmd/restic/cmd_check.go | 1 + cmd/restic/cmd_dump.go | 11 ++++---- cmd/restic/cmd_generate.go | 5 +--- cmd/restic/cmd_restore.go | 2 +- cmd/restic/cmd_tag_integration_test.go | 5 +++- cmd/restic/global.go | 38 ++++++++++---------------- cmd/restic/progress.go | 9 +++--- internal/terminal/stdio.go | 15 +++------- internal/ui/message.go | 13 +++++++-- internal/ui/mock.go | 4 +++ internal/ui/progress/printer.go | 10 +++++++ internal/ui/terminal.go | 1 + internal/ui/termstatus/status.go | 37 ++++++++++++++++--------- 14 files changed, 85 insertions(+), 68 deletions(-) diff --git a/cmd/restic/cmd_backup.go b/cmd/restic/cmd_backup.go index fe3ace448..09dbab183 100644 --- a/cmd/restic/cmd_backup.go +++ b/cmd/restic/cmd_backup.go @@ -523,7 +523,7 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, ter progressPrinter = backup.NewTextProgress(term, gopts.verbosity) } progressReporter := backup.NewProgress(progressPrinter, - calculateProgressInterval(!gopts.Quiet, gopts.JSON)) + calculateProgressInterval(!gopts.Quiet, gopts.JSON, term.CanUpdateStatus())) defer progressReporter.Done() // rejectByNameFuncs collect functions that can reject items from the backup based on path only diff --git a/cmd/restic/cmd_check.go b/cmd/restic/cmd_check.go index 8b9937b69..5fddcfc0c 100644 --- a/cmd/restic/cmd_check.go +++ b/cmd/restic/cmd_check.go @@ -540,5 +540,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{}) {} diff --git a/cmd/restic/cmd_dump.go b/cmd/restic/cmd_dump.go index 6a2a2cce8..2cd15c06f 100644 --- a/cmd/restic/cmd_dump.go +++ b/cmd/restic/cmd_dump.go @@ -11,7 +11,6 @@ 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" @@ -180,7 +179,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 +203,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 } } diff --git a/cmd/restic/cmd_generate.go b/cmd/restic/cmd_generate.go index 328b937ad..477910507 100644 --- a/cmd/restic/cmd_generate.go +++ b/cmd/restic/cmd_generate.go @@ -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" @@ -76,9 +75,7 @@ func writeManpages(root *cobra.Command, dir string, printer progress.Printer) er } func writeCompletion(filename string, shell string, generate func(w io.Writer) error, printer progress.Printer, gopts GlobalOptions) (err error) { - if terminal.StdoutIsTerminal() { - printer.P("writing %s completion file to %v", shell, filename) - } + printer.PT("writing %s completion file to %v", shell, filename) var outWriter io.Writer if filename != "-" { var outFile *os.File diff --git a/cmd/restic/cmd_restore.go b/cmd/restic/cmd_restore.go index 888226c8e..d0bf76d85 100644 --- a/cmd/restic/cmd_restore.go +++ b/cmd/restic/cmd_restore.go @@ -165,7 +165,7 @@ func runRestore(ctx context.Context, opts RestoreOptions, gopts GlobalOptions, printer = restoreui.NewTextProgress(term, gopts.verbosity) } - progress := restoreui.NewProgress(printer, calculateProgressInterval(!gopts.Quiet, gopts.JSON)) + progress := restoreui.NewProgress(printer, calculateProgressInterval(!gopts.Quiet, gopts.JSON, term.CanUpdateStatus())) res := restorer.NewRestorer(repo, sn, restorer.Options{ DryRun: opts.DryRun, Sparse: opts.Sparse, diff --git a/cmd/restic/cmd_tag_integration_test.go b/cmd/restic/cmd_tag_integration_test.go index 5d58f89c4..cbb08c5bf 100644 --- a/cmd/restic/cmd_tag_integration_test.go +++ b/cmd/restic/cmd_tag_integration_test.go @@ -6,10 +6,13 @@ import ( "github.com/restic/restic/internal/restic" rtest "github.com/restic/restic/internal/test" + "github.com/restic/restic/internal/ui" ) func testRunTag(t testing.TB, opts TagOptions, gopts GlobalOptions) { - rtest.OK(t, runTag(context.TODO(), opts, gopts, nil, []string{})) + rtest.OK(t, withTermStatus(gopts, func(ctx context.Context, term ui.Terminal) error { + return runTag(context.TODO(), opts, gopts, term, []string{}) + })) } func TestTag(t *testing.T) { diff --git a/cmd/restic/global.go b/cmd/restic/global.go index 391e1edf6..e3419fedc 100644 --- a/cmd/restic/global.go +++ b/cmd/restic/global.go @@ -267,9 +267,7 @@ func ReadPassword(ctx context.Context, opts GlobalOptions, prompt string, printe if terminal.StdinIsTerminal() { password, err = terminal.ReadPassword(ctx, os.Stdin, os.Stderr, prompt) } else { - if terminal.StdoutIsTerminal() { - printer.P("reading repository password from stdin") - } + printer.PT("reading repository password from stdin") password, err = readPassword(os.Stdin) } @@ -385,19 +383,15 @@ 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 " + opts.Compression.String() + } + printer.PT("repository %v opened (version %v%s)", id, s.Config().Version, extra) if opts.NoCache { return s, nil @@ -409,8 +403,8 @@ func OpenRepository(ctx context.Context, opts GlobalOptions, printer progress.Pr 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 @@ -428,9 +422,7 @@ 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) - } + 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 +431,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 diff --git a/cmd/restic/progress.go b/cmd/restic/progress.go index 37ba0e623..f72a052ae 100644 --- a/cmd/restic/progress.go +++ b/cmd/restic/progress.go @@ -6,7 +6,6 @@ import ( "strconv" "time" - "github.com/restic/restic/internal/terminal" "github.com/restic/restic/internal/ui" "github.com/restic/restic/internal/ui/progress" ) @@ -14,7 +13,7 @@ import ( // 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 { +func calculateProgressInterval(show bool, json bool, canUpdateStatus bool) time.Duration { interval := time.Second / 60 fps, err := strconv.ParseFloat(os.Getenv("RESTIC_PROGRESS_FPS"), 64) if err == nil && fps > 0 { @@ -22,7 +21,7 @@ func calculateProgressInterval(show bool, json bool) time.Duration { fps = 60 } interval = time.Duration(float64(time.Second) / fps) - } else if !json && !terminal.StdoutCanUpdateStatus() || !show { + } else if !json && !canUpdateStatus || !show { interval = 0 } return interval @@ -33,7 +32,7 @@ func newTerminalProgressMax(show bool, max uint64, description string, term ui.T if !show { return nil } - interval := calculateProgressInterval(show, false) + interval := calculateProgressInterval(show, false, term.CanUpdateStatus()) return progress.NewCounter(interval, max, func(v uint64, max uint64, d time.Duration, final bool) { var status string @@ -65,7 +64,7 @@ func (t *terminalProgressPrinter) NewCounter(description string) *progress.Count } func (t *terminalProgressPrinter) NewCounterTerminalOnly(description string) *progress.Counter { - return newTerminalProgressMax(t.show && terminal.StdoutIsTerminal(), 0, description, t.term) + return newTerminalProgressMax(t.show && t.term.OutputIsTerminal(), 0, description, t.term) } func newTerminalProgressPrinter(json bool, verbosity uint, term ui.Terminal) progress.Printer { diff --git a/internal/terminal/stdio.go b/internal/terminal/stdio.go index 70e465ccb..1ee33e025 100644 --- a/internal/terminal/stdio.go +++ b/internal/terminal/stdio.go @@ -10,18 +10,11 @@ func StdinIsTerminal() bool { return term.IsTerminal(int(os.Stdin.Fd())) } -func StdoutIsTerminal() bool { +func OutputIsTerminal(fd uintptr) bool { // mintty on windows can use pipes which behave like a posix terminal, - // but which are not a terminal handle - return term.IsTerminal(int(os.Stdout.Fd())) || StdoutCanUpdateStatus() -} - -func StdoutCanUpdateStatus() bool { - return CanUpdateStatus(os.Stdout.Fd()) -} - -func StdoutWidth() int { - return Width(os.Stdout.Fd()) + // but which are not a terminal handle. Thus also check `CanUpdateStatus`, + // which is able to detect such pipes. + return term.IsTerminal(int(fd)) || CanUpdateStatus(fd) } func Width(fd uintptr) int { diff --git a/internal/ui/message.go b/internal/ui/message.go index 612fd72a2..d186c3859 100644 --- a/internal/ui/message.go +++ b/internal/ui/message.go @@ -30,8 +30,17 @@ func (m *Message) S(msg string, args ...interface{}) { m.term.Print(fmt.Sprintf(msg, args...)) } -// P prints a message if verbosity >= 1 (neither --quiet nor --verbose is specified), -// this is used for normal messages which are not errors. +// PT prints a message if verbosity >= 1 (neither --quiet nor --verbose is specified) +// and stdout points to a terminal. +// This is used for informational messages. +func (m *Message) PT(msg string, args ...interface{}) { + if m.term.OutputIsTerminal() && m.v >= 1 { + m.term.Print(fmt.Sprintf(msg, args...)) + } +} + +// P prints a message if verbosity >= 1 (neither --quiet nor --verbose is specified). +// This is used for normal messages which are not errors. func (m *Message) P(msg string, args ...interface{}) { if m.v >= 1 { m.term.Print(fmt.Sprintf(msg, args...)) diff --git a/internal/ui/mock.go b/internal/ui/mock.go index 36452f4be..fc5488792 100644 --- a/internal/ui/mock.go +++ b/internal/ui/mock.go @@ -28,3 +28,7 @@ func (m *MockTerminal) CanUpdateStatus() bool { func (m *MockTerminal) OutputRaw() io.Writer { return nil } + +func (m *MockTerminal) OutputIsTerminal() bool { + return true +} diff --git a/internal/ui/progress/printer.go b/internal/ui/progress/printer.go index 37d81f4d6..edcf7256b 100644 --- a/internal/ui/progress/printer.go +++ b/internal/ui/progress/printer.go @@ -19,6 +19,10 @@ type Printer interface { // that are not errors. The message is even printed if --quiet is specified. // Appends a newline if not present. S(msg string, args ...interface{}) + // PT prints a message if verbosity >= 1 (neither --quiet nor --verbose is specified) + // and stdout points to a terminal. + // This is used for informational messages. + PT(msg string, args ...interface{}) // P prints a message if verbosity >= 1 (neither --quiet nor --verbose is specified), // this is used for normal messages which are not errors. Appends a newline if not present. P(msg string, args ...interface{}) @@ -47,6 +51,8 @@ func (*NoopPrinter) E(_ string, _ ...interface{}) {} func (*NoopPrinter) S(_ string, _ ...interface{}) {} +func (*NoopPrinter) PT(_ string, _ ...interface{}) {} + func (*NoopPrinter) P(_ string, _ ...interface{}) {} func (*NoopPrinter) V(_ string, _ ...interface{}) {} @@ -82,6 +88,10 @@ func (p *TestPrinter) S(msg string, args ...interface{}) { p.t.Logf("stdout: "+msg, args...) } +func (p *TestPrinter) PT(msg string, args ...interface{}) { + p.t.Logf("stdout(terminal): "+msg, args...) +} + func (p *TestPrinter) P(msg string, args ...interface{}) { p.t.Logf("print: "+msg, args...) } diff --git a/internal/ui/terminal.go b/internal/ui/terminal.go index 262f1bcf7..845e36508 100644 --- a/internal/ui/terminal.go +++ b/internal/ui/terminal.go @@ -17,4 +17,5 @@ type Terminal interface { // other option. Must not be used in combination with Print, Error, SetStatus // or any other method that writes to the terminal. OutputRaw() io.Writer + OutputIsTerminal() bool } diff --git a/internal/ui/termstatus/status.go b/internal/ui/termstatus/status.go index dc47c880a..f1cbd7ef4 100644 --- a/internal/ui/termstatus/status.go +++ b/internal/ui/termstatus/status.go @@ -16,13 +16,14 @@ var _ ui.Terminal = &Terminal{} // updated. When the output is redirected to a file, the status lines are not // printed. type Terminal struct { - wr io.Writer - fd uintptr - errWriter io.Writer - msg chan message - status chan status - canUpdateStatus bool - lastStatusLen int + wr io.Writer + fd uintptr + errWriter io.Writer + msg chan message + status chan status + outputIsTerminal bool + canUpdateStatus bool + lastStatusLen int // will be closed when the goroutine which runs Run() terminates, so it'll // yield a default value immediately @@ -65,12 +66,17 @@ func New(wr io.Writer, errWriter io.Writer, disableStatus bool) *Terminal { return t } - if d, ok := wr.(fder); ok && terminal.CanUpdateStatus(d.Fd()) { - // only use the fancy status code when we're running on a real terminal. - t.canUpdateStatus = true - t.fd = d.Fd() - t.clearCurrentLine = terminal.ClearCurrentLine(t.fd) - t.moveCursorUp = terminal.MoveCursorUp(t.fd) + if d, ok := wr.(fder); ok { + if terminal.CanUpdateStatus(d.Fd()) { + // only use the fancy status code when we're running on a real terminal. + t.canUpdateStatus = true + t.fd = d.Fd() + t.clearCurrentLine = terminal.ClearCurrentLine(t.fd) + t.moveCursorUp = terminal.MoveCursorUp(t.fd) + } + if terminal.OutputIsTerminal(d.Fd()) { + t.outputIsTerminal = true + } } return t @@ -88,6 +94,11 @@ func (t *Terminal) OutputRaw() io.Writer { return t.wr } +// OutputIsTerminal returns whether the output is a terminal. +func (t *Terminal) OutputIsTerminal() bool { + return t.outputIsTerminal +} + // Run updates the screen. It should be run in a separate goroutine. When // ctx is cancelled, the status lines are cleanly removed. func (t *Terminal) Run(ctx context.Context) { From 3410808dcf4ffc11fd2e6899946bdd39673cab13 Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Sun, 14 Sep 2025 17:21:30 +0200 Subject: [PATCH 08/25] deduplicate termstatus setup --- cmd/restic/cleanup.go | 15 +-- cmd/restic/cmd_backup.go | 6 +- cmd/restic/cmd_backup_integration_test.go | 5 +- cmd/restic/cmd_cache.go | 6 +- cmd/restic/cmd_cat.go | 6 +- cmd/restic/cmd_check.go | 8 +- cmd/restic/cmd_check_integration_test.go | 5 +- cmd/restic/cmd_copy.go | 6 +- cmd/restic/cmd_copy_integration_test.go | 5 +- cmd/restic/cmd_debug.go | 22 ++-- cmd/restic/cmd_debug_disabled.go | 2 +- cmd/restic/cmd_diff.go | 6 +- cmd/restic/cmd_diff_integration_test.go | 5 +- cmd/restic/cmd_dump.go | 6 +- cmd/restic/cmd_features.go | 2 +- cmd/restic/cmd_find.go | 6 +- cmd/restic/cmd_find_integration_test.go | 5 +- cmd/restic/cmd_forget.go | 6 +- cmd/restic/cmd_forget_integration_test.go | 5 +- cmd/restic/cmd_generate.go | 8 +- cmd/restic/cmd_generate_integration_test.go | 9 +- cmd/restic/cmd_init.go | 6 +- cmd/restic/cmd_init_integration_test.go | 13 ++- cmd/restic/cmd_key.go | 10 +- cmd/restic/cmd_key_add.go | 6 +- cmd/restic/cmd_key_integration_test.go | 75 +++++++------ cmd/restic/cmd_key_list.go | 6 +- cmd/restic/cmd_key_passwd.go | 6 +- cmd/restic/cmd_key_remove.go | 6 +- cmd/restic/cmd_list.go | 6 +- cmd/restic/cmd_list_integration_test.go | 5 +- cmd/restic/cmd_ls.go | 6 +- cmd/restic/cmd_ls_integration_test.go | 5 +- cmd/restic/cmd_migrate.go | 6 +- cmd/restic/cmd_mount.go | 10 +- cmd/restic/cmd_mount_disabled.go | 2 +- cmd/restic/cmd_mount_integration_test.go | 9 +- cmd/restic/cmd_options.go | 2 +- cmd/restic/cmd_prune.go | 6 +- cmd/restic/cmd_prune_integration_test.go | 25 +++-- cmd/restic/cmd_recover.go | 6 +- cmd/restic/cmd_recover_integration_test.go | 9 +- cmd/restic/cmd_repair.go | 8 +- cmd/restic/cmd_repair_index.go | 14 +-- .../cmd_repair_index_integration_test.go | 23 ++-- cmd/restic/cmd_repair_packs.go | 6 +- cmd/restic/cmd_repair_snapshots.go | 6 +- .../cmd_repair_snapshots_integration_test.go | 5 +- cmd/restic/cmd_restore.go | 6 +- cmd/restic/cmd_restore_integration_test.go | 12 +-- cmd/restic/cmd_rewrite.go | 6 +- cmd/restic/cmd_rewrite_integration_test.go | 30 +++--- cmd/restic/cmd_self_update.go | 10 +- cmd/restic/cmd_self_update_disabled.go | 2 +- cmd/restic/cmd_snapshots.go | 6 +- cmd/restic/cmd_snapshots_integration_test.go | 5 +- cmd/restic/cmd_stats.go | 6 +- cmd/restic/cmd_tag.go | 6 +- cmd/restic/cmd_tag_integration_test.go | 5 +- cmd/restic/cmd_unlock.go | 6 +- cmd/restic/cmd_version.go | 6 +- cmd/restic/flags_test.go | 2 +- cmd/restic/global.go | 8 +- cmd/restic/integration_helpers_test.go | 47 +++----- cmd/restic/integration_test.go | 19 ++-- cmd/restic/main.go | 102 ++++++++++-------- cmd/restic/termstatus.go | 41 ------- internal/ui/termstatus/status.go | 29 +++++ 68 files changed, 335 insertions(+), 450 deletions(-) delete mode 100644 cmd/restic/termstatus.go diff --git a/cmd/restic/cleanup.go b/cmd/restic/cleanup.go index b01029c0c..54a384f37 100644 --- a/cmd/restic/cleanup.go +++ b/cmd/restic/cleanup.go @@ -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() diff --git a/cmd/restic/cmd_backup.go b/cmd/restic/cmd_backup.go index 09dbab183..e62e0d1c2 100644 --- a/cmd/restic/cmd_backup.go +++ b/cmd/restic/cmd_backup.go @@ -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) }, } diff --git a/cmd/restic/cmd_backup_integration_test.go b/cmd/restic/cmd_backup_integration_test.go index ff4998991..1c59db852 100644 --- a/cmd/restic/cmd_backup_integration_test.go +++ b/cmd/restic/cmd_backup_integration_test.go @@ -13,11 +13,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(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 +24,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) }) } diff --git a/cmd/restic/cmd_cache.go b/cmd/restic/cmd_cache.go index f3f82954d..d61992f43 100644 --- a/cmd/restic/cmd_cache.go +++ b/cmd/restic/cmd_cache.go @@ -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) }, } diff --git a/cmd/restic/cmd_cat.go b/cmd/restic/cmd_cat.go index 1cbb54ef4..cca356740 100644 --- a/cmd/restic/cmd_cat.go +++ b/cmd/restic/cmd_cat.go @@ -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, } diff --git a/cmd/restic/cmd_check.go b/cmd/restic/cmd_check.go index 5fddcfc0c..04789dd4e 100644 --- a/cmd/restic/cmd_check.go +++ b/cmd/restic/cmd_check.go @@ -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 }, diff --git a/cmd/restic/cmd_check_integration_test.go b/cmd/restic/cmd_check_integration_test.go index 004f54b6b..b87bd1149 100644 --- a/cmd/restic/cmd_check_integration_test.go +++ b/cmd/restic/cmd_check_integration_test.go @@ -5,7 +5,6 @@ import ( "testing" rtest "github.com/restic/restic/internal/test" - "github.com/restic/restic/internal/ui" ) func testRunCheck(t testing.TB, gopts GlobalOptions) { @@ -25,12 +24,12 @@ func testRunCheckMustFail(t testing.TB, gopts GlobalOptions) { func testRunCheckOutput(gopts GlobalOptions, checkUnused bool) (string, error) { buf, err := withCaptureStdout(gopts, func(gopts GlobalOptions) error { - return withTermStatus(gopts, func(ctx context.Context, term ui.Terminal) error { + return withTermStatus(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 }) }) diff --git a/cmd/restic/cmd_copy.go b/cmd/restic/cmd_copy.go index eaa3ce846..ade86668c 100644 --- a/cmd/restic/cmd_copy.go +++ b/cmd/restic/cmd_copy.go @@ -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) }, } diff --git a/cmd/restic/cmd_copy_integration_test.go b/cmd/restic/cmd_copy_integration_test.go index 27f67bc73..c3b529a6c 100644 --- a/cmd/restic/cmd_copy_integration_test.go +++ b/cmd/restic/cmd_copy_integration_test.go @@ -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(gopts, func(ctx context.Context, gopts GlobalOptions) error { + return runCopy(context.TODO(), copyOpts, gopts, nil, gopts.term) })) } diff --git a/cmd/restic/cmd_debug.go b/cmd/restic/cmd_debug.go index a41a0d087..27041cf57 100644 --- a/cmd/restic/cmd_debug.go +++ b/cmd/restic/cmd_debug.go @@ -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) }, } diff --git a/cmd/restic/cmd_debug_disabled.go b/cmd/restic/cmd_debug_disabled.go index 34d06a467..ba794925c 100644 --- a/cmd/restic/cmd_debug_disabled.go +++ b/cmd/restic/cmd_debug_disabled.go @@ -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 } diff --git a/cmd/restic/cmd_diff.go b/cmd/restic/cmd_diff.go index 7650a225f..0b9a4ad2a 100644 --- a/cmd/restic/cmd_diff.go +++ b/cmd/restic/cmd_diff.go @@ -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) }, } diff --git a/cmd/restic/cmd_diff_integration_test.go b/cmd/restic/cmd_diff_integration_test.go index ee20671c4..14cd33d6d 100644 --- a/cmd/restic/cmd_diff_integration_test.go +++ b/cmd/restic/cmd_diff_integration_test.go @@ -12,7 +12,6 @@ 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) { @@ -20,8 +19,8 @@ func testRunDiffOutput(gopts GlobalOptions, firstSnapshotID string, secondSnapsh 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 withTermStatus(gopts, func(ctx context.Context, gopts GlobalOptions) error { + return runDiff(ctx, opts, gopts, []string{firstSnapshotID, secondSnapshotID}, gopts.term) }) }) return buf.String(), err diff --git a/cmd/restic/cmd_dump.go b/cmd/restic/cmd_dump.go index 2cd15c06f..522e4a65d 100644 --- a/cmd/restic/cmd_dump.go +++ b/cmd/restic/cmd_dump.go @@ -17,7 +17,7 @@ import ( "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", @@ -47,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) }, } diff --git a/cmd/restic/cmd_features.go b/cmd/restic/cmd_features.go index 7770e6a9f..8541b1c34 100644 --- a/cmd/restic/cmd_features.go +++ b/cmd/restic/cmd_features.go @@ -10,7 +10,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", diff --git a/cmd/restic/cmd_find.go b/cmd/restic/cmd_find.go index d7aa3665d..a1ad9668f 100644 --- a/cmd/restic/cmd_find.go +++ b/cmd/restic/cmd_find.go @@ -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) }, } diff --git a/cmd/restic/cmd_find_integration_test.go b/cmd/restic/cmd_find_integration_test.go index ad34923ed..834228664 100644 --- a/cmd/restic/cmd_find_integration_test.go +++ b/cmd/restic/cmd_find_integration_test.go @@ -8,15 +8,14 @@ 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 { gopts.JSON = wantJSON - return withTermStatus(gopts, func(ctx context.Context, term ui.Terminal) error { - return runFind(ctx, opts, gopts, []string{pattern}, term) + return withTermStatus(gopts, func(ctx context.Context, gopts GlobalOptions) error { + return runFind(ctx, opts, gopts, []string{pattern}, gopts.term) }) }) rtest.OK(t, err) diff --git a/cmd/restic/cmd_forget.go b/cmd/restic/cmd_forget.go index 42739cdf0..7a9a8105a 100644 --- a/cmd/restic/cmd_forget.go +++ b/cmd/restic/cmd_forget.go @@ -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) }, } diff --git a/cmd/restic/cmd_forget_integration_test.go b/cmd/restic/cmd_forget_integration_test.go index d3be8a60d..0a110cc70 100644 --- a/cmd/restic/cmd_forget_integration_test.go +++ b/cmd/restic/cmd_forget_integration_test.go @@ -8,15 +8,14 @@ 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 { 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(gopts, func(ctx context.Context, gopts GlobalOptions) error { + return runForget(context.TODO(), opts, pruneOpts, gopts, gopts.term, args) }) } diff --git a/cmd/restic/cmd_generate.go b/cmd/restic/cmd_generate.go index 477910507..e2fdf7fc3 100644 --- a/cmd/restic/cmd_generate.go +++ b/cmd/restic/cmd_generate.go @@ -13,7 +13,7 @@ import ( "github.com/spf13/pflag" ) -func newGenerateCommand() *cobra.Command { +func newGenerateCommand(globalOptions *GlobalOptions) *cobra.Command { var opts generateOptions cmd := &cobra.Command{ @@ -31,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()) @@ -118,7 +116,7 @@ func runGenerate(opts generateOptions, gopts GlobalOptions, args []string, term } printer := newTerminalProgressPrinter(gopts.JSON, gopts.verbosity, term) - cmdRoot := newRootCommand() + cmdRoot := newRootCommand(&GlobalOptions{}) if opts.ManDir != "" { err := writeManpages(cmdRoot, opts.ManDir, printer) diff --git a/cmd/restic/cmd_generate_integration_test.go b/cmd/restic/cmd_generate_integration_test.go index 858e72453..c1354a5cb 100644 --- a/cmd/restic/cmd_generate_integration_test.go +++ b/cmd/restic/cmd_generate_integration_test.go @@ -6,13 +6,12 @@ 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) + return withTermStatus(gopts, func(ctx context.Context, gopts GlobalOptions) error { + return runGenerate(opts, gopts, []string{}, gopts.term) }) }) return buf.Bytes(), err @@ -31,14 +30,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(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(GlobalOptions{}, generateOptions{BashCompletionFile: "-", FishCompletionFile: "-"}) rtest.Assert(t, err != nil, "generate shell completions to stdout for two shells fails") }) } diff --git a/cmd/restic/cmd_init.go b/cmd/restic/cmd_init.go index 8e8488355..e358ffd8a 100644 --- a/cmd/restic/cmd_init.go +++ b/cmd/restic/cmd_init.go @@ -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()) diff --git a/cmd/restic/cmd_init_integration_test.go b/cmd/restic/cmd_init_integration_test.go index 8ce14a23a..e5fba798a 100644 --- a/cmd/restic/cmd_init_integration_test.go +++ b/cmd/restic/cmd_init_integration_test.go @@ -9,7 +9,6 @@ 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" ) @@ -18,8 +17,8 @@ func testRunInit(t testing.TB, opts GlobalOptions) { 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(opts, func(ctx context.Context, gopts GlobalOptions) error { + return runInit(ctx, InitOptions{}, opts, nil, gopts.term) }) rtest.OK(t, err) t.Logf("repository initialized at %v", opts.Repo) @@ -44,14 +43,14 @@ 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(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(env.gopts, func(ctx context.Context, gopts GlobalOptions) error { + return runInit(ctx, initOpts, gopts, nil, gopts.term) }) rtest.OK(t, err) diff --git a/cmd/restic/cmd_key.go b/cmd/restic/cmd_key.go index 29d38bdce..508b428e0 100644 --- a/cmd/restic/cmd_key.go +++ b/cmd/restic/cmd_key.go @@ -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 } diff --git a/cmd/restic/cmd_key_add.go b/cmd/restic/cmd_key_add.go index 1c3fc04f6..28b91dfe3 100644 --- a/cmd/restic/cmd_key_add.go +++ b/cmd/restic/cmd_key_add.go @@ -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) }, } diff --git a/cmd/restic/cmd_key_integration_test.go b/cmd/restic/cmd_key_integration_test.go index d24e71b09..903fab07e 100644 --- a/cmd/restic/cmd_key_integration_test.go +++ b/cmd/restic/cmd_key_integration_test.go @@ -12,14 +12,13 @@ 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) + return withTermStatus(gopts, func(ctx context.Context, gopts GlobalOptions) error { + return runKeyList(ctx, gopts, []string{}, gopts.term) }) }) rtest.OK(t, err) @@ -43,8 +42,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(gopts, func(ctx context.Context, gopts GlobalOptions) error { + return runKeyAdd(ctx, gopts, KeyAddOptions{}, []string{}, gopts.term) }) rtest.OK(t, err) } @@ -56,11 +55,11 @@ 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(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) @@ -79,8 +78,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(gopts, func(ctx context.Context, gopts GlobalOptions) error { + return runKeyPasswd(ctx, gopts, KeyPasswdOptions{}, []string{}, gopts.term) }) rtest.OK(t, err) } @@ -88,8 +87,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(gopts, func(ctx context.Context, gopts GlobalOptions) error { + return runKeyRemove(ctx, gopts, []string{id}, gopts.term) }) rtest.OK(t, err) } @@ -121,8 +120,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(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 +134,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(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(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 +160,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(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 +195,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(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(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(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 +224,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(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(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(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(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(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) diff --git a/cmd/restic/cmd_key_list.go b/cmd/restic/cmd_key_list.go index bdc0a7f82..21eee0c79 100644 --- a/cmd/restic/cmd_key_list.go +++ b/cmd/restic/cmd_key_list.go @@ -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 diff --git a/cmd/restic/cmd_key_passwd.go b/cmd/restic/cmd_key_passwd.go index 798a9fbaa..97c782989 100644 --- a/cmd/restic/cmd_key_passwd.go +++ b/cmd/restic/cmd_key_passwd.go @@ -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) }, } diff --git a/cmd/restic/cmd_key_remove.go b/cmd/restic/cmd_key_remove.go index 5adb97d80..0e0c9704a 100644 --- a/cmd/restic/cmd_key_remove.go +++ b/cmd/restic/cmd_key_remove.go @@ -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 diff --git a/cmd/restic/cmd_list.go b/cmd/restic/cmd_list.go index fc425f07d..2cbfa5e72 100644 --- a/cmd/restic/cmd_list.go +++ b/cmd/restic/cmd_list.go @@ -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), diff --git a/cmd/restic/cmd_list_integration_test.go b/cmd/restic/cmd_list_integration_test.go index 69fef1d6b..412bd3a2a 100644 --- a/cmd/restic/cmd_list_integration_test.go +++ b/cmd/restic/cmd_list_integration_test.go @@ -8,13 +8,12 @@ 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) + return withTermStatus(opts, func(ctx context.Context, gopts GlobalOptions) error { + return runList(ctx, opts, []string{tpe}, gopts.term) }) }) rtest.OK(t, err) diff --git a/cmd/restic/cmd_ls.go b/cmd/restic/cmd_ls.go index b56f0b6e3..56bb0f9b6 100644 --- a/cmd/restic/cmd_ls.go +++ b/cmd/restic/cmd_ls.go @@ -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()) diff --git a/cmd/restic/cmd_ls_integration_test.go b/cmd/restic/cmd_ls_integration_test.go index b39e9e582..a4a54c081 100644 --- a/cmd/restic/cmd_ls_integration_test.go +++ b/cmd/restic/cmd_ls_integration_test.go @@ -10,14 +10,13 @@ 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 { gopts.Quiet = true - return withTermStatus(gopts, func(ctx context.Context, term ui.Terminal) error { - return runLs(context.TODO(), opts, gopts, args, term) + return withTermStatus(gopts, func(ctx context.Context, gopts GlobalOptions) error { + return runLs(context.TODO(), opts, gopts, args, gopts.term) }) }) rtest.OK(t, err) diff --git a/cmd/restic/cmd_migrate.go b/cmd/restic/cmd_migrate.go index 8e1d23c04..f7ac20f4f 100644 --- a/cmd/restic/cmd_migrate.go +++ b/cmd/restic/cmd_migrate.go @@ -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) }, } diff --git a/cmd/restic/cmd_mount.go b/cmd/restic/cmd_mount.go index a476422fd..6eb35f837 100644 --- a/cmd/restic/cmd_mount.go +++ b/cmd/restic/cmd_mount.go @@ -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) }, } diff --git a/cmd/restic/cmd_mount_disabled.go b/cmd/restic/cmd_mount_disabled.go index 8b83905e7..57a44940f 100644 --- a/cmd/restic/cmd_mount_disabled.go +++ b/cmd/restic/cmd_mount_disabled.go @@ -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 } diff --git a/cmd/restic/cmd_mount_integration_test.go b/cmd/restic/cmd_mount_integration_test.go index ea1451ba6..91c014234 100644 --- a/cmd/restic/cmd_mount_integration_test.go +++ b/cmd/restic/cmd_mount_integration_test.go @@ -16,7 +16,6 @@ import ( "github.com/restic/restic/internal/debug" "github.com/restic/restic/internal/restic" rtest "github.com/restic/restic/internal/test" - "github.com/restic/restic/internal/ui" ) const ( @@ -62,8 +61,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(gopts, func(ctx context.Context, gopts GlobalOptions) error { + return runMount(context.TODO(), opts, gopts, []string{dir}, gopts.term) })) } @@ -128,8 +127,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(gopts, func(ctx context.Context, gopts GlobalOptions) error { + printer := newTerminalProgressPrinter(gopts.JSON, gopts.verbosity, gopts.term) _, repo, unlock, err := openWithReadLock(ctx, gopts, false, printer) if err != nil { return err diff --git a/cmd/restic/cmd_options.go b/cmd/restic/cmd_options.go index 34cf8656e..86418c891 100644 --- a/cmd/restic/cmd_options.go +++ b/cmd/restic/cmd_options.go @@ -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", diff --git a/cmd/restic/cmd_prune.go b/cmd/restic/cmd_prune.go index 3adc6a90e..71e96954b 100644 --- a/cmd/restic/cmd_prune.go +++ b/cmd/restic/cmd_prune.go @@ -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) }, } diff --git a/cmd/restic/cmd_prune_integration_test.go b/cmd/restic/cmd_prune_integration_test.go index d9103fc8f..df850feff 100644 --- a/cmd/restic/cmd_prune_integration_test.go +++ b/cmd/restic/cmd_prune_integration_test.go @@ -9,7 +9,6 @@ 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) { @@ -29,8 +28,8 @@ func testRunPruneOutput(gopts GlobalOptions, opts PruneOptions) error { 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(gopts, func(ctx context.Context, gopts GlobalOptions) error { + return runPrune(context.TODO(), opts, gopts, gopts.term) }) } @@ -99,8 +98,8 @@ 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 withTermStatus(gopts, func(ctx context.Context, gopts GlobalOptions) error { + return runForget(context.TODO(), opts, pruneOpts, gopts, gopts.term, args) }) }) rtest.OK(t, err) @@ -122,8 +121,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(env.gopts, func(ctx context.Context, gopts GlobalOptions) error { + _, err := runCheck(context.TODO(), checkOpts, gopts, nil, gopts.term) return err })) } @@ -158,8 +157,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(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 +230,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(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 +241,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(env.gopts, func(ctx context.Context, gopts GlobalOptions) error { + return runPrune(context.TODO(), optionsPrune, gopts, gopts.term) }) != nil, "prune should have reported an error") } diff --git a/cmd/restic/cmd_recover.go b/cmd/restic/cmd_recover.go index 274066eed..2dcf51376 100644 --- a/cmd/restic/cmd_recover.go +++ b/cmd/restic/cmd_recover.go @@ -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 diff --git a/cmd/restic/cmd_recover_integration_test.go b/cmd/restic/cmd_recover_integration_test.go index 91dec1505..5d51ee2d9 100644 --- a/cmd/restic/cmd_recover_integration_test.go +++ b/cmd/restic/cmd_recover_integration_test.go @@ -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(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(env.gopts, func(ctx context.Context, gopts GlobalOptions) error { + return runCat(context.TODO(), gopts, []string{"tree", ids[0].String() + ":" + sn.Tree.Str()}, gopts.term) })) } diff --git a/cmd/restic/cmd_repair.go b/cmd/restic/cmd_repair.go index bb1c98d27..c6b5a212b 100644 --- a/cmd/restic/cmd_repair.go +++ b/cmd/restic/cmd_repair.go @@ -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 } diff --git a/cmd/restic/cmd_repair_index.go b/cmd/restic/cmd_repair_index.go index 52383f720..163e68a07 100644 --- a/cmd/restic/cmd_repair_index.go +++ b/cmd/restic/cmd_repair_index.go @@ -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) }, } diff --git a/cmd/restic/cmd_repair_index_integration_test.go b/cmd/restic/cmd_repair_index_integration_test.go index c03c8f3d0..bd9924e63 100644 --- a/cmd/restic/cmd_repair_index_integration_test.go +++ b/cmd/restic/cmd_repair_index_integration_test.go @@ -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 { - gopts.stdout = io.Discard - return runRebuildIndex(context.TODO(), RepairIndexOptions{}, gopts, term) - }) + rtest.OK(t, withTermStatus(gopts, func(ctx context.Context, gopts GlobalOptions) error { + gopts.stdout = io.Discard + return runRebuildIndex(context.TODO(), RepairIndexOptions{}, gopts, gopts.term) })) } @@ -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 { - env.gopts.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(env.gopts, func(ctx context.Context, gopts GlobalOptions) error { + gopts.stdout = io.Discard + return runRebuildIndex(context.TODO(), RepairIndexOptions{}, gopts, gopts.term) }) if err == nil { diff --git a/cmd/restic/cmd_repair_packs.go b/cmd/restic/cmd_repair_packs.go index e8d6a1196..9161cdb50 100644 --- a/cmd/restic/cmd_repair_packs.go +++ b/cmd/restic/cmd_repair_packs.go @@ -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 diff --git a/cmd/restic/cmd_repair_snapshots.go b/cmd/restic/cmd_repair_snapshots.go index 49ab7b151..d109e1097 100644 --- a/cmd/restic/cmd_repair_snapshots.go +++ b/cmd/restic/cmd_repair_snapshots.go @@ -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) }, } diff --git a/cmd/restic/cmd_repair_snapshots_integration_test.go b/cmd/restic/cmd_repair_snapshots_integration_test.go index 6594d211c..1173461ec 100644 --- a/cmd/restic/cmd_repair_snapshots_integration_test.go +++ b/cmd/restic/cmd_repair_snapshots_integration_test.go @@ -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(gopts, func(ctx context.Context, gopts GlobalOptions) error { + return runRepairSnapshots(context.TODO(), gopts, opts, nil, gopts.term) })) } diff --git a/cmd/restic/cmd_restore.go b/cmd/restic/cmd_restore.go index d0bf76d85..5fb7a65ea 100644 --- a/cmd/restic/cmd_restore.go +++ b/cmd/restic/cmd_restore.go @@ -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) }, } diff --git a/cmd/restic/cmd_restore_integration_test.go b/cmd/restic/cmd_restore_integration_test.go index 09d5f4d9e..9182f06aa 100644 --- a/cmd/restic/cmd_restore_integration_test.go +++ b/cmd/restic/cmd_restore_integration_test.go @@ -14,7 +14,6 @@ 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) { @@ -31,8 +30,8 @@ func testRunRestoreExcludes(t testing.TB, gopts GlobalOptions, dir string, snaps } func testRunRestoreAssumeFailure(snapshotID string, opts RestoreOptions, gopts GlobalOptions) error { - return withTermStatus(gopts, func(ctx context.Context, term ui.Terminal) error { - return runRestore(ctx, opts, gopts, term, []string{snapshotID}) + return withTermStatus(gopts, func(ctx context.Context, gopts GlobalOptions) error { + return runRestore(ctx, opts, gopts, gopts.term, []string{snapshotID}) }) } @@ -337,11 +336,8 @@ func TestRestoreWithPermissionFailure(t *testing.T) { snapshots := testListSnapshots(t, env.gopts, 1) - _ = withRestoreGlobalOptions(func() error { - env.gopts.stderr = io.Discard - testRunRestore(t, env.gopts, filepath.Join(env.base, "restore"), snapshots[0].String()) - return nil - }) + env.gopts.stderr = io.Discard + 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 diff --git a/cmd/restic/cmd_rewrite.go b/cmd/restic/cmd_rewrite.go index 45ec3c13b..26677f7a7 100644 --- a/cmd/restic/cmd_rewrite.go +++ b/cmd/restic/cmd_rewrite.go @@ -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) }, } diff --git a/cmd/restic/cmd_rewrite_integration_test.go b/cmd/restic/cmd_rewrite_integration_test.go index f011cdcc1..213ef0319 100644 --- a/cmd/restic/cmd_rewrite_integration_test.go +++ b/cmd/restic/cmd_rewrite_integration_test.go @@ -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(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(env.gopts, func(ctx context.Context, gopts GlobalOptions) error { + printer := newTerminalProgressPrinter(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(env.gopts, func(ctx context.Context, gopts GlobalOptions) error { + printer := newTerminalProgressPrinter(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(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(env.gopts, func(ctx context.Context, gopts GlobalOptions) error { + printer := newTerminalProgressPrinter(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(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] diff --git a/cmd/restic/cmd_self_update.go b/cmd/restic/cmd_self_update.go index b4173ab78..5615db808 100644 --- a/cmd/restic/cmd_self_update.go +++ b/cmd/restic/cmd_self_update.go @@ -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) }, } diff --git a/cmd/restic/cmd_self_update_disabled.go b/cmd/restic/cmd_self_update_disabled.go index ce9af10d2..bca03335f 100644 --- a/cmd/restic/cmd_self_update_disabled.go +++ b/cmd/restic/cmd_self_update_disabled.go @@ -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 } diff --git a/cmd/restic/cmd_snapshots.go b/cmd/restic/cmd_snapshots.go index 8d921194e..741da3331 100644 --- a/cmd/restic/cmd_snapshots.go +++ b/cmd/restic/cmd_snapshots.go @@ -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) }, } diff --git a/cmd/restic/cmd_snapshots_integration_test.go b/cmd/restic/cmd_snapshots_integration_test.go index a009b2908..af45fde9c 100644 --- a/cmd/restic/cmd_snapshots_integration_test.go +++ b/cmd/restic/cmd_snapshots_integration_test.go @@ -7,7 +7,6 @@ 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) { @@ -15,8 +14,8 @@ func testRunSnapshots(t testing.TB, gopts GlobalOptions) (newest *Snapshot, snap gopts.JSON = true opts := SnapshotOptions{} - return withTermStatus(gopts, func(ctx context.Context, term ui.Terminal) error { - return runSnapshots(ctx, opts, gopts, []string{}, term) + return withTermStatus(gopts, func(ctx context.Context, gopts GlobalOptions) error { + return runSnapshots(ctx, opts, gopts, []string{}, gopts.term) }) }) rtest.OK(t, err) diff --git a/cmd/restic/cmd_stats.go b/cmd/restic/cmd_stats.go index f25ed39bb..64230be95 100644 --- a/cmd/restic/cmd_stats.go +++ b/cmd/restic/cmd_stats.go @@ -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) }, } diff --git a/cmd/restic/cmd_tag.go b/cmd/restic/cmd_tag.go index fde4209bc..b1f82acfa 100644 --- a/cmd/restic/cmd_tag.go +++ b/cmd/restic/cmd_tag.go @@ -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) }, } diff --git a/cmd/restic/cmd_tag_integration_test.go b/cmd/restic/cmd_tag_integration_test.go index cbb08c5bf..9958be485 100644 --- a/cmd/restic/cmd_tag_integration_test.go +++ b/cmd/restic/cmd_tag_integration_test.go @@ -6,12 +6,11 @@ import ( "github.com/restic/restic/internal/restic" rtest "github.com/restic/restic/internal/test" - "github.com/restic/restic/internal/ui" ) func testRunTag(t testing.TB, opts TagOptions, gopts GlobalOptions) { - rtest.OK(t, withTermStatus(gopts, func(ctx context.Context, term ui.Terminal) error { - return runTag(context.TODO(), opts, gopts, term, []string{}) + rtest.OK(t, withTermStatus(gopts, func(ctx context.Context, gopts GlobalOptions) error { + return runTag(context.TODO(), opts, gopts, gopts.term, []string{}) })) } diff --git a/cmd/restic/cmd_unlock.go b/cmd/restic/cmd_unlock.go index 8cea239c6..096d21cac 100644 --- a/cmd/restic/cmd_unlock.go +++ b/cmd/restic/cmd_unlock.go @@ -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()) diff --git a/cmd/restic/cmd_version.go b/cmd/restic/cmd_version.go index 1acfba5ab..a32575389 100644 --- a/cmd/restic/cmd_version.go +++ b/cmd/restic/cmd_version.go @@ -7,7 +7,7 @@ import ( "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 +23,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 := newTerminalProgressPrinter(globalOptions.JSON, globalOptions.verbosity, globalOptions.term) if globalOptions.JSON { type jsonVersion struct { diff --git a/cmd/restic/flags_test.go b/cmd/restic/flags_test.go index b1001b7c5..aa177c45c 100644 --- a/cmd/restic/flags_test.go +++ b/cmd/restic/flags_test.go @@ -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"}) diff --git a/cmd/restic/global.go b/cmd/restic/global.go index e3419fedc..14dc8b6da 100644 --- a/cmd/restic/global.go +++ b/cmd/restic/global.go @@ -34,6 +34,7 @@ import ( "github.com/restic/restic/internal/restic" "github.com/restic/restic/internal/terminal" "github.com/restic/restic/internal/textfile" + "github.com/restic/restic/internal/ui" "github.com/restic/restic/internal/ui/progress" "github.com/spf13/pflag" @@ -77,6 +78,7 @@ type GlobalOptions struct { password string stdout io.Writer stderr io.Writer + term ui.Terminal backends *location.Registry backendTestHook, backendInnerTestHook backendWrapper @@ -177,12 +179,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()) diff --git a/cmd/restic/integration_helpers_test.go b/cmd/restic/integration_helpers_test.go index 7367bbe70..bfefe807f 100644 --- a/cmd/restic/integration_helpers_test.go +++ b/cmd/restic/integration_helpers_test.go @@ -20,7 +20,6 @@ import ( "github.com/restic/restic/internal/repository" "github.com/restic/restic/internal/restic" rtest "github.com/restic/restic/internal/test" - "github.com/restic/restic/internal/ui" "github.com/restic/restic/internal/ui/termstatus" ) @@ -222,12 +221,9 @@ func withTestEnvironment(t testing.TB) (env *testEnvironment, cleanup func()) { // 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 +244,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(gopts, func(ctx context.Context, gopts GlobalOptions) error { + printer := newTerminalProgressPrinter(gopts.JSON, gopts.verbosity, gopts.term) ctx, r, unlock, err := openWithReadLock(ctx, gopts, false, printer) rtest.OK(t, err) defer unlock() @@ -267,8 +263,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(gopts, func(ctx context.Context, gopts GlobalOptions) error { + printer := newTerminalProgressPrinter(gopts.JSON, gopts.verbosity, gopts.term) ctx, r, unlock, err := openWithReadLock(ctx, gopts, false, printer) rtest.OK(t, err) defer unlock() @@ -298,8 +294,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(gopts, func(ctx context.Context, gopts GlobalOptions) error { + printer := newTerminalProgressPrinter(gopts.JSON, gopts.verbosity, gopts.term) ctx, _, unlock, err := openWithExclusiveLock(ctx, gopts, false, printer) rtest.OK(t, err) defer unlock() @@ -314,8 +310,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(gopts, func(ctx context.Context, gopts GlobalOptions) error { + printer := newTerminalProgressPrinter(gopts.JSON, gopts.verbosity, gopts.term) ctx, r, unlock, err := openWithExclusiveLock(ctx, gopts, false, printer) rtest.OK(t, err) defer unlock() @@ -375,8 +371,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(gopts, func(ctx context.Context, gopts GlobalOptions) error { + printer := newTerminalProgressPrinter(gopts.JSON, gopts.verbosity, gopts.term) _, repo, unlock, err := openWithReadLock(ctx, gopts, false, printer) rtest.OK(t, err) defer unlock() @@ -420,30 +416,19 @@ 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) { buf := bytes.NewBuffer(nil) - err := withRestoreGlobalOptions(func() error { - globalOptions.stdout = buf - gopts.stdout = buf - return inner(gopts) - }) - + gopts.stdout = buf + err := inner(gopts) return buf, err } -func withTermStatus(gopts GlobalOptions, callback func(ctx context.Context, term ui.Terminal) error) error { +func withTermStatus(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) + gopts.term = term wg.Add(1) go func() { defer wg.Done() @@ -453,5 +438,5 @@ func withTermStatus(gopts GlobalOptions, callback func(ctx context.Context, term defer wg.Wait() defer cancel() - return callback(ctx, term) + return callback(ctx, gopts) } diff --git a/cmd/restic/integration_test.go b/cmd/restic/integration_test.go index c16f09bf1..c0e98e232 100644 --- a/cmd/restic/integration_test.go +++ b/cmd/restic/integration_test.go @@ -12,7 +12,6 @@ import ( "github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/restic" rtest "github.com/restic/restic/internal/test" - "github.com/restic/restic/internal/ui" ) func TestCheckRestoreNoLock(t *testing.T) { @@ -87,15 +86,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(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(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(env.gopts, func(ctx context.Context, gopts GlobalOptions) error { + return runRebuildIndex(context.TODO(), RepairIndexOptions{ReadAllPacks: true}, gopts, gopts.term) })) } @@ -162,9 +161,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(env.gopts, func(ctx context.Context, gopts GlobalOptions) error { + printer := newTerminalProgressPrinter(gopts.JSON, gopts.verbosity, gopts.term) + ctx, repo, unlock, err := openWithReadLock(ctx, gopts, false, printer) rtest.OK(t, err) defer unlock() diff --git a/cmd/restic/main.go b/cmd/restic/main.go index f373a418d..91fbf638d 100644 --- a/cmd/restic/main.go +++ b/cmd/restic/main.go @@ -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,40 +67,41 @@ 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) + 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,26 @@ 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{ + stdout: os.Stdout, + stderr: os.Stderr, + backends: collectBackends(), } + func() { + term, cancel := termstatus.Setup(os.Stdout, os.Stderr, globalOptions.Quiet) + defer cancel() + globalOptions.stdout, globalOptions.stderr = termstatus.WrapStdio(term) + 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 +236,7 @@ func main() { } if exitCode != 0 { - printExitError(exitCode, exitMessage) + printExitError(globalOptions, exitCode, exitMessage) } Exit(exitCode) } diff --git a/cmd/restic/termstatus.go b/cmd/restic/termstatus.go deleted file mode 100644 index c0e9a045b..000000000 --- a/cmd/restic/termstatus.go +++ /dev/null @@ -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() - } -} diff --git a/internal/ui/termstatus/status.go b/internal/ui/termstatus/status.go index f1cbd7ef4..a7bd60c31 100644 --- a/internal/ui/termstatus/status.go +++ b/internal/ui/termstatus/status.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "strings" + "sync" "github.com/restic/restic/internal/terminal" "github.com/restic/restic/internal/ui" @@ -46,6 +47,34 @@ type fder interface { Fd() uintptr } +// Setup creates a new termstatus. +// The returned function must be called to shut down the termstatus, +// +// Expected usage: +// ``` +// term, cancel := termstatus.Setup(os.stdout, os.stderr, false) +// defer cancel() +// // do stuff +// ``` +func Setup(stdout, stderr io.Writer, quiet bool) (*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 := New(stdout, stderr, quiet) + wg.Add(1) + go func() { + defer wg.Done() + term.Run(cancelCtx) + }() + + return term, func() { + // shutdown termstatus + cancel() + wg.Wait() + } +} + // New returns a new Terminal for wr. A goroutine is started to update the // terminal. It is terminated when ctx is cancelled. When wr is redirected to // a file (e.g. via shell output redirection) or is just an io.Writer (not the From ca5b0c0249ca080f8fe123fd74feaa8f53d832cc Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Sun, 14 Sep 2025 15:29:39 +0200 Subject: [PATCH 09/25] get rid of fmt.Print* usages --- cmd/restic/cmd_features.go | 4 +--- cmd/restic/cmd_options.go | 4 ++-- cmd/restic/cmd_snapshots.go | 12 ++++-------- internal/filter/filter_test.go | 2 +- internal/restic/node_test.go | 3 +-- internal/selfupdate/verify.go | 3 +-- internal/walker/rewriter_test.go | 21 ++++++++++----------- 7 files changed, 20 insertions(+), 29 deletions(-) diff --git a/cmd/restic/cmd_features.go b/cmd/restic/cmd_features.go index 8541b1c34..e705ff080 100644 --- a/cmd/restic/cmd_features.go +++ b/cmd/restic/cmd_features.go @@ -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" @@ -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() diff --git a/cmd/restic/cmd_options.go b/cmd/restic/cmd_options.go index 86418c891..801beb5cd 100644 --- a/cmd/restic/cmd_options.go +++ b/cmd/restic/cmd_options.go @@ -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)) } }, } diff --git a/cmd/restic/cmd_snapshots.go b/cmd/restic/cmd_snapshots.go index 741da3331..7cbe06f5f 100644 --- a/cmd/restic/cmd_snapshots.go +++ b/cmd/restic/cmd_snapshots.go @@ -299,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+"]") @@ -313,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 } diff --git a/internal/filter/filter_test.go b/internal/filter/filter_test.go index 59b4a4d2d..573c2ede6 100644 --- a/internal/filter/filter_test.go +++ b/internal/filter/filter_test.go @@ -389,7 +389,7 @@ func TestFilterPatternsFile(t *testing.T) { if match { c++ - // fmt.Printf("pattern %q, line %q\n", test.pattern, line) + // t.Logf("pattern %q, line %q\n", test.pattern, line) } } diff --git a/internal/restic/node_test.go b/internal/restic/node_test.go index 38a17cb09..b544b2afd 100644 --- a/internal/restic/node_test.go +++ b/internal/restic/node_test.go @@ -2,7 +2,6 @@ package restic import ( "encoding/json" - "fmt" "testing" "time" @@ -73,7 +72,7 @@ func TestSymlinkSerialization(t *testing.T) { var n2 Node err = json.Unmarshal(ser, &n2) test.OK(t, err) - fmt.Println(string(ser)) + t.Logf("serialized %q\n", string(ser)) test.Equals(t, n.LinkTarget, n2.LinkTarget) } diff --git a/internal/selfupdate/verify.go b/internal/selfupdate/verify.go index 8db93fe8b..558c27965 100644 --- a/internal/selfupdate/verify.go +++ b/internal/selfupdate/verify.go @@ -174,8 +174,7 @@ CwGc func GPGVerify(data, sig []byte) (ok bool, err error) { keyring, err := openpgp.ReadArmoredKeyRing(bytes.NewReader(key)) if err != nil { - fmt.Printf("reading keyring failed") - return false, err + return false, fmt.Errorf("reading keyring failed: %w", err) } _, err = openpgp.CheckArmoredDetachedSignature(keyring, bytes.NewReader(data), bytes.NewReader(sig)) diff --git a/internal/walker/rewriter_test.go b/internal/walker/rewriter_test.go index 58dd25cd0..7c613e4ae 100644 --- a/internal/walker/rewriter_test.go +++ b/internal/walker/rewriter_test.go @@ -2,7 +2,6 @@ package walker import ( "context" - "fmt" "testing" "github.com/pkg/errors" @@ -32,9 +31,9 @@ func (t WritableTreeMap) SaveBlob(_ context.Context, tpe restic.BlobType, buf [] return id, true, len(buf), nil } -func (t WritableTreeMap) Dump() { +func (t WritableTreeMap) Dump(test testing.TB) { for k, v := range t.TreeMap { - fmt.Printf("%v: %v", k, string(v)) + test.Logf("%v: %v", k, string(v)) } } @@ -294,10 +293,10 @@ func TestRewriter(t *testing.T) { // verifying against the expected tree root also implicitly checks the structural integrity if newRoot != expRoot { t.Error("hash mismatch") - fmt.Println("Got") - modrepo.Dump() - fmt.Println("Expected") - WritableTreeMap{expRepo}.Dump() + t.Log("Got") + modrepo.Dump(t) + t.Log("Expected") + WritableTreeMap{expRepo}.Dump(t) } }) } @@ -348,10 +347,10 @@ func TestSnapshotSizeQuery(t *testing.T) { // verifying against the expected tree root also implicitly checks the structural integrity if newRoot != expRoot { t.Error("hash mismatch") - fmt.Println("Got") - modrepo.Dump() - fmt.Println("Expected") - WritableTreeMap{expRepo}.Dump() + t.Log("Got") + modrepo.Dump(t) + t.Log("Expected") + WritableTreeMap{expRepo}.Dump(t) } }) From 96af35555a0cc96efde39ca3dace4e306eba9d4f Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Thu, 18 Sep 2025 22:17:21 +0200 Subject: [PATCH 10/25] termstatus: add stdin and inject into backup command --- cmd/restic/cmd_backup.go | 20 +++++++++--------- cmd/restic/cmd_backup_test.go | 2 +- cmd/restic/cmd_init_integration_test.go | 14 ++++++++++--- cmd/restic/cmd_key_integration_test.go | 15 ++++++++------ cmd/restic/cmd_list_integration_test.go | 2 +- cmd/restic/global.go | 6 +++--- cmd/restic/integration_helpers_test.go | 2 +- cmd/restic/main.go | 2 +- cmd/restic/secondary_repo_test.go | 6 ------ internal/terminal/stdio.go | 6 ++---- internal/ui/mock.go | 8 ++++++++ internal/ui/terminal.go | 2 ++ internal/ui/termstatus/status.go | 27 +++++++++++++++++++++---- internal/ui/termstatus/status_test.go | 2 +- 14 files changed, 73 insertions(+), 41 deletions(-) diff --git a/cmd/restic/cmd_backup.go b/cmd/restic/cmd_backup.go index e62e0d1c2..8c070b660 100644 --- a/cmd/restic/cmd_backup.go +++ b/cmd/restic/cmd_backup.go @@ -182,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 } @@ -193,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) } @@ -218,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) { + var f io.ReadCloser = stdin if filename != "-" { if f, err = os.Open(filename); err != nil { return nil, err @@ -378,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 } @@ -409,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 } @@ -422,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 } @@ -490,7 +490,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, msg.E, term.InputRaw()) if err != nil { return err } @@ -582,7 +582,7 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, ter progressPrinter.V("read data from stdin") } filename := path.Join("/", opts.StdinFilename) - var source io.ReadCloser = os.Stdin + var source io.ReadCloser = term.InputRaw() if opts.StdinCommand { source, err = fs.NewCommandReader(ctx, args, msg.E) if err != nil { diff --git a/cmd/restic/cmd_backup_test.go b/cmd/restic/cmd_backup_test.go index ef5f02825..b607532b4 100644 --- a/cmd/restic/cmd_backup_test.go +++ b/cmd/restic/cmd_backup_test.go @@ -67,7 +67,7 @@ func TestCollectTargets(t *testing.T) { FilesFromRaw: []string{f3.Name()}, } - targets, err := collectTargets(opts, []string{filepath.Join(dir, "cmdline arg")}, 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) diff --git a/cmd/restic/cmd_init_integration_test.go b/cmd/restic/cmd_init_integration_test.go index e5fba798a..878049ea6 100644 --- a/cmd/restic/cmd_init_integration_test.go +++ b/cmd/restic/cmd_init_integration_test.go @@ -18,7 +18,7 @@ func testRunInit(t testing.TB, opts GlobalOptions) { restic.TestSetLockTimeout(t, 0) err := withTermStatus(opts, func(ctx context.Context, gopts GlobalOptions) error { - return runInit(ctx, InitOptions{}, opts, nil, gopts.term) + return runInit(ctx, InitOptions{}, gopts, nil, gopts.term) }) rtest.OK(t, err) t.Logf("repository initialized at %v", opts.Repo) @@ -54,10 +54,18 @@ func TestInitCopyChunkerParams(t *testing.T) { }) rtest.OK(t, err) - repo, err := OpenRepository(context.TODO(), env.gopts, &progress.NoopPrinter{}) + var repo *repository.Repository + err = withTermStatus(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(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, diff --git a/cmd/restic/cmd_key_integration_test.go b/cmd/restic/cmd_key_integration_test.go index 903fab07e..dad7f7e67 100644 --- a/cmd/restic/cmd_key_integration_test.go +++ b/cmd/restic/cmd_key_integration_test.go @@ -63,13 +63,16 @@ func testRunKeyAddNewKeyUserHost(t testing.TB, gopts GlobalOptions) { }) 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(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) { diff --git a/cmd/restic/cmd_list_integration_test.go b/cmd/restic/cmd_list_integration_test.go index 412bd3a2a..5b1409e9c 100644 --- a/cmd/restic/cmd_list_integration_test.go +++ b/cmd/restic/cmd_list_integration_test.go @@ -13,7 +13,7 @@ import ( 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, gopts GlobalOptions) error { - return runList(ctx, opts, []string{tpe}, gopts.term) + return runList(ctx, gopts, []string{tpe}, gopts.term) }) }) rtest.OK(t, err) diff --git a/cmd/restic/global.go b/cmd/restic/global.go index 14dc8b6da..9872f0070 100644 --- a/cmd/restic/global.go +++ b/cmd/restic/global.go @@ -260,7 +260,7 @@ func ReadPassword(ctx context.Context, opts GlobalOptions, prompt string, printe err error ) - if terminal.StdinIsTerminal() { + if opts.term.InputIsTerminal() { password, err = terminal.ReadPassword(ctx, os.Stdin, os.Stderr, prompt) } else { printer.PT("reading repository password from stdin") @@ -286,7 +286,7 @@ func ReadPasswordTwice(ctx context.Context, gopts GlobalOptions, prompt1, prompt if err != nil { return "", err } - if terminal.StdinIsTerminal() { + if gopts.term.InputIsTerminal() { pw2, err := ReadPassword(ctx, gopts, prompt2, printer) if err != nil { return "", err @@ -349,7 +349,7 @@ func OpenRepository(ctx context.Context, opts GlobalOptions, printer progress.Pr } passwordTriesLeft := 1 - if terminal.StdinIsTerminal() && opts.password == "" && !opts.InsecureNoPassword { + if opts.term.InputIsTerminal() && opts.password == "" && !opts.InsecureNoPassword { passwordTriesLeft = 3 } diff --git a/cmd/restic/integration_helpers_test.go b/cmd/restic/integration_helpers_test.go index bfefe807f..dc8b3eda9 100644 --- a/cmd/restic/integration_helpers_test.go +++ b/cmd/restic/integration_helpers_test.go @@ -427,7 +427,7 @@ func withTermStatus(gopts GlobalOptions, callback func(ctx context.Context, gopt ctx, cancel := context.WithCancel(context.TODO()) var wg sync.WaitGroup - term := termstatus.New(gopts.stdout, gopts.stderr, gopts.Quiet) + term := termstatus.New(os.Stdin, gopts.stdout, gopts.stderr, gopts.Quiet) gopts.term = term wg.Add(1) go func() { diff --git a/cmd/restic/main.go b/cmd/restic/main.go index 91fbf638d..1c4fa5e9c 100644 --- a/cmd/restic/main.go +++ b/cmd/restic/main.go @@ -178,7 +178,7 @@ func main() { backends: collectBackends(), } func() { - term, cancel := termstatus.Setup(os.Stdout, os.Stderr, globalOptions.Quiet) + term, cancel := termstatus.Setup(os.Stdin, os.Stdout, os.Stderr, globalOptions.Quiet) defer cancel() globalOptions.stdout, globalOptions.stderr = termstatus.WrapStdio(term) globalOptions.term = term diff --git a/cmd/restic/secondary_repo_test.go b/cmd/restic/secondary_repo_test.go index 2c31bcecf..32206318a 100644 --- a/cmd/restic/secondary_repo_test.go +++ b/cmd/restic/secondary_repo_test.go @@ -131,12 +131,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{ diff --git a/internal/terminal/stdio.go b/internal/terminal/stdio.go index 1ee33e025..827b8426a 100644 --- a/internal/terminal/stdio.go +++ b/internal/terminal/stdio.go @@ -1,13 +1,11 @@ package terminal import ( - "os" - "golang.org/x/term" ) -func StdinIsTerminal() bool { - return term.IsTerminal(int(os.Stdin.Fd())) +func InputIsTerminal(fd uintptr) bool { + return term.IsTerminal(int(fd)) } func OutputIsTerminal(fd uintptr) bool { diff --git a/internal/ui/mock.go b/internal/ui/mock.go index fc5488792..70a95fe1b 100644 --- a/internal/ui/mock.go +++ b/internal/ui/mock.go @@ -25,6 +25,14 @@ func (m *MockTerminal) CanUpdateStatus() bool { return true } +func (m *MockTerminal) InputRaw() io.ReadCloser { + return nil +} + +func (m *MockTerminal) InputIsTerminal() bool { + return true +} + func (m *MockTerminal) OutputRaw() io.Writer { return nil } diff --git a/internal/ui/terminal.go b/internal/ui/terminal.go index 845e36508..8ff5d6f27 100644 --- a/internal/ui/terminal.go +++ b/internal/ui/terminal.go @@ -13,6 +13,8 @@ type Terminal interface { SetStatus(lines []string) // CanUpdateStatus returns true if the terminal can update the status lines. CanUpdateStatus() bool + InputRaw() io.ReadCloser + InputIsTerminal() bool // OutputRaw returns the output writer. Should only be used if there is no // other option. Must not be used in combination with Print, Error, SetStatus // or any other method that writes to the terminal. diff --git a/internal/ui/termstatus/status.go b/internal/ui/termstatus/status.go index a7bd60c31..be3a3ce59 100644 --- a/internal/ui/termstatus/status.go +++ b/internal/ui/termstatus/status.go @@ -17,14 +17,16 @@ var _ ui.Terminal = &Terminal{} // updated. When the output is redirected to a file, the status lines are not // printed. type Terminal struct { + rd io.ReadCloser wr io.Writer fd uintptr errWriter io.Writer msg chan message status chan status + lastStatusLen int + inputIsTerminal bool outputIsTerminal bool canUpdateStatus bool - lastStatusLen int // will be closed when the goroutine which runs Run() terminates, so it'll // yield a default value immediately @@ -56,12 +58,12 @@ type fder interface { // defer cancel() // // do stuff // ``` -func Setup(stdout, stderr io.Writer, quiet bool) (*Terminal, func()) { +func Setup(stdin io.ReadCloser, stdout, stderr io.Writer, quiet bool) (*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 := New(stdout, stderr, quiet) + term := New(stdin, stdout, stderr, quiet) wg.Add(1) go func() { defer wg.Done() @@ -82,8 +84,9 @@ func Setup(stdout, stderr io.Writer, quiet bool) (*Terminal, func()) { // normal output (via Print/Printf) are written to wr, error messages are // written to errWriter. If disableStatus is set to true, no status messages // are printed even if the terminal supports it. -func New(wr io.Writer, errWriter io.Writer, disableStatus bool) *Terminal { +func New(rd io.ReadCloser, wr io.Writer, errWriter io.Writer, disableStatus bool) *Terminal { t := &Terminal{ + rd: rd, wr: wr, errWriter: errWriter, msg: make(chan message), @@ -95,6 +98,12 @@ func New(wr io.Writer, errWriter io.Writer, disableStatus bool) *Terminal { return t } + if d, ok := rd.(fder); ok { + if terminal.InputIsTerminal(d.Fd()) { + t.inputIsTerminal = true + } + } + if d, ok := wr.(fder); ok { if terminal.CanUpdateStatus(d.Fd()) { // only use the fancy status code when we're running on a real terminal. @@ -111,6 +120,16 @@ func New(wr io.Writer, errWriter io.Writer, disableStatus bool) *Terminal { return t } +// InputIsTerminal returns whether the input is a terminal. +func (t *Terminal) InputIsTerminal() bool { + return t.inputIsTerminal +} + +// InputRaw returns the input reader. +func (t *Terminal) InputRaw() io.ReadCloser { + return t.rd +} + // CanUpdateStatus return whether the status output is updated in place. func (t *Terminal) CanUpdateStatus() bool { return t.canUpdateStatus diff --git a/internal/ui/termstatus/status_test.go b/internal/ui/termstatus/status_test.go index b12928931..064b02989 100644 --- a/internal/ui/termstatus/status_test.go +++ b/internal/ui/termstatus/status_test.go @@ -13,7 +13,7 @@ import ( func TestSetStatus(t *testing.T) { var buf bytes.Buffer - term := New(&buf, io.Discard, false) + term := New(nil, &buf, io.Discard, false) term.canUpdateStatus = true term.fd = ^uintptr(0) From 013c565c299f4239cb3710b9afbfeafe2418e823 Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Thu, 18 Sep 2025 22:19:38 +0200 Subject: [PATCH 11/25] standardize shorten variable name for GlobalOptions to gopts --- cmd/restic/cmd_init_integration_test.go | 8 ++-- cmd/restic/cmd_list_integration_test.go | 10 ++-- cmd/restic/cmd_restore_integration_test.go | 4 +- cmd/restic/global.go | 54 +++++++++++----------- cmd/restic/global_test.go | 18 ++++---- 5 files changed, 47 insertions(+), 47 deletions(-) diff --git a/cmd/restic/cmd_init_integration_test.go b/cmd/restic/cmd_init_integration_test.go index 878049ea6..2fbe88fc4 100644 --- a/cmd/restic/cmd_init_integration_test.go +++ b/cmd/restic/cmd_init_integration_test.go @@ -12,20 +12,20 @@ import ( "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, gopts GlobalOptions) error { + err := withTermStatus(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)) } } diff --git a/cmd/restic/cmd_list_integration_test.go b/cmd/restic/cmd_list_integration_test.go index 5b1409e9c..58b240c5c 100644 --- a/cmd/restic/cmd_list_integration_test.go +++ b/cmd/restic/cmd_list_integration_test.go @@ -10,9 +10,9 @@ import ( rtest "github.com/restic/restic/internal/test" ) -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, gopts GlobalOptions) error { +func testRunList(t testing.TB, gopts GlobalOptions, tpe string) restic.IDs { + buf, err := withCaptureStdout(gopts, func(gopts GlobalOptions) error { + return withTermStatus(gopts, func(ctx context.Context, gopts GlobalOptions) error { return runList(ctx, gopts, []string{tpe}, gopts.term) }) }) @@ -38,9 +38,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 } diff --git a/cmd/restic/cmd_restore_integration_test.go b/cmd/restic/cmd_restore_integration_test.go index 9182f06aa..3c2403685 100644 --- a/cmd/restic/cmd_restore_integration_test.go +++ b/cmd/restic/cmd_restore_integration_test.go @@ -16,8 +16,8 @@ import ( rtest "github.com/restic/restic/internal/test" ) -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) { diff --git a/cmd/restic/global.go b/cmd/restic/global.go index 9872f0070..b31c37515 100644 --- a/cmd/restic/global.go +++ b/cmd/restic/global.go @@ -243,16 +243,16 @@ func readPassword(in io.Reader) (password string, err error) { // ReadPassword reads the password from a password file, the environment // variable RESTIC_PASSWORD or prompts the user. If the context is canceled, // the function leaks the password reading goroutine. -func ReadPassword(ctx context.Context, opts GlobalOptions, prompt string, printer progress.Printer) (string, error) { - if opts.InsecureNoPassword { - if opts.password != "" { +func ReadPassword(ctx context.Context, gopts GlobalOptions, prompt string, printer progress.Printer) (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 + if gopts.password != "" { + return gopts.password, nil } var ( @@ -260,7 +260,7 @@ func ReadPassword(ctx context.Context, opts GlobalOptions, prompt string, printe err error ) - if opts.term.InputIsTerminal() { + if gopts.term.InputIsTerminal() { password, err = terminal.ReadPassword(ctx, os.Stdin, os.Stderr, prompt) } else { printer.PT("reading repository password from stdin") @@ -300,20 +300,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 @@ -328,47 +328,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 opts.term.InputIsTerminal() && 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: ", printer) 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,15 +385,15 @@ func OpenRepository(ctx context.Context, opts GlobalOptions, printer progress.Pr } extra := "" if s.Config().Version >= 2 { - extra = ", compression level " + opts.Compression.String() + 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 @@ -417,7 +417,7 @@ func OpenRepository(ctx context.Context, opts GlobalOptions, printer progress.Pr } // cleanup old cache dirs if instructed to do so - if opts.CleanupCache { + 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()) diff --git a/cmd/restic/global_test.go b/cmd/restic/global_test.go index 57bf19862..173a7a2a8 100644 --- a/cmd/restic/global_test.go +++ b/cmd/restic/global_test.go @@ -26,9 +26,9 @@ 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 +37,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") } From ff5a0cc8515638806f84bb726520d643737c7ce9 Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Sun, 14 Sep 2025 19:21:51 +0200 Subject: [PATCH 12/25] termstatus: fully wrap reading password from terminal --- cmd/restic/global.go | 25 ++----------------------- cmd/restic/global_test.go | 11 ----------- internal/terminal/password.go | 15 +++++++-------- internal/ui/mock.go | 9 ++++++++- internal/ui/terminal.go | 6 +++++- internal/ui/termstatus/status.go | 23 +++++++++++++++++++++++ internal/ui/termstatus/status_test.go | 11 +++++++++++ 7 files changed, 56 insertions(+), 44 deletions(-) diff --git a/cmd/restic/global.go b/cmd/restic/global.go index b31c37515..0170830f0 100644 --- a/cmd/restic/global.go +++ b/cmd/restic/global.go @@ -1,7 +1,6 @@ package main import ( - "bufio" "context" "fmt" "io" @@ -32,7 +31,6 @@ 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" @@ -232,14 +230,6 @@ 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. @@ -255,20 +245,9 @@ func ReadPassword(ctx context.Context, gopts GlobalOptions, prompt string, print return gopts.password, nil } - var ( - password string - err error - ) - - if gopts.term.InputIsTerminal() { - password, err = terminal.ReadPassword(ctx, os.Stdin, os.Stderr, prompt) - } else { - printer.PT("reading repository password from stdin") - password, err = readPassword(os.Stdin) - } - + 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 { diff --git a/cmd/restic/global_test.go b/cmd/restic/global_test.go index 173a7a2a8..de8275876 100644 --- a/cmd/restic/global_test.go +++ b/cmd/restic/global_test.go @@ -7,21 +7,10 @@ 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) diff --git a/internal/terminal/password.go b/internal/terminal/password.go index 6d1b6c912..675344f77 100644 --- a/internal/terminal/password.go +++ b/internal/terminal/password.go @@ -3,7 +3,7 @@ package terminal import ( "context" "fmt" - "os" + "io" "golang.org/x/term" ) @@ -12,11 +12,10 @@ import ( // tty. Prompt is printed on the writer out before attempting to read the // password. If the context is canceled, the function leaks the password reading // goroutine. -func ReadPassword(ctx context.Context, in *os.File, out *os.File, prompt string) (password string, err error) { - fd := int(out.Fd()) - state, err := term.GetState(fd) +func ReadPassword(ctx context.Context, inFd int, out io.Writer, prompt string) (password string, err error) { + state, err := term.GetState(inFd) if err != nil { - _, _ = fmt.Fprintf(os.Stderr, "unable to get terminal state: %v\n", err) + _, _ = fmt.Fprintf(out, "unable to get terminal state: %v\n", err) return "", err } @@ -29,7 +28,7 @@ func ReadPassword(ctx context.Context, in *os.File, out *os.File, prompt string) if err != nil { return } - buf, err = term.ReadPassword(int(in.Fd())) + buf, err = term.ReadPassword(inFd) if err != nil { return } @@ -38,9 +37,9 @@ func ReadPassword(ctx context.Context, in *os.File, out *os.File, prompt string) select { case <-ctx.Done(): - err := term.Restore(fd, state) + err := term.Restore(inFd, state) if err != nil { - _, _ = fmt.Fprintf(os.Stderr, "unable to restore terminal state: %v\n", err) + _, _ = fmt.Fprintf(out, "unable to restore terminal state: %v\n", err) } return "", ctx.Err() case <-done: diff --git a/internal/ui/mock.go b/internal/ui/mock.go index 70a95fe1b..edc9050f9 100644 --- a/internal/ui/mock.go +++ b/internal/ui/mock.go @@ -1,6 +1,9 @@ package ui -import "io" +import ( + "context" + "io" +) var _ Terminal = &MockTerminal{} @@ -33,6 +36,10 @@ func (m *MockTerminal) InputIsTerminal() bool { return true } +func (m *MockTerminal) ReadPassword(_ context.Context, _ string) (string, error) { + return "password", nil +} + func (m *MockTerminal) OutputRaw() io.Writer { return nil } diff --git a/internal/ui/terminal.go b/internal/ui/terminal.go index 8ff5d6f27..c53de7bf2 100644 --- a/internal/ui/terminal.go +++ b/internal/ui/terminal.go @@ -1,6 +1,9 @@ package ui -import "io" +import ( + "context" + "io" +) // Terminal is used to write messages and display status lines which can be // updated. See termstatus.Terminal for a concrete implementation. @@ -15,6 +18,7 @@ type Terminal interface { CanUpdateStatus() bool InputRaw() io.ReadCloser InputIsTerminal() bool + ReadPassword(ctx context.Context, prompt string) (string, error) // OutputRaw returns the output writer. Should only be used if there is no // other option. Must not be used in combination with Print, Error, SetStatus // or any other method that writes to the terminal. diff --git a/internal/ui/termstatus/status.go b/internal/ui/termstatus/status.go index be3a3ce59..5ee21eb37 100644 --- a/internal/ui/termstatus/status.go +++ b/internal/ui/termstatus/status.go @@ -1,6 +1,7 @@ package termstatus import ( + "bufio" "context" "fmt" "io" @@ -18,6 +19,7 @@ var _ ui.Terminal = &Terminal{} // printed. type Terminal struct { rd io.ReadCloser + inFd uintptr wr io.Writer fd uintptr errWriter io.Writer @@ -100,6 +102,7 @@ func New(rd io.ReadCloser, wr io.Writer, errWriter io.Writer, disableStatus bool if d, ok := rd.(fder); ok { if terminal.InputIsTerminal(d.Fd()) { + t.inFd = d.Fd() t.inputIsTerminal = true } } @@ -130,6 +133,26 @@ func (t *Terminal) InputRaw() io.ReadCloser { return t.rd } +func (t *Terminal) ReadPassword(ctx context.Context, prompt string) (string, error) { + if t.InputIsTerminal() { + return terminal.ReadPassword(ctx, int(t.inFd), t.errWriter, prompt) + } + if t.OutputIsTerminal() { + t.Print("reading repository password from stdin") + } + return readPassword(t.rd) +} + +// readPassword reads the password from the given reader directly. +func readPassword(in io.Reader) (password string, err error) { + sc := bufio.NewScanner(in) + sc.Scan() + if sc.Err() != nil { + return "", fmt.Errorf("readPassword: %w", sc.Err()) + } + return sc.Text(), nil +} + // CanUpdateStatus return whether the status output is updated in place. func (t *Terminal) CanUpdateStatus() bool { return t.canUpdateStatus diff --git a/internal/ui/termstatus/status_test.go b/internal/ui/termstatus/status_test.go index 064b02989..bddb7c5d1 100644 --- a/internal/ui/termstatus/status_test.go +++ b/internal/ui/termstatus/status_test.go @@ -3,6 +3,7 @@ package termstatus import ( "bytes" "context" + "errors" "fmt" "io" "testing" @@ -76,3 +77,13 @@ func TestSanitizeLines(t *testing.T) { }) } } + +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) +} From e753941ad3d5a67ae539a04148e7bd9238ee68f8 Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Sun, 14 Sep 2025 19:42:33 +0200 Subject: [PATCH 13/25] move NewProgressPrinter to ui package --- cmd/restic/cmd_backup.go | 6 +- cmd/restic/cmd_cache.go | 2 +- cmd/restic/cmd_cat.go | 6 +- cmd/restic/cmd_check.go | 4 +- cmd/restic/cmd_copy.go | 6 +- cmd/restic/cmd_debug.go | 6 +- cmd/restic/cmd_diff.go | 4 +- cmd/restic/cmd_dump.go | 4 +- cmd/restic/cmd_find.go | 4 +- cmd/restic/cmd_forget.go | 2 +- cmd/restic/cmd_generate.go | 2 +- cmd/restic/cmd_init.go | 2 +- cmd/restic/cmd_key_add.go | 2 +- cmd/restic/cmd_key_list.go | 2 +- cmd/restic/cmd_key_passwd.go | 2 +- cmd/restic/cmd_key_remove.go | 2 +- cmd/restic/cmd_list.go | 2 +- cmd/restic/cmd_ls.go | 4 +- cmd/restic/cmd_migrate.go | 2 +- cmd/restic/cmd_mount.go | 4 +- cmd/restic/cmd_mount_integration_test.go | 3 +- cmd/restic/cmd_prune.go | 4 +- cmd/restic/cmd_recover.go | 4 +- cmd/restic/cmd_repair_index.go | 2 +- cmd/restic/cmd_repair_packs.go | 4 +- cmd/restic/cmd_repair_snapshots.go | 4 +- cmd/restic/cmd_restore.go | 6 +- cmd/restic/cmd_rewrite.go | 4 +- cmd/restic/cmd_rewrite_integration_test.go | 6 +- cmd/restic/cmd_self_update.go | 2 +- cmd/restic/cmd_snapshots.go | 2 +- cmd/restic/cmd_stats.go | 4 +- cmd/restic/cmd_tag.go | 2 +- cmd/restic/cmd_unlock.go | 2 +- cmd/restic/cmd_version.go | 3 +- cmd/restic/integration_helpers_test.go | 11 +-- cmd/restic/integration_test.go | 3 +- cmd/restic/progress.go | 83 ---------------------- internal/ui/progress.go | 82 +++++++++++++++++++++ 39 files changed, 151 insertions(+), 148 deletions(-) delete mode 100644 cmd/restic/progress.go create mode 100644 internal/ui/progress.go diff --git a/cmd/restic/cmd_backup.go b/cmd/restic/cmd_backup.go index 8c070b660..a69a4b46a 100644 --- a/cmd/restic/cmd_backup.go +++ b/cmd/restic/cmd_backup.go @@ -478,7 +478,7 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, ter var vsscfg fs.VSSConfig var err error - msg := newTerminalProgressPrinter(gopts.JSON, gopts.verbosity, term) + msg := ui.NewProgressPrinter(gopts.JSON, gopts.verbosity, term) if runtime.GOOS == "windows" { if vsscfg, err = fs.ParseVSSConfig(gopts.extended); err != nil { return err @@ -521,7 +521,7 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, ter progressPrinter = backup.NewTextProgress(term, gopts.verbosity) } progressReporter := backup.NewProgress(progressPrinter, - calculateProgressInterval(!gopts.Quiet, gopts.JSON, term.CanUpdateStatus())) + 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 @@ -550,7 +550,7 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, ter progressPrinter.V("load index files") } - bar := newIndexTerminalProgress(msg) + bar := ui.NewIndexCounter(msg) err = repo.LoadIndex(ctx, bar) if err != nil { return err diff --git a/cmd/restic/cmd_cache.go b/cmd/restic/cmd_cache.go index d61992f43..0640716cd 100644 --- a/cmd/restic/cmd_cache.go +++ b/cmd/restic/cmd_cache.go @@ -56,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") diff --git a/cmd/restic/cmd_cat.go b/cmd/restic/cmd_cat.go index cca356740..3c3573d66 100644 --- a/cmd/restic/cmd_cat.go +++ b/cmd/restic/cmd_cat.go @@ -65,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 @@ -168,7 +168,7 @@ func runCat(ctx context.Context, gopts GlobalOptions, args []string, term ui.Ter return err case "blob": - bar := newIndexTerminalProgress(printer) + bar := ui.NewIndexCounter(printer) err = repo.LoadIndex(ctx, bar) if err != nil { return err @@ -196,7 +196,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) + bar := ui.NewIndexCounter(printer) err = repo.LoadIndex(ctx, bar) if err != nil { return err diff --git a/cmd/restic/cmd_check.go b/cmd/restic/cmd_check.go index 04789dd4e..9897924e4 100644 --- a/cmd/restic/cmd_check.go +++ b/cmd/restic/cmd_check.go @@ -225,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) } @@ -249,7 +249,7 @@ func runCheck(ctx context.Context, opts CheckOptions, gopts GlobalOptions, args } printer.P("load indexes\n") - bar := newIndexTerminalProgress(printer) + bar := ui.NewIndexCounter(printer) hints, errs := chkr.LoadIndex(ctx, bar) if ctx.Err() != nil { return summary, ctx.Err() diff --git a/cmd/restic/cmd_copy.go b/cmd/restic/cmd_copy.go index ade86668c..cd627af3a 100644 --- a/cmd/restic/cmd_copy.go +++ b/cmd/restic/cmd_copy.go @@ -68,7 +68,7 @@ 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) + printer := ui.NewProgressPrinter(false, gopts.verbosity, term) secondaryGopts, isFromRepo, err := fillSecondaryGlobalOpts(ctx, opts.secondaryRepoOptions, gopts, "destination", printer) if err != nil { return err @@ -101,11 +101,11 @@ func runCopy(ctx context.Context, opts CopyOptions, gopts GlobalOptions, args [] } debug.Log("Loading source index") - bar := newIndexTerminalProgress(printer) + bar := ui.NewIndexCounter(printer) if err := srcRepo.LoadIndex(ctx, bar); err != nil { return err } - bar = newIndexTerminalProgress(printer) + bar = ui.NewIndexCounter(printer) debug.Log("Loading destination index") if err := dstRepo.LoadIndex(ctx, bar); err != nil { return err diff --git a/cmd/restic/cmd_debug.go b/cmd/restic/cmd_debug.go index 27041cf57..a168327eb 100644 --- a/cmd/restic/cmd_debug.go +++ b/cmd/restic/cmd_debug.go @@ -184,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") @@ -455,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") @@ -484,7 +484,7 @@ func runDebugExamine(ctx context.Context, gopts GlobalOptions, opts DebugExamine return errors.Fatal("no pack files to examine") } - bar := newIndexTerminalProgress(printer) + bar := ui.NewIndexCounter(printer) err = repo.LoadIndex(ctx, bar) if err != nil { return err diff --git a/cmd/restic/cmd_diff.go b/cmd/restic/cmd_diff.go index 0b9a4ad2a..c10f9898d 100644 --- a/cmd/restic/cmd_diff.go +++ b/cmd/restic/cmd_diff.go @@ -365,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 { @@ -391,7 +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) + bar := ui.NewIndexCounter(printer) if err = repo.LoadIndex(ctx, bar); err != nil { return err } diff --git a/cmd/restic/cmd_dump.go b/cmd/restic/cmd_dump.go index 522e4a65d..fcbd9cfd4 100644 --- a/cmd/restic/cmd_dump.go +++ b/cmd/restic/cmd_dump.go @@ -130,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": @@ -160,7 +160,7 @@ func runDump(ctx context.Context, opts DumpOptions, gopts GlobalOptions, args [] return errors.Fatalf("failed to find snapshot: %v", err) } - bar := newIndexTerminalProgress(printer) + bar := ui.NewIndexCounter(printer) err = repo.LoadIndex(ctx, bar) if err != nil { return err diff --git a/cmd/restic/cmd_find.go b/cmd/restic/cmd_find.go index a1ad9668f..dc2564a57 100644 --- a/cmd/restic/cmd_find.go +++ b/cmd/restic/cmd_find.go @@ -582,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} @@ -623,7 +623,7 @@ func runFind(ctx context.Context, opts FindOptions, gopts GlobalOptions, args [] if err != nil { return err } - bar := newIndexTerminalProgress(printer) + bar := ui.NewIndexCounter(printer) if err = repo.LoadIndex(ctx, bar); err != nil { return err } diff --git a/cmd/restic/cmd_forget.go b/cmd/restic/cmd_forget.go index 7a9a8105a..edb702842 100644 --- a/cmd/restic/cmd_forget.go +++ b/cmd/restic/cmd_forget.go @@ -186,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 diff --git a/cmd/restic/cmd_generate.go b/cmd/restic/cmd_generate.go index e2fdf7fc3..e35035b77 100644 --- a/cmd/restic/cmd_generate.go +++ b/cmd/restic/cmd_generate.go @@ -115,7 +115,7 @@ 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) + printer := ui.NewProgressPrinter(gopts.JSON, gopts.verbosity, term) cmdRoot := newRootCommand(&GlobalOptions{}) if opts.ManDir != "" { diff --git a/cmd/restic/cmd_init.go b/cmd/restic/cmd_init.go index e358ffd8a..ca5b5b770 100644 --- a/cmd/restic/cmd_init.go +++ b/cmd/restic/cmd_init.go @@ -60,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 { diff --git a/cmd/restic/cmd_key_add.go b/cmd/restic/cmd_key_add.go index 28b91dfe3..196c4a8de 100644 --- a/cmd/restic/cmd_key_add.go +++ b/cmd/restic/cmd_key_add.go @@ -59,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 diff --git a/cmd/restic/cmd_key_list.go b/cmd/restic/cmd_key_list.go index 21eee0c79..6394da75e 100644 --- a/cmd/restic/cmd_key_list.go +++ b/cmd/restic/cmd_key_list.go @@ -45,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 diff --git a/cmd/restic/cmd_key_passwd.go b/cmd/restic/cmd_key_passwd.go index 97c782989..5eacd11b3 100644 --- a/cmd/restic/cmd_key_passwd.go +++ b/cmd/restic/cmd_key_passwd.go @@ -54,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 diff --git a/cmd/restic/cmd_key_remove.go b/cmd/restic/cmd_key_remove.go index 0e0c9704a..6ab034bd9 100644 --- a/cmd/restic/cmd_key_remove.go +++ b/cmd/restic/cmd_key_remove.go @@ -42,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 diff --git a/cmd/restic/cmd_list.go b/cmd/restic/cmd_list.go index 2cbfa5e72..385cc82f6 100644 --- a/cmd/restic/cmd_list.go +++ b/cmd/restic/cmd_list.go @@ -43,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") diff --git a/cmd/restic/cmd_ls.go b/cmd/restic/cmd_ls.go index 56bb0f9b6..c3c48b97f 100644 --- a/cmd/restic/cmd_ls.go +++ b/cmd/restic/cmd_ls.go @@ -302,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'") @@ -373,7 +373,7 @@ func runLs(ctx context.Context, opts LsOptions, gopts GlobalOptions, args []stri return err } - bar := newIndexTerminalProgress(termPrinter) + bar := ui.NewIndexCounter(termPrinter) if err = repo.LoadIndex(ctx, bar); err != nil { return err } diff --git a/cmd/restic/cmd_migrate.go b/cmd/restic/cmd_migrate.go index f7ac20f4f..bacc7f24a 100644 --- a/cmd/restic/cmd_migrate.go +++ b/cmd/restic/cmd_migrate.go @@ -134,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 { diff --git a/cmd/restic/cmd_mount.go b/cmd/restic/cmd_mount.go index 6eb35f837..2b12b9b98 100644 --- a/cmd/restic/cmd_mount.go +++ b/cmd/restic/cmd_mount.go @@ -114,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") @@ -146,7 +146,7 @@ func runMount(ctx context.Context, opts MountOptions, gopts GlobalOptions, args } defer unlock() - bar := newIndexTerminalProgress(printer) + bar := ui.NewIndexCounter(printer) err = repo.LoadIndex(ctx, bar) if err != nil { return err diff --git a/cmd/restic/cmd_mount_integration_test.go b/cmd/restic/cmd_mount_integration_test.go index 91c014234..ffaeaac37 100644 --- a/cmd/restic/cmd_mount_integration_test.go +++ b/cmd/restic/cmd_mount_integration_test.go @@ -16,6 +16,7 @@ import ( "github.com/restic/restic/internal/debug" "github.com/restic/restic/internal/restic" rtest "github.com/restic/restic/internal/test" + "github.com/restic/restic/internal/ui" ) const ( @@ -128,7 +129,7 @@ func checkSnapshots(t testing.TB, gopts GlobalOptions, mountpoint string, snapsh } err := withTermStatus(gopts, func(ctx context.Context, gopts GlobalOptions) error { - printer := newTerminalProgressPrinter(gopts.JSON, gopts.verbosity, gopts.term) + printer := ui.NewProgressPrinter(gopts.JSON, gopts.verbosity, gopts.term) _, repo, unlock, err := openWithReadLock(ctx, gopts, false, printer) if err != nil { return err diff --git a/cmd/restic/cmd_prune.go b/cmd/restic/cmd_prune.go index 71e96954b..6cefc2d81 100644 --- a/cmd/restic/cmd_prune.go +++ b/cmd/restic/cmd_prune.go @@ -166,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 @@ -190,7 +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) + bar := ui.NewIndexCounter(printer) err := repo.LoadIndex(ctx, bar) if err != nil { return err diff --git a/cmd/restic/cmd_recover.go b/cmd/restic/cmd_recover.go index 2dcf51376..94570ae31 100644 --- a/cmd/restic/cmd_recover.go +++ b/cmd/restic/cmd_recover.go @@ -47,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 @@ -66,7 +66,7 @@ func runRecover(ctx context.Context, gopts GlobalOptions, term ui.Terminal) erro } printer.P("load index files\n") - bar := newIndexTerminalProgress(printer) + bar := ui.NewIndexCounter(printer) if err = repo.LoadIndex(ctx, bar); err != nil { return err } diff --git a/cmd/restic/cmd_repair_index.go b/cmd/restic/cmd_repair_index.go index 163e68a07..bff6af46e 100644 --- a/cmd/restic/cmd_repair_index.go +++ b/cmd/restic/cmd_repair_index.go @@ -69,7 +69,7 @@ func newRebuildIndexCommand(globalOptions *GlobalOptions) *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 { diff --git a/cmd/restic/cmd_repair_packs.go b/cmd/restic/cmd_repair_packs.go index 9161cdb50..237eec913 100644 --- a/cmd/restic/cmd_repair_packs.go +++ b/cmd/restic/cmd_repair_packs.go @@ -51,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 { @@ -59,7 +59,7 @@ func runRepairPacks(ctx context.Context, gopts GlobalOptions, term ui.Terminal, } defer unlock() - bar := newIndexTerminalProgress(printer) + bar := ui.NewIndexCounter(printer) err = repo.LoadIndex(ctx, bar) if err != nil { return errors.Fatalf("%s", err) diff --git a/cmd/restic/cmd_repair_snapshots.go b/cmd/restic/cmd_repair_snapshots.go index d109e1097..eacf7f2e5 100644 --- a/cmd/restic/cmd_repair_snapshots.go +++ b/cmd/restic/cmd_repair_snapshots.go @@ -74,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 { @@ -87,7 +87,7 @@ func runRepairSnapshots(ctx context.Context, gopts GlobalOptions, opts RepairOpt return err } - bar := newIndexTerminalProgress(printer) + bar := ui.NewIndexCounter(printer) if err := repo.LoadIndex(ctx, bar); err != nil { return err } diff --git a/cmd/restic/cmd_restore.go b/cmd/restic/cmd_restore.go index 5fb7a65ea..f4d89db60 100644 --- a/cmd/restic/cmd_restore.go +++ b/cmd/restic/cmd_restore.go @@ -89,7 +89,7 @@ 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) + msg := ui.NewProgressPrinter(gopts.JSON, gopts.verbosity, term) excludePatternFns, err := opts.ExcludePatternOptions.CollectPatterns(msg.E) if err != nil { return err @@ -145,7 +145,7 @@ func runRestore(ctx context.Context, opts RestoreOptions, gopts GlobalOptions, return errors.Fatalf("failed to find snapshot: %v", err) } - bar := newIndexTerminalProgress(msg) + bar := ui.NewIndexCounter(msg) err = repo.LoadIndex(ctx, bar) if err != nil { return err @@ -163,7 +163,7 @@ func runRestore(ctx context.Context, opts RestoreOptions, gopts GlobalOptions, printer = restoreui.NewTextProgress(term, gopts.verbosity) } - progress := restoreui.NewProgress(printer, calculateProgressInterval(!gopts.Quiet, gopts.JSON, term.CanUpdateStatus())) + 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, diff --git a/cmd/restic/cmd_rewrite.go b/cmd/restic/cmd_rewrite.go index 26677f7a7..45a0d6a01 100644 --- a/cmd/restic/cmd_rewrite.go +++ b/cmd/restic/cmd_rewrite.go @@ -294,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 @@ -318,7 +318,7 @@ func runRewrite(ctx context.Context, opts RewriteOptions, gopts GlobalOptions, a return err } - bar := newIndexTerminalProgress(printer) + bar := ui.NewIndexCounter(printer) if err = repo.LoadIndex(ctx, bar); err != nil { return err } diff --git a/cmd/restic/cmd_rewrite_integration_test.go b/cmd/restic/cmd_rewrite_integration_test.go index 213ef0319..37a884589 100644 --- a/cmd/restic/cmd_rewrite_integration_test.go +++ b/cmd/restic/cmd_rewrite_integration_test.go @@ -42,7 +42,7 @@ func getSnapshot(t testing.TB, snapshotID restic.ID, env *testEnvironment) *rest var snapshots []*restic.Snapshot err := withTermStatus(env.gopts, func(ctx context.Context, gopts GlobalOptions) error { - printer := newTerminalProgressPrinter(gopts.JSON, gopts.verbosity, gopts.term) + printer := ui.NewProgressPrinter(gopts.JSON, gopts.verbosity, gopts.term) ctx, repo, unlock, err := openWithReadLock(ctx, gopts, false, printer) rtest.OK(t, err) defer unlock() @@ -118,7 +118,7 @@ func testRewriteMetadata(t *testing.T, metadata snapshotMetadataArgs) { var snapshots []*restic.Snapshot err := withTermStatus(env.gopts, func(ctx context.Context, gopts GlobalOptions) error { - printer := newTerminalProgressPrinter(gopts.JSON, gopts.verbosity, gopts.term) + printer := ui.NewProgressPrinter(gopts.JSON, gopts.verbosity, gopts.term) ctx, repo, unlock, err := openWithReadLock(ctx, gopts, false, printer) rtest.OK(t, err) defer unlock() @@ -166,7 +166,7 @@ func TestRewriteSnaphotSummary(t *testing.T) { // replace snapshot by one without a summary var oldSummary *restic.SnapshotSummary err := withTermStatus(env.gopts, func(ctx context.Context, gopts GlobalOptions) error { - printer := newTerminalProgressPrinter(gopts.JSON, gopts.verbosity, gopts.term) + printer := ui.NewProgressPrinter(gopts.JSON, gopts.verbosity, gopts.term) _, repo, unlock, err := openWithExclusiveLock(ctx, gopts, false, printer) rtest.OK(t, err) defer unlock() diff --git a/cmd/restic/cmd_self_update.go b/cmd/restic/cmd_self_update.go index 5615db808..e8cd64d48 100644 --- a/cmd/restic/cmd_self_update.go +++ b/cmd/restic/cmd_self_update.go @@ -86,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) diff --git a/cmd/restic/cmd_snapshots.go b/cmd/restic/cmd_snapshots.go index 7cbe06f5f..908ad90b8 100644 --- a/cmd/restic/cmd_snapshots.go +++ b/cmd/restic/cmd_snapshots.go @@ -67,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 diff --git a/cmd/restic/cmd_stats.go b/cmd/restic/cmd_stats.go index 64230be95..27a0bd012 100644 --- a/cmd/restic/cmd_stats.go +++ b/cmd/restic/cmd_stats.go @@ -99,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 { @@ -111,7 +111,7 @@ func runStats(ctx context.Context, opts StatsOptions, gopts GlobalOptions, args if err != nil { return err } - bar := newIndexTerminalProgress(printer) + bar := ui.NewIndexCounter(printer) if err = repo.LoadIndex(ctx, bar); err != nil { return err } diff --git a/cmd/restic/cmd_tag.go b/cmd/restic/cmd_tag.go index b1f82acfa..46ded63a1 100644 --- a/cmd/restic/cmd_tag.go +++ b/cmd/restic/cmd_tag.go @@ -117,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!") diff --git a/cmd/restic/cmd_unlock.go b/cmd/restic/cmd_unlock.go index 096d21cac..91ce9105a 100644 --- a/cmd/restic/cmd_unlock.go +++ b/cmd/restic/cmd_unlock.go @@ -44,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 diff --git a/cmd/restic/cmd_version.go b/cmd/restic/cmd_version.go index a32575389..7bf968d6c 100644 --- a/cmd/restic/cmd_version.go +++ b/cmd/restic/cmd_version.go @@ -4,6 +4,7 @@ import ( "encoding/json" "runtime" + "github.com/restic/restic/internal/ui" "github.com/spf13/cobra" ) @@ -23,7 +24,7 @@ Exit status is 1 if there was any error. `, DisableAutoGenTag: true, Run: func(_ *cobra.Command, _ []string) { - printer := newTerminalProgressPrinter(globalOptions.JSON, globalOptions.verbosity, globalOptions.term) + printer := ui.NewProgressPrinter(globalOptions.JSON, globalOptions.verbosity, globalOptions.term) if globalOptions.JSON { type jsonVersion struct { diff --git a/cmd/restic/integration_helpers_test.go b/cmd/restic/integration_helpers_test.go index dc8b3eda9..4724fc818 100644 --- a/cmd/restic/integration_helpers_test.go +++ b/cmd/restic/integration_helpers_test.go @@ -20,6 +20,7 @@ import ( "github.com/restic/restic/internal/repository" "github.com/restic/restic/internal/restic" rtest "github.com/restic/restic/internal/test" + "github.com/restic/restic/internal/ui" "github.com/restic/restic/internal/ui/termstatus" ) @@ -245,7 +246,7 @@ 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, gopts GlobalOptions) error { - printer := newTerminalProgressPrinter(gopts.JSON, gopts.verbosity, gopts.term) + printer := ui.NewProgressPrinter(gopts.JSON, gopts.verbosity, gopts.term) ctx, r, unlock, err := openWithReadLock(ctx, gopts, false, printer) rtest.OK(t, err) defer unlock() @@ -264,7 +265,7 @@ 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, gopts GlobalOptions) error { - printer := newTerminalProgressPrinter(gopts.JSON, gopts.verbosity, gopts.term) + printer := ui.NewProgressPrinter(gopts.JSON, gopts.verbosity, gopts.term) ctx, r, unlock, err := openWithReadLock(ctx, gopts, false, printer) rtest.OK(t, err) defer unlock() @@ -295,7 +296,7 @@ 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, gopts GlobalOptions) error { - printer := newTerminalProgressPrinter(gopts.JSON, gopts.verbosity, gopts.term) + printer := ui.NewProgressPrinter(gopts.JSON, gopts.verbosity, gopts.term) ctx, _, unlock, err := openWithExclusiveLock(ctx, gopts, false, printer) rtest.OK(t, err) defer unlock() @@ -311,7 +312,7 @@ 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, gopts GlobalOptions) error { - printer := newTerminalProgressPrinter(gopts.JSON, gopts.verbosity, gopts.term) + printer := ui.NewProgressPrinter(gopts.JSON, gopts.verbosity, gopts.term) ctx, r, unlock, err := openWithExclusiveLock(ctx, gopts, false, printer) rtest.OK(t, err) defer unlock() @@ -372,7 +373,7 @@ 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, gopts GlobalOptions) error { - printer := newTerminalProgressPrinter(gopts.JSON, gopts.verbosity, gopts.term) + printer := ui.NewProgressPrinter(gopts.JSON, gopts.verbosity, gopts.term) _, repo, unlock, err := openWithReadLock(ctx, gopts, false, printer) rtest.OK(t, err) defer unlock() diff --git a/cmd/restic/integration_test.go b/cmd/restic/integration_test.go index c0e98e232..685304e5b 100644 --- a/cmd/restic/integration_test.go +++ b/cmd/restic/integration_test.go @@ -12,6 +12,7 @@ import ( "github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/restic" rtest "github.com/restic/restic/internal/test" + "github.com/restic/restic/internal/ui" ) func TestCheckRestoreNoLock(t *testing.T) { @@ -162,7 +163,7 @@ func TestFindListOnce(t *testing.T) { var snapshotIDs restic.IDSet rtest.OK(t, withTermStatus(env.gopts, func(ctx context.Context, gopts GlobalOptions) error { - printer := newTerminalProgressPrinter(gopts.JSON, gopts.verbosity, gopts.term) + printer := ui.NewProgressPrinter(gopts.JSON, gopts.verbosity, gopts.term) ctx, repo, unlock, err := openWithReadLock(ctx, gopts, false, printer) rtest.OK(t, err) defer unlock() diff --git a/cmd/restic/progress.go b/cmd/restic/progress.go deleted file mode 100644 index f72a052ae..000000000 --- a/cmd/restic/progress.go +++ /dev/null @@ -1,83 +0,0 @@ -package main - -import ( - "fmt" - "os" - "strconv" - "time" - - "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, canUpdateStatus 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 && !canUpdateStatus || !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, term.CanUpdateStatus()) - - 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 && t.term.OutputIsTerminal(), 0, description, t.term) -} - -func newTerminalProgressPrinter(json bool, verbosity uint, term ui.Terminal) progress.Printer { - if json { - verbosity = 0 - } - return &terminalProgressPrinter{ - term: term, - Message: *ui.NewMessage(term, verbosity), - show: verbosity > 0, - } -} - -func newIndexTerminalProgress(printer progress.Printer) *progress.Counter { - return printer.NewCounterTerminalOnly("index files loaded") -} diff --git a/internal/ui/progress.go b/internal/ui/progress.go new file mode 100644 index 000000000..9e18da5ec --- /dev/null +++ b/internal/ui/progress.go @@ -0,0 +1,82 @@ +package ui + +import ( + "fmt" + "os" + "strconv" + "time" + + "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, canUpdateStatus 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 && !canUpdateStatus || !show { + interval = 0 + } + return interval +} + +// newProgressMax returns a progress.Counter that prints to terminal if provided. +func newProgressMax(show bool, max uint64, description string, term Terminal) *progress.Counter { + if !show { + return nil + } + interval := CalculateProgressInterval(show, false, term.CanUpdateStatus()) + + 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", + FormatDuration(d), v, description) + } else { + status = fmt.Sprintf("[%s] %s %d / %d %s", + FormatDuration(d), FormatPercent(v, max), v, max, description) + } + + if final { + term.SetStatus(nil) + term.Print(status) + } else { + term.SetStatus([]string{status}) + } + }) +} + +type progressPrinter struct { + term Terminal + Message + show bool +} + +func (t *progressPrinter) NewCounter(description string) *progress.Counter { + return newProgressMax(t.show, 0, description, t.term) +} + +func (t *progressPrinter) NewCounterTerminalOnly(description string) *progress.Counter { + return newProgressMax(t.show && t.term.OutputIsTerminal(), 0, description, t.term) +} + +func NewProgressPrinter(json bool, verbosity uint, term Terminal) progress.Printer { + if json { + verbosity = 0 + } + return &progressPrinter{ + term: term, + Message: *NewMessage(term, verbosity), + show: verbosity > 0, + } +} + +func NewIndexCounter(printer progress.Printer) *progress.Counter { + return printer.NewCounterTerminalOnly("index files loaded") +} From 1a76f988ea0bcad89f4fd86a2ab13cacd17079ee Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Sun, 14 Sep 2025 19:47:54 +0200 Subject: [PATCH 14/25] backup: embed progress.Printer in backup specific printer --- cmd/restic/cmd_backup.go | 45 ++++++++++++++--------------- internal/ui/backup/json.go | 5 ++-- internal/ui/backup/progress.go | 3 +- internal/ui/backup/progress_test.go | 5 ++-- internal/ui/backup/text.go | 5 ++-- 5 files changed, 31 insertions(+), 32 deletions(-) diff --git a/cmd/restic/cmd_backup.go b/cmd/restic/cmd_backup.go index a69a4b46a..c137eafe1 100644 --- a/cmd/restic/cmd_backup.go +++ b/cmd/restic/cmd_backup.go @@ -478,7 +478,12 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, ter var vsscfg fs.VSSConfig var err error - msg := ui.NewProgressPrinter(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 @@ -490,7 +495,7 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, ter return err } - targets, err := collectTargets(opts, args, msg.E, term.InputRaw()) + targets, err := collectTargets(opts, args, printer.E, term.InputRaw()) if err != nil { return err } @@ -505,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, + 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 } @@ -539,18 +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 := ui.NewIndexCounter(msg) + bar := ui.NewIndexCounter(printer) err = repo.LoadIndex(ctx, bar) if err != nil { return err @@ -568,7 +567,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...) } } @@ -579,12 +578,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 = term.InputRaw() if opts.StdinCommand { - source, err = fs.NewCommandReader(ctx, args, msg.E) + source, err = fs.NewCommandReader(ctx, args, printer.E) if err != nil { return err } @@ -604,7 +603,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 } @@ -620,11 +619,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) }) } @@ -669,7 +668,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) diff --git a/internal/ui/backup/json.go b/internal/ui/backup/json.go index 79da353eb..b46bbdc5f 100644 --- a/internal/ui/backup/json.go +++ b/internal/ui/backup/json.go @@ -7,11 +7,12 @@ import ( "github.com/restic/restic/internal/archiver" "github.com/restic/restic/internal/restic" "github.com/restic/restic/internal/ui" + "github.com/restic/restic/internal/ui/progress" ) // JSONProgress reports progress for the `backup` command in JSON. type JSONProgress struct { - *ui.Message + progress.Printer term ui.Terminal v uint @@ -23,7 +24,7 @@ var _ ProgressPrinter = &JSONProgress{} // NewJSONProgress returns a new backup progress reporter. func NewJSONProgress(term ui.Terminal, verbosity uint) *JSONProgress { return &JSONProgress{ - Message: ui.NewMessage(term, verbosity), + Printer: ui.NewProgressPrinter(true, verbosity, term), term: term, v: verbosity, } diff --git a/internal/ui/backup/progress.go b/internal/ui/backup/progress.go index 318d30435..0feaf64eb 100644 --- a/internal/ui/backup/progress.go +++ b/internal/ui/backup/progress.go @@ -20,8 +20,7 @@ type ProgressPrinter interface { Finish(snapshotID restic.ID, summary *archiver.Summary, dryRun bool) Reset() - P(msg string, args ...interface{}) - V(msg string, args ...interface{}) + progress.Printer } type Counter struct { diff --git a/internal/ui/backup/progress_test.go b/internal/ui/backup/progress_test.go index 60e754b4a..5c088336d 100644 --- a/internal/ui/backup/progress_test.go +++ b/internal/ui/backup/progress_test.go @@ -7,10 +7,12 @@ import ( "github.com/restic/restic/internal/archiver" "github.com/restic/restic/internal/restic" + "github.com/restic/restic/internal/ui/progress" ) type mockPrinter struct { sync.Mutex + progress.NoopPrinter dirUnchanged, fileNew bool id restic.ID } @@ -42,9 +44,6 @@ func (p *mockPrinter) Finish(id restic.ID, _ *archiver.Summary, _ bool) { func (p *mockPrinter) Reset() {} -func (p *mockPrinter) P(_ string, _ ...interface{}) {} -func (p *mockPrinter) V(_ string, _ ...interface{}) {} - func TestProgress(t *testing.T) { t.Parallel() diff --git a/internal/ui/backup/text.go b/internal/ui/backup/text.go index 359331b27..e4981fdec 100644 --- a/internal/ui/backup/text.go +++ b/internal/ui/backup/text.go @@ -8,11 +8,12 @@ import ( "github.com/restic/restic/internal/archiver" "github.com/restic/restic/internal/restic" "github.com/restic/restic/internal/ui" + "github.com/restic/restic/internal/ui/progress" ) // TextProgress reports progress for the `backup` command. type TextProgress struct { - *ui.Message + progress.Printer term ui.Terminal verbosity uint @@ -24,7 +25,7 @@ var _ ProgressPrinter = &TextProgress{} // NewTextProgress returns a new backup progress reporter. func NewTextProgress(term ui.Terminal, verbosity uint) *TextProgress { return &TextProgress{ - Message: ui.NewMessage(term, verbosity), + Printer: ui.NewProgressPrinter(false, verbosity, term), term: term, verbosity: verbosity, } From 1939cff334ec5fe951cc16aab328821e0c47e91c Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Sun, 14 Sep 2025 19:51:08 +0200 Subject: [PATCH 15/25] restore: embed progress.Printer in restore-specific printer --- cmd/restic/cmd_restore.go | 37 ++++++++++++++-------------- internal/restorer/restorer_test.go | 1 + internal/ui/restore/json.go | 4 +++ internal/ui/restore/progress.go | 1 + internal/ui/restore/progress_test.go | 2 ++ internal/ui/restore/text.go | 5 ++-- 6 files changed, 29 insertions(+), 21 deletions(-) diff --git a/cmd/restic/cmd_restore.go b/cmd/restic/cmd_restore.go index f4d89db60..692c5e259 100644 --- a/cmd/restic/cmd_restore.go +++ b/cmd/restic/cmd_restore.go @@ -89,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 := ui.NewProgressPrinter(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 } @@ -130,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 } @@ -145,7 +151,7 @@ func runRestore(ctx context.Context, opts RestoreOptions, gopts GlobalOptions, return errors.Fatalf("failed to find snapshot: %v", err) } - bar := ui.NewIndexCounter(msg) + bar := ui.NewIndexCounter(printer) err = repo.LoadIndex(ctx, bar) if err != nil { return err @@ -156,13 +162,6 @@ 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, ui.CalculateProgressInterval(!gopts.Quiet, gopts.JSON, term.CanUpdateStatus())) res := restorer.NewRestorer(repo, sn, restorer.Options{ DryRun: opts.DryRun, @@ -178,13 +177,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) { @@ -232,13 +231,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) @@ -254,11 +253,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 @@ -268,7 +267,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)) } } diff --git a/internal/restorer/restorer_test.go b/internal/restorer/restorer_test.go index 0b5e34d12..39e5411f5 100644 --- a/internal/restorer/restorer_test.go +++ b/internal/restorer/restorer_test.go @@ -999,6 +999,7 @@ func TestRestorerSparseOverwrite(t *testing.T) { type printerMock struct { s restoreui.State + progress.NoopPrinter } func (p *printerMock) Update(_ restoreui.State, _ time.Duration) { diff --git a/internal/ui/restore/json.go b/internal/ui/restore/json.go index f7f7bdd1f..8e6e3cfd4 100644 --- a/internal/ui/restore/json.go +++ b/internal/ui/restore/json.go @@ -4,15 +4,19 @@ import ( "time" "github.com/restic/restic/internal/ui" + "github.com/restic/restic/internal/ui/progress" ) type jsonPrinter struct { + progress.Printer + terminal ui.Terminal verbosity uint } func NewJSONProgress(terminal ui.Terminal, verbosity uint) ProgressPrinter { return &jsonPrinter{ + Printer: ui.NewProgressPrinter(true, verbosity, terminal), terminal: terminal, verbosity: verbosity, } diff --git a/internal/ui/restore/progress.go b/internal/ui/restore/progress.go index 41367f346..fb3d5ad02 100644 --- a/internal/ui/restore/progress.go +++ b/internal/ui/restore/progress.go @@ -38,6 +38,7 @@ type ProgressPrinter interface { Error(item string, err error) error CompleteItem(action ItemAction, item string, size uint64) Finish(progress State, duration time.Duration) + progress.Printer } type ItemAction string diff --git a/internal/ui/restore/progress_test.go b/internal/ui/restore/progress_test.go index b6f72726c..4c59d11ee 100644 --- a/internal/ui/restore/progress_test.go +++ b/internal/ui/restore/progress_test.go @@ -6,6 +6,7 @@ import ( "github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/test" + "github.com/restic/restic/internal/ui/progress" ) type printerTraceEntry struct { @@ -36,6 +37,7 @@ type mockPrinter struct { trace printerTrace items itemTrace errors errorTrace + progress.NoopPrinter } const mockFinishDuration = 42 * time.Second diff --git a/internal/ui/restore/text.go b/internal/ui/restore/text.go index 35c9db029..464615ecb 100644 --- a/internal/ui/restore/text.go +++ b/internal/ui/restore/text.go @@ -5,17 +5,18 @@ import ( "time" "github.com/restic/restic/internal/ui" + "github.com/restic/restic/internal/ui/progress" ) type textPrinter struct { - *ui.Message + progress.Printer terminal ui.Terminal } func NewTextProgress(terminal ui.Terminal, verbosity uint) ProgressPrinter { return &textPrinter{ - Message: ui.NewMessage(terminal, verbosity), + Printer: ui.NewProgressPrinter(false, verbosity, terminal), terminal: terminal, } } From c293736841a07008fcf95de564543a6fe0c1b370 Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Thu, 18 Sep 2025 22:40:36 +0200 Subject: [PATCH 16/25] drop unused stderr from GlobalOptions --- cmd/restic/cmd_backup_integration_test.go | 5 +-- cmd/restic/cmd_check_integration_test.go | 8 ++--- cmd/restic/cmd_copy_integration_test.go | 2 +- cmd/restic/cmd_diff_integration_test.go | 14 ++++---- cmd/restic/cmd_find_integration_test.go | 2 +- cmd/restic/cmd_forget_integration_test.go | 12 +++---- cmd/restic/cmd_generate_integration_test.go | 8 ++--- cmd/restic/cmd_init_integration_test.go | 10 +++--- cmd/restic/cmd_key_integration_test.go | 36 +++++++++---------- cmd/restic/cmd_list_integration_test.go | 2 +- cmd/restic/cmd_ls_integration_test.go | 2 +- cmd/restic/cmd_mount_integration_test.go | 4 +-- cmd/restic/cmd_prune_integration_test.go | 18 +++++----- cmd/restic/cmd_recover_integration_test.go | 4 +-- .../cmd_repair_index_integration_test.go | 8 ++--- .../cmd_repair_snapshots_integration_test.go | 8 ++--- cmd/restic/cmd_restore_integration_test.go | 18 +++++----- cmd/restic/cmd_rewrite_integration_test.go | 12 +++---- cmd/restic/cmd_snapshots_integration_test.go | 2 +- cmd/restic/cmd_tag_integration_test.go | 2 +- cmd/restic/global.go | 1 - cmd/restic/integration_filter_pattern_test.go | 16 ++++----- cmd/restic/integration_helpers_test.go | 15 ++++---- cmd/restic/integration_test.go | 8 ++--- cmd/restic/main.go | 3 +- internal/ui/termstatus/stdio_wrapper.go | 6 ++-- 26 files changed, 109 insertions(+), 117 deletions(-) diff --git a/cmd/restic/cmd_backup_integration_test.go b/cmd/restic/cmd_backup_integration_test.go index 1c59db852..004b09a1d 100644 --- a/cmd/restic/cmd_backup_integration_test.go +++ b/cmd/restic/cmd_backup_integration_test.go @@ -3,7 +3,6 @@ package main import ( "context" "fmt" - "io" "os" "path/filepath" "runtime" @@ -16,7 +15,7 @@ import ( ) func testRunBackupAssumeFailure(t testing.TB, dir string, target []string, opts BackupOptions, gopts GlobalOptions) error { - return withTermStatus(gopts, func(ctx context.Context, gopts GlobalOptions) 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) @@ -261,8 +260,6 @@ func TestBackupNonExistingFile(t *testing.T) { testSetupBackupData(t, env) - env.gopts.stderr = io.Discard - p := filepath.Join(env.testdata, "0", "0", "9") dirs := []string{ filepath.Join(p, "0"), diff --git a/cmd/restic/cmd_check_integration_test.go b/cmd/restic/cmd_check_integration_test.go index b87bd1149..fd913c4cb 100644 --- a/cmd/restic/cmd_check_integration_test.go +++ b/cmd/restic/cmd_check_integration_test.go @@ -9,7 +9,7 @@ import ( 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) @@ -18,13 +18,13 @@ 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) { +func testRunCheckOutput(t testing.TB, gopts GlobalOptions, checkUnused bool) (string, error) { buf, err := withCaptureStdout(gopts, func(gopts GlobalOptions) error { - return withTermStatus(gopts, func(ctx context.Context, gopts GlobalOptions) error { + return withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error { opts := CheckOptions{ ReadData: true, CheckUnused: checkUnused, diff --git a/cmd/restic/cmd_copy_integration_test.go b/cmd/restic/cmd_copy_integration_test.go index c3b529a6c..73bc038dd 100644 --- a/cmd/restic/cmd_copy_integration_test.go +++ b/cmd/restic/cmd_copy_integration_test.go @@ -22,7 +22,7 @@ func testRunCopy(t testing.TB, srcGopts GlobalOptions, dstGopts GlobalOptions) { }, } - rtest.OK(t, withTermStatus(gopts, func(ctx context.Context, gopts GlobalOptions) error { + rtest.OK(t, withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error { return runCopy(context.TODO(), copyOpts, gopts, nil, gopts.term) })) } diff --git a/cmd/restic/cmd_diff_integration_test.go b/cmd/restic/cmd_diff_integration_test.go index 14cd33d6d..4143789c7 100644 --- a/cmd/restic/cmd_diff_integration_test.go +++ b/cmd/restic/cmd_diff_integration_test.go @@ -14,12 +14,12 @@ import ( rtest "github.com/restic/restic/internal/test" ) -func testRunDiffOutput(gopts GlobalOptions, firstSnapshotID string, secondSnapshotID string) (string, error) { +func testRunDiffOutput(t testing.TB, gopts GlobalOptions, firstSnapshotID string, secondSnapshotID string) (string, error) { buf, err := withCaptureStdout(gopts, func(gopts GlobalOptions) error { opts := DiffOptions{ ShowMetadata: false, } - return withTermStatus(gopts, func(ctx context.Context, gopts GlobalOptions) error { + return withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error { return runDiff(ctx, opts, gopts, []string{firstSnapshotID, secondSnapshotID}, gopts.term) }) }) @@ -125,10 +125,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 { @@ -139,7 +139,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)) @@ -156,7 +156,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 @@ -183,7 +183,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{} diff --git a/cmd/restic/cmd_find_integration_test.go b/cmd/restic/cmd_find_integration_test.go index 834228664..f4dca38b4 100644 --- a/cmd/restic/cmd_find_integration_test.go +++ b/cmd/restic/cmd_find_integration_test.go @@ -14,7 +14,7 @@ func testRunFind(t testing.TB, wantJSON bool, opts FindOptions, gopts GlobalOpti buf, err := withCaptureStdout(gopts, func(gopts GlobalOptions) error { gopts.JSON = wantJSON - return withTermStatus(gopts, func(ctx context.Context, gopts GlobalOptions) error { + return withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error { return runFind(ctx, opts, gopts, []string{pattern}, gopts.term) }) }) diff --git a/cmd/restic/cmd_forget_integration_test.go b/cmd/restic/cmd_forget_integration_test.go index 0a110cc70..3ce19182b 100644 --- a/cmd/restic/cmd_forget_integration_test.go +++ b/cmd/restic/cmd_forget_integration_test.go @@ -10,17 +10,17 @@ import ( rtest "github.com/restic/restic/internal/test" ) -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, gopts GlobalOptions) error { + 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) { @@ -37,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 diff --git a/cmd/restic/cmd_generate_integration_test.go b/cmd/restic/cmd_generate_integration_test.go index c1354a5cb..e6a426a9f 100644 --- a/cmd/restic/cmd_generate_integration_test.go +++ b/cmd/restic/cmd_generate_integration_test.go @@ -8,9 +8,9 @@ import ( rtest "github.com/restic/restic/internal/test" ) -func testRunGenerate(gopts GlobalOptions, opts generateOptions) ([]byte, error) { +func testRunGenerate(t testing.TB, gopts GlobalOptions, opts generateOptions) ([]byte, error) { buf, err := withCaptureStdout(gopts, func(gopts GlobalOptions) error { - return withTermStatus(gopts, func(ctx context.Context, gopts GlobalOptions) error { + return withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error { return runGenerate(opts, gopts, []string{}, gopts.term) }) }) @@ -30,14 +30,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") }) } diff --git a/cmd/restic/cmd_init_integration_test.go b/cmd/restic/cmd_init_integration_test.go index 2fbe88fc4..174b17360 100644 --- a/cmd/restic/cmd_init_integration_test.go +++ b/cmd/restic/cmd_init_integration_test.go @@ -17,7 +17,7 @@ func testRunInit(t testing.TB, gopts GlobalOptions) { restic.TestDisableCheckPolynomial(t) restic.TestSetLockTimeout(t, 0) - err := withTermStatus(gopts, func(ctx context.Context, gopts GlobalOptions) error { + err := withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error { return runInit(ctx, InitOptions{}, gopts, nil, gopts.term) }) rtest.OK(t, err) @@ -43,26 +43,26 @@ func TestInitCopyChunkerParams(t *testing.T) { password: env2.gopts.password, }, } - err := withTermStatus(env.gopts, func(ctx context.Context, gopts GlobalOptions) error { + 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, gopts GlobalOptions) error { + err = withTermStatus(t, env.gopts, func(ctx context.Context, gopts GlobalOptions) error { return runInit(ctx, initOpts, gopts, nil, gopts.term) }) rtest.OK(t, err) var repo *repository.Repository - err = withTermStatus(env.gopts, func(ctx context.Context, gopts GlobalOptions) error { + 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) var otherRepo *repository.Repository - err = withTermStatus(env2.gopts, func(ctx context.Context, gopts GlobalOptions) error { + err = withTermStatus(t, env2.gopts, func(ctx context.Context, gopts GlobalOptions) error { otherRepo, err = OpenRepository(ctx, gopts, &progress.NoopPrinter{}) return err }) diff --git a/cmd/restic/cmd_key_integration_test.go b/cmd/restic/cmd_key_integration_test.go index dad7f7e67..48fbbe6ba 100644 --- a/cmd/restic/cmd_key_integration_test.go +++ b/cmd/restic/cmd_key_integration_test.go @@ -17,7 +17,7 @@ import ( func testRunKeyListOtherIDs(t testing.TB, gopts GlobalOptions) []string { buf, err := withCaptureStdout(gopts, func(gopts GlobalOptions) error { - return withTermStatus(gopts, func(ctx context.Context, gopts GlobalOptions) error { + return withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error { return runKeyList(ctx, gopts, []string{}, gopts.term) }) }) @@ -42,7 +42,7 @@ func testRunKeyAddNewKey(t testing.TB, newPassword string, gopts GlobalOptions) testKeyNewPassword = "" }() - err := withTermStatus(gopts, func(ctx context.Context, gopts GlobalOptions) error { + err := withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error { return runKeyAdd(ctx, gopts, KeyAddOptions{}, []string{}, gopts.term) }) rtest.OK(t, err) @@ -55,7 +55,7 @@ func testRunKeyAddNewKeyUserHost(t testing.TB, gopts GlobalOptions) { }() t.Log("adding key for john@example.com") - err := withTermStatus(gopts, func(ctx context.Context, gopts GlobalOptions) error { + err := withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error { return runKeyAdd(ctx, gopts, KeyAddOptions{ Username: "john", Hostname: "example.com", @@ -63,7 +63,7 @@ func testRunKeyAddNewKeyUserHost(t testing.TB, gopts GlobalOptions) { }) rtest.OK(t, err) - _ = withTermStatus(gopts, func(ctx context.Context, gopts GlobalOptions) error { + _ = 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, "") @@ -81,7 +81,7 @@ func testRunKeyPasswd(t testing.TB, newPassword string, gopts GlobalOptions) { testKeyNewPassword = "" }() - err := withTermStatus(gopts, func(ctx context.Context, gopts GlobalOptions) error { + err := withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error { return runKeyPasswd(ctx, gopts, KeyPasswdOptions{}, []string{}, gopts.term) }) rtest.OK(t, err) @@ -90,7 +90,7 @@ 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, gopts GlobalOptions) error { + err := withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error { return runKeyRemove(ctx, gopts, []string{id}, gopts.term) }) rtest.OK(t, err) @@ -123,7 +123,7 @@ 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, gopts GlobalOptions) error { + err := withTermStatus(t, env.gopts, func(ctx context.Context, gopts GlobalOptions) error { return runKeyList(ctx, gopts, []string{}, gopts.term) }) rtest.OK(t, err) @@ -137,7 +137,7 @@ func TestKeyAddInvalid(t *testing.T) { defer cleanup() testRunInit(t, env.gopts) - err := withTermStatus(env.gopts, func(ctx context.Context, gopts GlobalOptions) error { + err := withTermStatus(t, env.gopts, func(ctx context.Context, gopts GlobalOptions) error { return runKeyAdd(ctx, gopts, KeyAddOptions{ NewPasswordFile: "some-file", InsecureNoPassword: true, @@ -148,7 +148,7 @@ func TestKeyAddInvalid(t *testing.T) { pwfile := filepath.Join(t.TempDir(), "pwfile") rtest.OK(t, os.WriteFile(pwfile, []byte{}, 0o666)) - err = withTermStatus(env.gopts, func(ctx context.Context, gopts GlobalOptions) error { + err = withTermStatus(t, env.gopts, func(ctx context.Context, gopts GlobalOptions) error { return runKeyAdd(ctx, gopts, KeyAddOptions{ NewPasswordFile: pwfile, }, []string{}, gopts.term) @@ -163,7 +163,7 @@ func TestKeyAddEmpty(t *testing.T) { defer cleanup() testRunInit(t, env.gopts) - err := withTermStatus(env.gopts, func(ctx context.Context, gopts GlobalOptions) error { + err := withTermStatus(t, env.gopts, func(ctx context.Context, gopts GlobalOptions) error { return runKeyAdd(ctx, gopts, KeyAddOptions{ InsecureNoPassword: true, }, []string{}, gopts.term) @@ -198,20 +198,20 @@ func TestKeyProblems(t *testing.T) { testKeyNewPassword = "" }() - err := withTermStatus(env.gopts, func(ctx context.Context, gopts GlobalOptions) error { + 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, gopts GlobalOptions) error { + 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, gopts GlobalOptions) error { + err = withTermStatus(t, env.gopts, func(ctx context.Context, gopts GlobalOptions) error { return runKeyList(ctx, gopts, []string{}, gopts.term) }) rtest.OK(t, err) @@ -227,31 +227,31 @@ func TestKeyCommandInvalidArguments(t *testing.T) { return &emptySaveBackend{r}, nil } - err := withTermStatus(env.gopts, func(ctx context.Context, gopts GlobalOptions) error { + 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, gopts GlobalOptions) error { + 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, gopts GlobalOptions) error { + 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, gopts GlobalOptions) error { + 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, gopts GlobalOptions) error { + err = withTermStatus(t, env.gopts, func(ctx context.Context, gopts GlobalOptions) error { return runKeyRemove(ctx, gopts, []string{"john", "doe"}, gopts.term) }) t.Log(err) diff --git a/cmd/restic/cmd_list_integration_test.go b/cmd/restic/cmd_list_integration_test.go index 58b240c5c..d18c0715e 100644 --- a/cmd/restic/cmd_list_integration_test.go +++ b/cmd/restic/cmd_list_integration_test.go @@ -12,7 +12,7 @@ import ( func testRunList(t testing.TB, gopts GlobalOptions, tpe string) restic.IDs { buf, err := withCaptureStdout(gopts, func(gopts GlobalOptions) error { - return withTermStatus(gopts, func(ctx context.Context, gopts GlobalOptions) error { + return withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error { return runList(ctx, gopts, []string{tpe}, gopts.term) }) }) diff --git a/cmd/restic/cmd_ls_integration_test.go b/cmd/restic/cmd_ls_integration_test.go index a4a54c081..cda8bbb54 100644 --- a/cmd/restic/cmd_ls_integration_test.go +++ b/cmd/restic/cmd_ls_integration_test.go @@ -15,7 +15,7 @@ import ( func testRunLsWithOpts(t testing.TB, gopts GlobalOptions, opts LsOptions, args []string) []byte { buf, err := withCaptureStdout(gopts, func(gopts GlobalOptions) error { gopts.Quiet = true - return withTermStatus(gopts, func(ctx context.Context, gopts GlobalOptions) error { + return withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error { return runLs(context.TODO(), opts, gopts, args, gopts.term) }) }) diff --git a/cmd/restic/cmd_mount_integration_test.go b/cmd/restic/cmd_mount_integration_test.go index ffaeaac37..9c76b1b9a 100644 --- a/cmd/restic/cmd_mount_integration_test.go +++ b/cmd/restic/cmd_mount_integration_test.go @@ -62,7 +62,7 @@ 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, gopts GlobalOptions) error { + rtest.OK(t, withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error { return runMount(context.TODO(), opts, gopts, []string{dir}, gopts.term) })) } @@ -128,7 +128,7 @@ func checkSnapshots(t testing.TB, gopts GlobalOptions, mountpoint string, snapsh } } - err := withTermStatus(gopts, func(ctx context.Context, gopts GlobalOptions) error { + 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 { diff --git a/cmd/restic/cmd_prune_integration_test.go b/cmd/restic/cmd_prune_integration_test.go index df850feff..d56aa4afd 100644 --- a/cmd/restic/cmd_prune_integration_test.go +++ b/cmd/restic/cmd_prune_integration_test.go @@ -13,22 +13,22 @@ import ( 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, gopts GlobalOptions) error { + return withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error { return runPrune(context.TODO(), opts, gopts, gopts.term) }) } @@ -98,7 +98,7 @@ func testRunForgetJSON(t testing.TB, gopts GlobalOptions, args ...string) { pruneOpts := PruneOptions{ MaxUnused: "5%", } - return withTermStatus(gopts, func(ctx context.Context, gopts GlobalOptions) error { + return withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error { return runForget(context.TODO(), opts, pruneOpts, gopts, gopts.term, args) }) }) @@ -121,7 +121,7 @@ func testPrune(t *testing.T, pruneOpts PruneOptions, checkOpts CheckOptions) { createPrunableRepo(t, env) testRunPrune(t, env.gopts, pruneOpts) - rtest.OK(t, withTermStatus(env.gopts, func(ctx context.Context, gopts GlobalOptions) error { + 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 })) @@ -157,7 +157,7 @@ func TestPruneWithDamagedRepository(t *testing.T) { env.gopts.backendTestHook = oldHook }() // prune should fail - rtest.Equals(t, repository.ErrPacksMissing, withTermStatus(env.gopts, func(ctx context.Context, gopts GlobalOptions) error { + 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") } @@ -230,7 +230,7 @@ func testEdgeCaseRepo(t *testing.T, tarfile string, optionsCheck CheckOptions, o if checkOK { testRunCheck(t, env.gopts) } else { - rtest.Assert(t, withTermStatus(env.gopts, func(ctx context.Context, gopts GlobalOptions) error { + 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, @@ -241,7 +241,7 @@ func testEdgeCaseRepo(t *testing.T, tarfile string, optionsCheck CheckOptions, o testRunPrune(t, env.gopts, optionsPrune) testRunCheck(t, env.gopts) } else { - rtest.Assert(t, withTermStatus(env.gopts, func(ctx context.Context, gopts GlobalOptions) error { + 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") diff --git a/cmd/restic/cmd_recover_integration_test.go b/cmd/restic/cmd_recover_integration_test.go index 5d51ee2d9..cbcd2b019 100644 --- a/cmd/restic/cmd_recover_integration_test.go +++ b/cmd/restic/cmd_recover_integration_test.go @@ -8,7 +8,7 @@ import ( ) func testRunRecover(t testing.TB, gopts GlobalOptions) { - rtest.OK(t, withTermStatus(gopts, func(ctx context.Context, gopts GlobalOptions) error { + rtest.OK(t, withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error { return runRecover(context.TODO(), gopts, gopts.term) })) } @@ -32,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, gopts GlobalOptions) error { + 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) })) } diff --git a/cmd/restic/cmd_repair_index_integration_test.go b/cmd/restic/cmd_repair_index_integration_test.go index bd9924e63..0319a77b4 100644 --- a/cmd/restic/cmd_repair_index_integration_test.go +++ b/cmd/restic/cmd_repair_index_integration_test.go @@ -16,7 +16,7 @@ import ( ) func testRunRebuildIndex(t testing.TB, gopts GlobalOptions) { - rtest.OK(t, withTermStatus(gopts, func(ctx context.Context, gopts GlobalOptions) error { + rtest.OK(t, withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error { gopts.stdout = io.Discard return runRebuildIndex(context.TODO(), RepairIndexOptions{}, gopts, gopts.term) })) @@ -29,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") } @@ -46,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,7 +128,7 @@ func TestRebuildIndexFailsOnAppendOnly(t *testing.T) { env.gopts.backendTestHook = func(r backend.Backend) (backend.Backend, error) { return &appendOnlyBackend{r}, nil } - err := withTermStatus(env.gopts, func(ctx context.Context, gopts GlobalOptions) error { + err := withTermStatus(t, env.gopts, func(ctx context.Context, gopts GlobalOptions) error { gopts.stdout = io.Discard return runRebuildIndex(context.TODO(), RepairIndexOptions{}, gopts, gopts.term) }) diff --git a/cmd/restic/cmd_repair_snapshots_integration_test.go b/cmd/restic/cmd_repair_snapshots_integration_test.go index 1173461ec..942f03d0b 100644 --- a/cmd/restic/cmd_repair_snapshots_integration_test.go +++ b/cmd/restic/cmd_repair_snapshots_integration_test.go @@ -19,7 +19,7 @@ func testRunRepairSnapshot(t testing.TB, gopts GlobalOptions, forget bool) { Forget: forget, } - rtest.OK(t, withTermStatus(gopts, func(ctx context.Context, gopts GlobalOptions) error { + rtest.OK(t, withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error { return runRepairSnapshots(context.TODO(), gopts, opts, nil, gopts.term) })) } @@ -66,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) } @@ -95,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) } @@ -118,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) } diff --git a/cmd/restic/cmd_restore_integration_test.go b/cmd/restic/cmd_restore_integration_test.go index 3c2403685..7716555d4 100644 --- a/cmd/restic/cmd_restore_integration_test.go +++ b/cmd/restic/cmd_restore_integration_test.go @@ -3,7 +3,6 @@ package main import ( "context" "fmt" - "io" "math/rand" "os" "path/filepath" @@ -26,11 +25,11 @@ 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, gopts GlobalOptions) error { +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}) }) } @@ -44,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) { @@ -53,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) { @@ -62,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) { @@ -71,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) { @@ -92,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) } @@ -336,7 +335,6 @@ func TestRestoreWithPermissionFailure(t *testing.T) { snapshots := testListSnapshots(t, env.gopts, 1) - env.gopts.stderr = io.Discard testRunRestore(t, env.gopts, filepath.Join(env.base, "restore"), snapshots[0].String()) // make sure that all files have been restored, regardless of any diff --git a/cmd/restic/cmd_rewrite_integration_test.go b/cmd/restic/cmd_rewrite_integration_test.go index 37a884589..b43f80dd0 100644 --- a/cmd/restic/cmd_rewrite_integration_test.go +++ b/cmd/restic/cmd_rewrite_integration_test.go @@ -20,7 +20,7 @@ func testRunRewriteExclude(t testing.TB, gopts GlobalOptions, excludes []string, Metadata: metadata, } - rtest.OK(t, withTermStatus(gopts, func(ctx context.Context, gopts GlobalOptions) error { + rtest.OK(t, withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error { return runRewrite(context.TODO(), opts, gopts, nil, gopts.term) })) } @@ -41,7 +41,7 @@ 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, gopts GlobalOptions) error { + 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) @@ -117,7 +117,7 @@ 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, gopts GlobalOptions) error { + 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) @@ -157,7 +157,7 @@ func TestRewriteSnaphotSummary(t *testing.T) { defer cleanup() createBasicRewriteRepo(t, env) - rtest.OK(t, withTermStatus(env.gopts, func(ctx context.Context, gopts GlobalOptions) error { + 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 @@ -165,7 +165,7 @@ func TestRewriteSnaphotSummary(t *testing.T) { // replace snapshot by one without a summary var oldSummary *restic.SnapshotSummary - err := withTermStatus(env.gopts, func(ctx context.Context, gopts GlobalOptions) error { + 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) @@ -182,7 +182,7 @@ 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, gopts GlobalOptions) error { + 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) diff --git a/cmd/restic/cmd_snapshots_integration_test.go b/cmd/restic/cmd_snapshots_integration_test.go index af45fde9c..00e60afd8 100644 --- a/cmd/restic/cmd_snapshots_integration_test.go +++ b/cmd/restic/cmd_snapshots_integration_test.go @@ -14,7 +14,7 @@ func testRunSnapshots(t testing.TB, gopts GlobalOptions) (newest *Snapshot, snap gopts.JSON = true opts := SnapshotOptions{} - return withTermStatus(gopts, func(ctx context.Context, gopts GlobalOptions) error { + return withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error { return runSnapshots(ctx, opts, gopts, []string{}, gopts.term) }) }) diff --git a/cmd/restic/cmd_tag_integration_test.go b/cmd/restic/cmd_tag_integration_test.go index 9958be485..3a92cc7b0 100644 --- a/cmd/restic/cmd_tag_integration_test.go +++ b/cmd/restic/cmd_tag_integration_test.go @@ -9,7 +9,7 @@ import ( ) func testRunTag(t testing.TB, opts TagOptions, gopts GlobalOptions) { - rtest.OK(t, withTermStatus(gopts, func(ctx context.Context, gopts GlobalOptions) error { + rtest.OK(t, withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error { return runTag(context.TODO(), opts, gopts, gopts.term, []string{}) })) } diff --git a/cmd/restic/global.go b/cmd/restic/global.go index 0170830f0..7cbbad651 100644 --- a/cmd/restic/global.go +++ b/cmd/restic/global.go @@ -75,7 +75,6 @@ type GlobalOptions struct { password string stdout io.Writer - stderr io.Writer term ui.Terminal backends *location.Registry diff --git a/cmd/restic/integration_filter_pattern_test.go b/cmd/restic/integration_filter_pattern_test.go index 46badbe4f..c3281e36a 100644 --- a/cmd/restic/integration_filter_pattern_test.go +++ b/cmd/restic/integration_filter_pattern_test.go @@ -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()) diff --git a/cmd/restic/integration_helpers_test.go b/cmd/restic/integration_helpers_test.go index 4724fc818..790c54c36 100644 --- a/cmd/restic/integration_helpers_test.go +++ b/cmd/restic/integration_helpers_test.go @@ -216,7 +216,6 @@ func withTestEnvironment(t testing.TB) (env *testEnvironment, cleanup func()) { // 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 @@ -245,7 +244,7 @@ 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, gopts GlobalOptions) error { + 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) @@ -264,7 +263,7 @@ 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, gopts GlobalOptions) error { + 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) @@ -295,7 +294,7 @@ 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, gopts GlobalOptions) error { + 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) @@ -311,7 +310,7 @@ 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, gopts GlobalOptions) error { + 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) @@ -372,7 +371,7 @@ 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, gopts GlobalOptions) error { + 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) @@ -424,11 +423,11 @@ func withCaptureStdout(gopts GlobalOptions, inner func(gopts GlobalOptions) erro return buf, err } -func withTermStatus(gopts GlobalOptions, callback func(ctx context.Context, gopts GlobalOptions) error) error { +func withTermStatus(t testing.TB, gopts GlobalOptions, callback func(ctx context.Context, gopts GlobalOptions) error) error { ctx, cancel := context.WithCancel(context.TODO()) var wg sync.WaitGroup - term := termstatus.New(os.Stdin, gopts.stdout, gopts.stderr, gopts.Quiet) + term := termstatus.New(os.Stdin, gopts.stdout, &logOutputter{t: t}, gopts.Quiet) gopts.term = term wg.Add(1) go func() { diff --git a/cmd/restic/integration_test.go b/cmd/restic/integration_test.go index 685304e5b..dcad82c9b 100644 --- a/cmd/restic/integration_test.go +++ b/cmd/restic/integration_test.go @@ -87,14 +87,14 @@ func TestListOnce(t *testing.T) { createPrunableRepo(t, env) testRunPrune(t, env.gopts, pruneOpts) - rtest.OK(t, withTermStatus(env.gopts, func(ctx context.Context, gopts GlobalOptions) error { + 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, gopts GlobalOptions) error { + 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, gopts GlobalOptions) error { + 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,7 +162,7 @@ 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, gopts GlobalOptions) error { + 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) diff --git a/cmd/restic/main.go b/cmd/restic/main.go index 1c4fa5e9c..b5b8d592f 100644 --- a/cmd/restic/main.go +++ b/cmd/restic/main.go @@ -174,13 +174,12 @@ func main() { globalOptions := GlobalOptions{ stdout: os.Stdout, - stderr: os.Stderr, backends: collectBackends(), } func() { term, cancel := termstatus.Setup(os.Stdin, os.Stdout, os.Stderr, globalOptions.Quiet) defer cancel() - globalOptions.stdout, globalOptions.stderr = termstatus.WrapStdio(term) + globalOptions.stdout = termstatus.WrapStdout(term) globalOptions.term = term ctx := createGlobalContext(os.Stderr) err = newRootCommand(&globalOptions).ExecuteContext(ctx) diff --git a/internal/ui/termstatus/stdio_wrapper.go b/internal/ui/termstatus/stdio_wrapper.go index 4909c0fa3..384357d98 100644 --- a/internal/ui/termstatus/stdio_wrapper.go +++ b/internal/ui/termstatus/stdio_wrapper.go @@ -6,10 +6,10 @@ import ( "sync" ) -// WrapStdio returns line-buffering replacements for os.Stdout and os.Stderr. +// WrapStdout returns line-buffering replacements for os.Stdout. // On Close, the remaining bytes are written, followed by a line break. -func WrapStdio(term *Terminal) (stdout, stderr io.WriteCloser) { - return newLineWriter(term.Print), newLineWriter(term.Error) +func WrapStdout(term *Terminal) (stdout io.WriteCloser) { + return newLineWriter(term.Print) } type lineWriter struct { From 76b2cdd4fbec324947b13c1238517ea1070e4f28 Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Sat, 20 Sep 2025 23:06:28 +0200 Subject: [PATCH 17/25] replace globalOptions.stdout with termstatus.OutputWriter --- cmd/restic/cmd_cache.go | 2 +- cmd/restic/cmd_check_integration_test.go | 16 +++++++--------- cmd/restic/cmd_debug.go | 10 +++++----- cmd/restic/cmd_diff.go | 4 ++-- cmd/restic/cmd_diff_integration_test.go | 6 ++---- cmd/restic/cmd_features.go | 2 +- cmd/restic/cmd_find_integration_test.go | 6 ++---- cmd/restic/cmd_forget.go | 8 ++++---- cmd/restic/cmd_generate.go | 2 +- cmd/restic/cmd_generate_integration_test.go | 6 ++---- cmd/restic/cmd_init.go | 2 +- cmd/restic/cmd_key_integration_test.go | 6 ++---- cmd/restic/cmd_key_list.go | 4 ++-- cmd/restic/cmd_list_integration_test.go | 6 ++---- cmd/restic/cmd_ls.go | 4 ++-- cmd/restic/cmd_ls_integration_test.go | 6 ++---- cmd/restic/cmd_prune_integration_test.go | 6 ++---- .../cmd_repair_index_integration_test.go | 4 ++-- cmd/restic/cmd_snapshots.go | 6 +++--- cmd/restic/cmd_snapshots_integration_test.go | 6 ++---- cmd/restic/cmd_stats.go | 2 +- cmd/restic/cmd_version.go | 2 +- cmd/restic/global.go | 2 -- cmd/restic/integration_helpers_test.go | 18 ++++++++++-------- cmd/restic/main.go | 2 -- internal/ui/mock.go | 4 ++++ internal/ui/terminal.go | 3 +++ internal/ui/termstatus/status.go | 13 +++++++++++++ internal/ui/termstatus/stdio_wrapper.go | 6 ------ 29 files changed, 79 insertions(+), 85 deletions(-) diff --git a/cmd/restic/cmd_cache.go b/cmd/restic/cmd_cache.go index 0640716cd..da6537fcb 100644 --- a/cmd/restic/cmd_cache.go +++ b/cmd/restic/cmd_cache.go @@ -161,7 +161,7 @@ func runCache(opts CacheOptions, gopts GlobalOptions, args []string, term ui.Ter }) } - _ = tab.Write(gopts.stdout) + _ = tab.Write(gopts.term.OutputWriter()) printer.S("%d cache dirs in %s", len(dirs), cachedir) return nil diff --git a/cmd/restic/cmd_check_integration_test.go b/cmd/restic/cmd_check_integration_test.go index fd913c4cb..c28a3ed4f 100644 --- a/cmd/restic/cmd_check_integration_test.go +++ b/cmd/restic/cmd_check_integration_test.go @@ -23,15 +23,13 @@ func testRunCheckMustFail(t testing.TB, gopts GlobalOptions) { } func testRunCheckOutput(t testing.TB, gopts GlobalOptions, checkUnused bool) (string, error) { - buf, err := withCaptureStdout(gopts, func(gopts GlobalOptions) error { - return withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error { - opts := CheckOptions{ - ReadData: true, - CheckUnused: checkUnused, - } - _, err := runCheck(context.TODO(), opts, gopts, nil, gopts.term) - return err - }) + 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, gopts.term) + return err }) return buf.String(), err } diff --git a/cmd/restic/cmd_debug.go b/cmd/restic/cmd_debug.go index a168327eb..4709fffec 100644 --- a/cmd/restic/cmd_debug.go +++ b/cmd/restic/cmd_debug.go @@ -200,20 +200,20 @@ func runDebugDump(ctx context.Context, gopts GlobalOptions, args []string, term switch tpe { case "indexes": - return dumpIndexes(ctx, repo, gopts.stdout, printer) + return dumpIndexes(ctx, repo, gopts.term.OutputWriter(), printer) case "snapshots": - return debugPrintSnapshots(ctx, repo, gopts.stdout) + return debugPrintSnapshots(ctx, repo, gopts.term.OutputWriter()) case "packs": - return printPacks(ctx, repo, gopts.stdout, printer) + return printPacks(ctx, repo, gopts.term.OutputWriter(), printer) case "all": printer.S("snapshots:") - err := debugPrintSnapshots(ctx, repo, gopts.stdout) + err := debugPrintSnapshots(ctx, repo, gopts.term.OutputWriter()) if err != nil { return err } printer.S("indexes:") - err = dumpIndexes(ctx, repo, gopts.stdout, printer) + err = dumpIndexes(ctx, repo, gopts.term.OutputWriter(), printer) if err != nil { return err } diff --git a/cmd/restic/cmd_diff.go b/cmd/restic/cmd_diff.go index c10f9898d..f1e832cb8 100644 --- a/cmd/restic/cmd_diff.go +++ b/cmd/restic/cmd_diff.go @@ -424,7 +424,7 @@ func runDiff(ctx context.Context, opts DiffOptions, gopts GlobalOptions, args [] } if gopts.JSON { - enc := json.NewEncoder(gopts.stdout) + enc := json.NewEncoder(gopts.term.OutputWriter()) c.printChange = func(change *Change) { err := enc.Encode(change) if err != nil { @@ -458,7 +458,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(gopts.stdout).Encode(stats) + err := json.NewEncoder(gopts.term.OutputWriter()).Encode(stats) if err != nil { printer.E("JSON encode failed: %v", err) } diff --git a/cmd/restic/cmd_diff_integration_test.go b/cmd/restic/cmd_diff_integration_test.go index 4143789c7..bf62f1e19 100644 --- a/cmd/restic/cmd_diff_integration_test.go +++ b/cmd/restic/cmd_diff_integration_test.go @@ -15,13 +15,11 @@ import ( ) func testRunDiffOutput(t testing.TB, gopts GlobalOptions, firstSnapshotID string, secondSnapshotID string) (string, error) { - buf, err := withCaptureStdout(gopts, func(gopts GlobalOptions) error { + buf, err := withCaptureStdout(t, gopts, func(ctx context.Context, gopts GlobalOptions) error { opts := DiffOptions{ ShowMetadata: false, } - return withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error { - return runDiff(ctx, opts, gopts, []string{firstSnapshotID, secondSnapshotID}, gopts.term) - }) + return runDiff(ctx, opts, gopts, []string{firstSnapshotID, secondSnapshotID}, gopts.term) }) return buf.String(), err } diff --git a/cmd/restic/cmd_features.go b/cmd/restic/cmd_features.go index e705ff080..ec3b93db5 100644 --- a/cmd/restic/cmd_features.go +++ b/cmd/restic/cmd_features.go @@ -49,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()) }, } diff --git a/cmd/restic/cmd_find_integration_test.go b/cmd/restic/cmd_find_integration_test.go index f4dca38b4..f49d34127 100644 --- a/cmd/restic/cmd_find_integration_test.go +++ b/cmd/restic/cmd_find_integration_test.go @@ -11,12 +11,10 @@ import ( ) 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(t, gopts, func(ctx context.Context, gopts GlobalOptions) error { - return runFind(ctx, opts, gopts, []string{pattern}, gopts.term) - }) + return runFind(ctx, opts, gopts, []string{pattern}, gopts.term) }) rtest.OK(t, err) return buf.Bytes() diff --git a/cmd/restic/cmd_forget.go b/cmd/restic/cmd_forget.go index edb702842..51613987c 100644 --- a/cmd/restic/cmd_forget.go +++ b/cmd/restic/cmd_forget.go @@ -251,7 +251,7 @@ func runForget(ctx context.Context, opts ForgetOptions, pruneOptions PruneOption } if gopts.Verbose >= 1 && !gopts.JSON { - err = PrintSnapshotGroupHeader(gopts.stdout, k) + err = PrintSnapshotGroupHeader(gopts.term.OutputWriter(), k) if err != nil { return err } @@ -274,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(gopts.stdout, keep, reasons, opts.Compact); err != nil { + if err := PrintSnapshots(gopts.term.OutputWriter(), keep, reasons, opts.Compact); err != nil { return err } printer.P("\n") @@ -283,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(gopts.stdout, remove, nil, opts.Compact); err != nil { + if err := PrintSnapshots(gopts.term.OutputWriter(), remove, nil, opts.Compact); err != nil { return err } printer.P("\n") @@ -328,7 +328,7 @@ func runForget(ctx context.Context, opts ForgetOptions, pruneOptions PruneOption } if gopts.JSON && len(jsonGroups) > 0 { - err = printJSONForget(gopts.stdout, jsonGroups) + err = printJSONForget(gopts.term.OutputWriter(), jsonGroups) if err != nil { return err } diff --git a/cmd/restic/cmd_generate.go b/cmd/restic/cmd_generate.go index e35035b77..580e704dc 100644 --- a/cmd/restic/cmd_generate.go +++ b/cmd/restic/cmd_generate.go @@ -84,7 +84,7 @@ func writeCompletion(filename string, shell string, generate func(w io.Writer) e defer func() { err = outFile.Close() }() outWriter = outFile } else { - outWriter = gopts.stdout + outWriter = gopts.term.OutputWriter() } err = generate(outWriter) diff --git a/cmd/restic/cmd_generate_integration_test.go b/cmd/restic/cmd_generate_integration_test.go index e6a426a9f..97248d370 100644 --- a/cmd/restic/cmd_generate_integration_test.go +++ b/cmd/restic/cmd_generate_integration_test.go @@ -9,10 +9,8 @@ import ( ) func testRunGenerate(t testing.TB, gopts GlobalOptions, opts generateOptions) ([]byte, error) { - buf, err := withCaptureStdout(gopts, func(gopts GlobalOptions) error { - return withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error { - return runGenerate(opts, gopts, []string{}, gopts.term) - }) + buf, err := withCaptureStdout(t, gopts, func(ctx context.Context, gopts GlobalOptions) error { + return runGenerate(opts, gopts, []string{}, gopts.term) }) return buf.Bytes(), err } diff --git a/cmd/restic/cmd_init.go b/cmd/restic/cmd_init.go index ca5b5b770..1858cd22c 100644 --- a/cmd/restic/cmd_init.go +++ b/cmd/restic/cmd_init.go @@ -132,7 +132,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(gopts.stdout).Encode(status) + return json.NewEncoder(gopts.term.OutputWriter()).Encode(status) } return nil diff --git a/cmd/restic/cmd_key_integration_test.go b/cmd/restic/cmd_key_integration_test.go index 48fbbe6ba..3a127946a 100644 --- a/cmd/restic/cmd_key_integration_test.go +++ b/cmd/restic/cmd_key_integration_test.go @@ -16,10 +16,8 @@ import ( ) func testRunKeyListOtherIDs(t testing.TB, gopts GlobalOptions) []string { - buf, err := withCaptureStdout(gopts, func(gopts GlobalOptions) error { - return withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error { - return runKeyList(ctx, gopts, []string{}, gopts.term) - }) + buf, err := withCaptureStdout(t, gopts, func(ctx context.Context, gopts GlobalOptions) error { + return runKeyList(ctx, gopts, []string{}, gopts.term) }) rtest.OK(t, err) diff --git a/cmd/restic/cmd_key_list.go b/cmd/restic/cmd_key_list.go index 6394da75e..91c14f6c0 100644 --- a/cmd/restic/cmd_key_list.go +++ b/cmd/restic/cmd_key_list.go @@ -95,7 +95,7 @@ func listKeys(ctx context.Context, s *repository.Repository, gopts GlobalOptions } if gopts.JSON { - return json.NewEncoder(gopts.stdout).Encode(keys) + return json.NewEncoder(gopts.term.OutputWriter()).Encode(keys) } tab := table.New() @@ -108,5 +108,5 @@ func listKeys(ctx context.Context, s *repository.Repository, gopts GlobalOptions tab.AddRow(key) } - return tab.Write(gopts.stdout) + return tab.Write(gopts.term.OutputWriter()) } diff --git a/cmd/restic/cmd_list_integration_test.go b/cmd/restic/cmd_list_integration_test.go index d18c0715e..d9fb2a562 100644 --- a/cmd/restic/cmd_list_integration_test.go +++ b/cmd/restic/cmd_list_integration_test.go @@ -11,10 +11,8 @@ import ( ) func testRunList(t testing.TB, gopts GlobalOptions, tpe string) restic.IDs { - buf, err := withCaptureStdout(gopts, func(gopts GlobalOptions) error { - return withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error { - return runList(ctx, gopts, []string{tpe}, gopts.term) - }) + 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) diff --git a/cmd/restic/cmd_ls.go b/cmd/restic/cmd_ls.go index c3c48b97f..2c973586c 100644 --- a/cmd/restic/cmd_ls.go +++ b/cmd/restic/cmd_ls.go @@ -382,11 +382,11 @@ func runLs(ctx context.Context, opts LsOptions, gopts GlobalOptions, args []stri if gopts.JSON { printer = &jsonLsPrinter{ - enc: json.NewEncoder(gopts.stdout), + enc: json.NewEncoder(gopts.term.OutputWriter()), } } else if opts.Ncdu { printer = &ncduLsPrinter{ - out: gopts.stdout, + out: gopts.term.OutputWriter(), } } else { printer = &textLsPrinter{ diff --git a/cmd/restic/cmd_ls_integration_test.go b/cmd/restic/cmd_ls_integration_test.go index cda8bbb54..4f13b89e3 100644 --- a/cmd/restic/cmd_ls_integration_test.go +++ b/cmd/restic/cmd_ls_integration_test.go @@ -13,11 +13,9 @@ import ( ) 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(t, gopts, func(ctx context.Context, gopts GlobalOptions) error { - return runLs(context.TODO(), opts, gopts, args, gopts.term) - }) + return runLs(context.TODO(), opts, gopts, args, gopts.term) }) rtest.OK(t, err) return buf.Bytes() diff --git a/cmd/restic/cmd_prune_integration_test.go b/cmd/restic/cmd_prune_integration_test.go index d56aa4afd..70e66e383 100644 --- a/cmd/restic/cmd_prune_integration_test.go +++ b/cmd/restic/cmd_prune_integration_test.go @@ -89,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, @@ -98,9 +98,7 @@ func testRunForgetJSON(t testing.TB, gopts GlobalOptions, args ...string) { pruneOpts := PruneOptions{ MaxUnused: "5%", } - return withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error { - return runForget(context.TODO(), opts, pruneOpts, gopts, gopts.term, args) - }) + return runForget(context.TODO(), opts, pruneOpts, gopts, gopts.term, args) }) rtest.OK(t, err) diff --git a/cmd/restic/cmd_repair_index_integration_test.go b/cmd/restic/cmd_repair_index_integration_test.go index 0319a77b4..7b8488889 100644 --- a/cmd/restic/cmd_repair_index_integration_test.go +++ b/cmd/restic/cmd_repair_index_integration_test.go @@ -17,7 +17,7 @@ import ( func testRunRebuildIndex(t testing.TB, gopts GlobalOptions) { rtest.OK(t, withTermStatus(t, gopts, func(ctx context.Context, gopts GlobalOptions) error { - gopts.stdout = io.Discard + gopts.Quiet = true return runRebuildIndex(context.TODO(), RepairIndexOptions{}, gopts, gopts.term) })) } @@ -129,7 +129,7 @@ func TestRebuildIndexFailsOnAppendOnly(t *testing.T) { return &appendOnlyBackend{r}, nil } err := withTermStatus(t, env.gopts, func(ctx context.Context, gopts GlobalOptions) error { - gopts.stdout = io.Discard + gopts.Quiet = true return runRebuildIndex(context.TODO(), RepairIndexOptions{}, gopts, gopts.term) }) diff --git a/cmd/restic/cmd_snapshots.go b/cmd/restic/cmd_snapshots.go index 908ad90b8..fc2930e85 100644 --- a/cmd/restic/cmd_snapshots.go +++ b/cmd/restic/cmd_snapshots.go @@ -103,7 +103,7 @@ func runSnapshots(ctx context.Context, opts SnapshotOptions, gopts GlobalOptions } if gopts.JSON { - err := printSnapshotGroupJSON(gopts.stdout, snapshotGroups, grouped) + err := printSnapshotGroupJSON(gopts.term.OutputWriter(), snapshotGroups, grouped) if err != nil { printer.E("error printing snapshots: %v", err) } @@ -116,12 +116,12 @@ func runSnapshots(ctx context.Context, opts SnapshotOptions, gopts GlobalOptions } if grouped { - err := PrintSnapshotGroupHeader(gopts.stdout, k) + err := PrintSnapshotGroupHeader(gopts.term.OutputWriter(), k) if err != nil { return err } } - err := PrintSnapshots(gopts.stdout, list, nil, opts.Compact) + err := PrintSnapshots(gopts.term.OutputWriter(), list, nil, opts.Compact) if err != nil { return err } diff --git a/cmd/restic/cmd_snapshots_integration_test.go b/cmd/restic/cmd_snapshots_integration_test.go index 00e60afd8..d962ddfc8 100644 --- a/cmd/restic/cmd_snapshots_integration_test.go +++ b/cmd/restic/cmd_snapshots_integration_test.go @@ -10,13 +10,11 @@ import ( ) 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(t, gopts, func(ctx context.Context, gopts GlobalOptions) error { - return runSnapshots(ctx, opts, gopts, []string{}, gopts.term) - }) + return runSnapshots(ctx, opts, gopts, []string{}, gopts.term) }) rtest.OK(t, err) diff --git a/cmd/restic/cmd_stats.go b/cmd/restic/cmd_stats.go index 27a0bd012..651e7ad2f 100644 --- a/cmd/restic/cmd_stats.go +++ b/cmd/restic/cmd_stats.go @@ -169,7 +169,7 @@ func runStats(ctx context.Context, opts StatsOptions, gopts GlobalOptions, args } if gopts.JSON { - err = json.NewEncoder(gopts.stdout).Encode(stats) + err = json.NewEncoder(gopts.term.OutputWriter()).Encode(stats) if err != nil { return fmt.Errorf("encoding output: %v", err) } diff --git a/cmd/restic/cmd_version.go b/cmd/restic/cmd_version.go index 7bf968d6c..f2aa47eb2 100644 --- a/cmd/restic/cmd_version.go +++ b/cmd/restic/cmd_version.go @@ -43,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 diff --git a/cmd/restic/global.go b/cmd/restic/global.go index 7cbbad651..bcd7d52fc 100644 --- a/cmd/restic/global.go +++ b/cmd/restic/global.go @@ -3,7 +3,6 @@ package main import ( "context" "fmt" - "io" "os" "os/exec" "path/filepath" @@ -74,7 +73,6 @@ type GlobalOptions struct { limiter.Limits password string - stdout io.Writer term ui.Terminal backends *location.Registry diff --git a/cmd/restic/integration_helpers_test.go b/cmd/restic/integration_helpers_test.go index 790c54c36..892f61e10 100644 --- a/cmd/restic/integration_helpers_test.go +++ b/cmd/restic/integration_helpers_test.go @@ -212,10 +212,6 @@ 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}, extended: make(options.Options), // replace this hook with "nil" if listing a filetype more than once is necessary @@ -416,18 +412,24 @@ func testFileSize(filename string, size int64) error { return nil } -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) - gopts.stdout = buf - err := inner(gopts) + err := withTermStatusRaw(os.Stdin, buf, &logOutputter{t: t}, gopts, callback) return buf, err } 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(os.Stdin, gopts.stdout, &logOutputter{t: t}, gopts.Quiet) + term := termstatus.New(stdin, stdout, stderr, gopts.Quiet) gopts.term = term wg.Add(1) go func() { diff --git a/cmd/restic/main.go b/cmd/restic/main.go index b5b8d592f..43e1f0b7a 100644 --- a/cmd/restic/main.go +++ b/cmd/restic/main.go @@ -173,13 +173,11 @@ func main() { version, runtime.Version(), runtime.GOOS, runtime.GOARCH) globalOptions := GlobalOptions{ - stdout: os.Stdout, backends: collectBackends(), } func() { term, cancel := termstatus.Setup(os.Stdin, os.Stdout, os.Stderr, globalOptions.Quiet) defer cancel() - globalOptions.stdout = termstatus.WrapStdout(term) globalOptions.term = term ctx := createGlobalContext(os.Stderr) err = newRootCommand(&globalOptions).ExecuteContext(ctx) diff --git a/internal/ui/mock.go b/internal/ui/mock.go index edc9050f9..fe18e647a 100644 --- a/internal/ui/mock.go +++ b/internal/ui/mock.go @@ -40,6 +40,10 @@ func (m *MockTerminal) ReadPassword(_ context.Context, _ string) (string, error) return "password", nil } +func (m *MockTerminal) OutputWriter() io.Writer { + return nil +} + func (m *MockTerminal) OutputRaw() io.Writer { return nil } diff --git a/internal/ui/terminal.go b/internal/ui/terminal.go index c53de7bf2..e3052c5e1 100644 --- a/internal/ui/terminal.go +++ b/internal/ui/terminal.go @@ -16,9 +16,12 @@ type Terminal interface { SetStatus(lines []string) // CanUpdateStatus returns true if the terminal can update the status lines. CanUpdateStatus() bool + InputRaw() io.ReadCloser InputIsTerminal() bool ReadPassword(ctx context.Context, prompt string) (string, error) + + OutputWriter() io.Writer // OutputRaw returns the output writer. Should only be used if there is no // other option. Must not be used in combination with Print, Error, SetStatus // or any other method that writes to the terminal. diff --git a/internal/ui/termstatus/status.go b/internal/ui/termstatus/status.go index 5ee21eb37..3d9b2d857 100644 --- a/internal/ui/termstatus/status.go +++ b/internal/ui/termstatus/status.go @@ -30,6 +30,9 @@ type Terminal struct { outputIsTerminal bool canUpdateStatus bool + outputWriter io.WriteCloser + outputWriterOnce sync.Once + // will be closed when the goroutine which runs Run() terminates, so it'll // yield a default value immediately closed chan struct{} @@ -73,6 +76,9 @@ func Setup(stdin io.ReadCloser, stdout, stderr io.Writer, quiet bool) (*Terminal }() return term, func() { + if term.outputWriter != nil { + _ = term.outputWriter.Close() + } // shutdown termstatus cancel() wg.Wait() @@ -158,6 +164,13 @@ func (t *Terminal) CanUpdateStatus() bool { return t.canUpdateStatus } +func (t *Terminal) OutputWriter() io.Writer { + t.outputWriterOnce.Do(func() { + t.outputWriter = newLineWriter(t.Print) + }) + return t.outputWriter +} + // OutputRaw returns the output writer. Should only be used if there is no // other option. Must not be used in combination with Print, Error, SetStatus // or any other method that writes to the terminal. diff --git a/internal/ui/termstatus/stdio_wrapper.go b/internal/ui/termstatus/stdio_wrapper.go index 384357d98..662e3b848 100644 --- a/internal/ui/termstatus/stdio_wrapper.go +++ b/internal/ui/termstatus/stdio_wrapper.go @@ -6,12 +6,6 @@ import ( "sync" ) -// WrapStdout returns line-buffering replacements for os.Stdout. -// On Close, the remaining bytes are written, followed by a line break. -func WrapStdout(term *Terminal) (stdout io.WriteCloser) { - return newLineWriter(term.Print) -} - type lineWriter struct { m sync.Mutex buf bytes.Buffer From b459d662884a7f3d88a2d289b2b095f50e7fa559 Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Sun, 21 Sep 2025 17:17:24 +0200 Subject: [PATCH 18/25] termstatus: additional comments --- internal/ui/terminal.go | 8 +++++++- internal/ui/termstatus/status.go | 4 +++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/internal/ui/terminal.go b/internal/ui/terminal.go index e3052c5e1..d4d5a10de 100644 --- a/internal/ui/terminal.go +++ b/internal/ui/terminal.go @@ -17,14 +17,20 @@ type Terminal interface { // CanUpdateStatus returns true if the terminal can update the status lines. CanUpdateStatus() bool + // InputRaw returns the input reader. InputRaw() io.ReadCloser + // InputIsTerminal returns true if the input is a terminal. InputIsTerminal() bool + // ReadPassword reads the password from the terminal. ReadPassword(ctx context.Context, prompt string) (string, error) + // OutputWriter returns a output writer that is safe for concurrent use with + // other output methods. Output is only shown after a line break. OutputWriter() io.Writer - // OutputRaw returns the output writer. Should only be used if there is no + // OutputRaw returns the raw output writer. Should only be used if there is no // other option. Must not be used in combination with Print, Error, SetStatus // or any other method that writes to the terminal. OutputRaw() io.Writer + // OutputIsTerminal returns true if the output is a terminal. OutputIsTerminal() bool } diff --git a/internal/ui/termstatus/status.go b/internal/ui/termstatus/status.go index 3d9b2d857..17f51c261 100644 --- a/internal/ui/termstatus/status.go +++ b/internal/ui/termstatus/status.go @@ -164,6 +164,8 @@ func (t *Terminal) CanUpdateStatus() bool { return t.canUpdateStatus } +// OutputWriter returns a output writer that is safe for concurrent use with +// other output methods. Output is only shown after a line break. func (t *Terminal) OutputWriter() io.Writer { t.outputWriterOnce.Do(func() { t.outputWriter = newLineWriter(t.Print) @@ -171,7 +173,7 @@ func (t *Terminal) OutputWriter() io.Writer { return t.outputWriter } -// OutputRaw returns the output writer. Should only be used if there is no +// OutputRaw returns the raw output writer. Should only be used if there is no // other option. Must not be used in combination with Print, Error, SetStatus // or any other method that writes to the terminal. func (t *Terminal) OutputRaw() io.Writer { From 52eb66929f447f4e05a84fe132f7770d804327d3 Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Sun, 21 Sep 2025 17:43:44 +0200 Subject: [PATCH 19/25] repository: deduplicate index progress bar initializaton --- cmd/restic/cmd_backup.go | 3 +-- cmd/restic/cmd_cat.go | 6 ++---- cmd/restic/cmd_check.go | 3 +-- cmd/restic/cmd_copy.go | 6 ++---- cmd/restic/cmd_debug.go | 3 +-- cmd/restic/cmd_diff.go | 3 +-- cmd/restic/cmd_dump.go | 3 +-- cmd/restic/cmd_find.go | 3 +-- cmd/restic/cmd_ls.go | 3 +-- cmd/restic/cmd_mount.go | 3 +-- cmd/restic/cmd_prune.go | 3 +-- cmd/restic/cmd_recover.go | 5 ++--- cmd/restic/cmd_repair_packs.go | 3 +-- cmd/restic/cmd_repair_snapshots.go | 3 +-- cmd/restic/cmd_restore.go | 3 +-- cmd/restic/cmd_rewrite.go | 3 +-- cmd/restic/cmd_stats.go | 3 +-- internal/checker/checker.go | 9 +++++++-- internal/repository/repository.go | 9 +++++++-- internal/restic/repository.go | 8 +++++++- internal/ui/progress.go | 4 ---- 21 files changed, 41 insertions(+), 48 deletions(-) diff --git a/cmd/restic/cmd_backup.go b/cmd/restic/cmd_backup.go index c137eafe1..9cd22ae01 100644 --- a/cmd/restic/cmd_backup.go +++ b/cmd/restic/cmd_backup.go @@ -549,8 +549,7 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, ter printer.V("load index files") } - bar := ui.NewIndexCounter(printer) - err = repo.LoadIndex(ctx, bar) + err = repo.LoadIndex(ctx, printer) if err != nil { return err } diff --git a/cmd/restic/cmd_cat.go b/cmd/restic/cmd_cat.go index 3c3573d66..967687171 100644 --- a/cmd/restic/cmd_cat.go +++ b/cmd/restic/cmd_cat.go @@ -168,8 +168,7 @@ func runCat(ctx context.Context, gopts GlobalOptions, args []string, term ui.Ter return err case "blob": - bar := ui.NewIndexCounter(printer) - err = repo.LoadIndex(ctx, bar) + err = repo.LoadIndex(ctx, printer) if err != nil { return err } @@ -196,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 := ui.NewIndexCounter(printer) - err = repo.LoadIndex(ctx, bar) + err = repo.LoadIndex(ctx, printer) if err != nil { return err } diff --git a/cmd/restic/cmd_check.go b/cmd/restic/cmd_check.go index 9897924e4..afc7835f0 100644 --- a/cmd/restic/cmd_check.go +++ b/cmd/restic/cmd_check.go @@ -249,8 +249,7 @@ func runCheck(ctx context.Context, opts CheckOptions, gopts GlobalOptions, args } printer.P("load indexes\n") - bar := ui.NewIndexCounter(printer) - hints, errs := chkr.LoadIndex(ctx, bar) + hints, errs := chkr.LoadIndex(ctx, printer) if ctx.Err() != nil { return summary, ctx.Err() } diff --git a/cmd/restic/cmd_copy.go b/cmd/restic/cmd_copy.go index cd627af3a..19bfe8679 100644 --- a/cmd/restic/cmd_copy.go +++ b/cmd/restic/cmd_copy.go @@ -101,13 +101,11 @@ func runCopy(ctx context.Context, opts CopyOptions, gopts GlobalOptions, args [] } debug.Log("Loading source index") - bar := ui.NewIndexCounter(printer) - if err := srcRepo.LoadIndex(ctx, bar); err != nil { + if err := srcRepo.LoadIndex(ctx, printer); err != nil { return err } - bar = ui.NewIndexCounter(printer) debug.Log("Loading destination index") - if err := dstRepo.LoadIndex(ctx, bar); err != nil { + if err := dstRepo.LoadIndex(ctx, printer); err != nil { return err } diff --git a/cmd/restic/cmd_debug.go b/cmd/restic/cmd_debug.go index 4709fffec..00e312b13 100644 --- a/cmd/restic/cmd_debug.go +++ b/cmd/restic/cmd_debug.go @@ -484,8 +484,7 @@ func runDebugExamine(ctx context.Context, gopts GlobalOptions, opts DebugExamine return errors.Fatal("no pack files to examine") } - bar := ui.NewIndexCounter(printer) - err = repo.LoadIndex(ctx, bar) + err = repo.LoadIndex(ctx, printer) if err != nil { return err } diff --git a/cmd/restic/cmd_diff.go b/cmd/restic/cmd_diff.go index f1e832cb8..a25c79fd9 100644 --- a/cmd/restic/cmd_diff.go +++ b/cmd/restic/cmd_diff.go @@ -391,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 := ui.NewIndexCounter(printer) - if err = repo.LoadIndex(ctx, bar); err != nil { + if err = repo.LoadIndex(ctx, printer); err != nil { return err } diff --git a/cmd/restic/cmd_dump.go b/cmd/restic/cmd_dump.go index fcbd9cfd4..b36fb2236 100644 --- a/cmd/restic/cmd_dump.go +++ b/cmd/restic/cmd_dump.go @@ -160,8 +160,7 @@ func runDump(ctx context.Context, opts DumpOptions, gopts GlobalOptions, args [] return errors.Fatalf("failed to find snapshot: %v", err) } - bar := ui.NewIndexCounter(printer) - err = repo.LoadIndex(ctx, bar) + err = repo.LoadIndex(ctx, printer) if err != nil { return err } diff --git a/cmd/restic/cmd_find.go b/cmd/restic/cmd_find.go index dc2564a57..bb3c0b55a 100644 --- a/cmd/restic/cmd_find.go +++ b/cmd/restic/cmd_find.go @@ -623,8 +623,7 @@ func runFind(ctx context.Context, opts FindOptions, gopts GlobalOptions, args [] if err != nil { return err } - bar := ui.NewIndexCounter(printer) - if err = repo.LoadIndex(ctx, bar); err != nil { + if err = repo.LoadIndex(ctx, printer); err != nil { return err } diff --git a/cmd/restic/cmd_ls.go b/cmd/restic/cmd_ls.go index 2c973586c..de015f180 100644 --- a/cmd/restic/cmd_ls.go +++ b/cmd/restic/cmd_ls.go @@ -373,8 +373,7 @@ func runLs(ctx context.Context, opts LsOptions, gopts GlobalOptions, args []stri return err } - bar := ui.NewIndexCounter(termPrinter) - if err = repo.LoadIndex(ctx, bar); err != nil { + if err = repo.LoadIndex(ctx, termPrinter); err != nil { return err } diff --git a/cmd/restic/cmd_mount.go b/cmd/restic/cmd_mount.go index 2b12b9b98..84e28f86f 100644 --- a/cmd/restic/cmd_mount.go +++ b/cmd/restic/cmd_mount.go @@ -146,8 +146,7 @@ func runMount(ctx context.Context, opts MountOptions, gopts GlobalOptions, args } defer unlock() - bar := ui.NewIndexCounter(printer) - err = repo.LoadIndex(ctx, bar) + err = repo.LoadIndex(ctx, printer) if err != nil { return err } diff --git a/cmd/restic/cmd_prune.go b/cmd/restic/cmd_prune.go index 6cefc2d81..dabcc8293 100644 --- a/cmd/restic/cmd_prune.go +++ b/cmd/restic/cmd_prune.go @@ -190,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 := ui.NewIndexCounter(printer) - err := repo.LoadIndex(ctx, bar) + err := repo.LoadIndex(ctx, printer) if err != nil { return err } diff --git a/cmd/restic/cmd_recover.go b/cmd/restic/cmd_recover.go index 94570ae31..3d1b7aec0 100644 --- a/cmd/restic/cmd_recover.go +++ b/cmd/restic/cmd_recover.go @@ -66,8 +66,7 @@ func runRecover(ctx context.Context, gopts GlobalOptions, term ui.Terminal) erro } printer.P("load index files\n") - bar := ui.NewIndexCounter(printer) - if err = repo.LoadIndex(ctx, bar); err != nil { + if err = repo.LoadIndex(ctx, printer); err != nil { return err } @@ -85,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) diff --git a/cmd/restic/cmd_repair_packs.go b/cmd/restic/cmd_repair_packs.go index 237eec913..b8e68a65e 100644 --- a/cmd/restic/cmd_repair_packs.go +++ b/cmd/restic/cmd_repair_packs.go @@ -59,8 +59,7 @@ func runRepairPacks(ctx context.Context, gopts GlobalOptions, term ui.Terminal, } defer unlock() - bar := ui.NewIndexCounter(printer) - err = repo.LoadIndex(ctx, bar) + err = repo.LoadIndex(ctx, printer) if err != nil { return errors.Fatalf("%s", err) } diff --git a/cmd/restic/cmd_repair_snapshots.go b/cmd/restic/cmd_repair_snapshots.go index eacf7f2e5..0c7608e7f 100644 --- a/cmd/restic/cmd_repair_snapshots.go +++ b/cmd/restic/cmd_repair_snapshots.go @@ -87,8 +87,7 @@ func runRepairSnapshots(ctx context.Context, gopts GlobalOptions, opts RepairOpt return err } - bar := ui.NewIndexCounter(printer) - if err := repo.LoadIndex(ctx, bar); err != nil { + if err := repo.LoadIndex(ctx, printer); err != nil { return err } diff --git a/cmd/restic/cmd_restore.go b/cmd/restic/cmd_restore.go index 692c5e259..fab22327e 100644 --- a/cmd/restic/cmd_restore.go +++ b/cmd/restic/cmd_restore.go @@ -151,8 +151,7 @@ func runRestore(ctx context.Context, opts RestoreOptions, gopts GlobalOptions, return errors.Fatalf("failed to find snapshot: %v", err) } - bar := ui.NewIndexCounter(printer) - err = repo.LoadIndex(ctx, bar) + err = repo.LoadIndex(ctx, printer) if err != nil { return err } diff --git a/cmd/restic/cmd_rewrite.go b/cmd/restic/cmd_rewrite.go index 45a0d6a01..fb6f66b32 100644 --- a/cmd/restic/cmd_rewrite.go +++ b/cmd/restic/cmd_rewrite.go @@ -318,8 +318,7 @@ func runRewrite(ctx context.Context, opts RewriteOptions, gopts GlobalOptions, a return err } - bar := ui.NewIndexCounter(printer) - if err = repo.LoadIndex(ctx, bar); err != nil { + if err = repo.LoadIndex(ctx, printer); err != nil { return err } diff --git a/cmd/restic/cmd_stats.go b/cmd/restic/cmd_stats.go index 651e7ad2f..604805e67 100644 --- a/cmd/restic/cmd_stats.go +++ b/cmd/restic/cmd_stats.go @@ -111,8 +111,7 @@ func runStats(ctx context.Context, opts StatsOptions, gopts GlobalOptions, args if err != nil { return err } - bar := ui.NewIndexCounter(printer) - if err = repo.LoadIndex(ctx, bar); err != nil { + if err = repo.LoadIndex(ctx, printer); err != nil { return err } diff --git a/internal/checker/checker.go b/internal/checker/checker.go index 12020891a..1f5d29a67 100644 --- a/internal/checker/checker.go +++ b/internal/checker/checker.go @@ -93,11 +93,16 @@ func computePackTypes(ctx context.Context, idx restic.ListBlobser) (map[restic.I } // LoadIndex loads all index files. -func (c *Checker) LoadIndex(ctx context.Context, p *progress.Counter) (hints []error, errs []error) { +func (c *Checker) LoadIndex(ctx context.Context, p restic.TerminalCounterFactory) (hints []error, errs []error) { debug.Log("Start") + var bar *progress.Counter + if p != nil { + bar = p.NewCounterTerminalOnly("index files loaded") + } + packToIndex := make(map[restic.ID]restic.IDSet) - err := c.masterIndex.Load(ctx, c.repo, p, func(id restic.ID, idx *index.Index, err error) error { + err := c.masterIndex.Load(ctx, c.repo, bar, func(id restic.ID, idx *index.Index, err error) error { debug.Log("process index %v, err %v", id, err) err = errors.Wrapf(err, "error loading index %v", id) diff --git a/internal/repository/repository.go b/internal/repository/repository.go index d5da0ca5f..8d1b35d91 100644 --- a/internal/repository/repository.go +++ b/internal/repository/repository.go @@ -639,13 +639,18 @@ func (r *Repository) clearIndex() { } // LoadIndex loads all index files from the backend in parallel and stores them -func (r *Repository) LoadIndex(ctx context.Context, p *progress.Counter) error { +func (r *Repository) LoadIndex(ctx context.Context, p restic.TerminalCounterFactory) error { debug.Log("Loading index") // reset in-memory index before loading it from the repository r.clearIndex() - err := r.idx.Load(ctx, r, p, nil) + var bar *progress.Counter + if p != nil { + bar = p.NewCounterTerminalOnly("index files loaded") + } + + err := r.idx.Load(ctx, r, bar, nil) if err != nil { return err } diff --git a/internal/restic/repository.go b/internal/restic/repository.go index 8462e1b9e..5631202a9 100644 --- a/internal/restic/repository.go +++ b/internal/restic/repository.go @@ -21,7 +21,7 @@ type Repository interface { Config() Config Key() *crypto.Key - LoadIndex(ctx context.Context, p *progress.Counter) error + LoadIndex(ctx context.Context, p TerminalCounterFactory) error SetIndex(mi MasterIndex) error LookupBlob(t BlobType, id ID) []PackedBlob @@ -131,6 +131,12 @@ type PackBlobs struct { Blobs []Blob } +type TerminalCounterFactory interface { + // NewCounterTerminalOnly returns a new progress counter that is only shown if stdout points to a + // terminal. It is not shown if --quiet or --json is specified. + NewCounterTerminalOnly(description string) *progress.Counter +} + // MasterIndex keeps track of the blobs are stored within files. type MasterIndex interface { Has(bh BlobHandle) bool diff --git a/internal/ui/progress.go b/internal/ui/progress.go index 9e18da5ec..b3a3226e9 100644 --- a/internal/ui/progress.go +++ b/internal/ui/progress.go @@ -76,7 +76,3 @@ func NewProgressPrinter(json bool, verbosity uint, term Terminal) progress.Print show: verbosity > 0, } } - -func NewIndexCounter(printer progress.Printer) *progress.Counter { - return printer.NewCounterTerminalOnly("index files loaded") -} From f045297348e46b9aff780581a26180579c60c922 Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Sun, 21 Sep 2025 18:09:28 +0200 Subject: [PATCH 20/25] termstatus: fix typo in comment --- internal/ui/termstatus/status.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/ui/termstatus/status.go b/internal/ui/termstatus/status.go index 17f51c261..e654f5cac 100644 --- a/internal/ui/termstatus/status.go +++ b/internal/ui/termstatus/status.go @@ -59,7 +59,7 @@ type fder interface { // // Expected usage: // ``` -// term, cancel := termstatus.Setup(os.stdout, os.stderr, false) +// term, cancel := termstatus.Setup(os.Stdin, os.Stdout, os.Stderr, false) // defer cancel() // // do stuff // ``` From 711194276cf28e112b3ca09b0100bbffc773ef5d Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Sun, 21 Sep 2025 18:16:48 +0200 Subject: [PATCH 21/25] remove unused printer from ReadPassword --- cmd/restic/cmd_copy.go | 2 +- cmd/restic/cmd_init.go | 5 ++--- cmd/restic/cmd_key_add.go | 7 +++---- cmd/restic/cmd_key_passwd.go | 2 +- cmd/restic/global.go | 10 +++++----- cmd/restic/global_test.go | 5 ++--- cmd/restic/secondary_repo.go | 5 ++--- cmd/restic/secondary_repo_test.go | 5 ++--- 8 files changed, 18 insertions(+), 23 deletions(-) diff --git a/cmd/restic/cmd_copy.go b/cmd/restic/cmd_copy.go index 19bfe8679..9e772a6e9 100644 --- a/cmd/restic/cmd_copy.go +++ b/cmd/restic/cmd_copy.go @@ -69,7 +69,7 @@ func (opts *CopyOptions) AddFlags(f *pflag.FlagSet) { func runCopy(ctx context.Context, opts CopyOptions, gopts GlobalOptions, args []string, term ui.Terminal) error { printer := ui.NewProgressPrinter(false, gopts.verbosity, term) - secondaryGopts, isFromRepo, err := fillSecondaryGlobalOpts(ctx, opts.secondaryRepoOptions, gopts, "destination", printer) + secondaryGopts, isFromRepo, err := fillSecondaryGlobalOpts(ctx, opts.secondaryRepoOptions, gopts, "destination") if err != nil { return err } diff --git a/cmd/restic/cmd_init.go b/cmd/restic/cmd_init.go index 1858cd22c..c13b69e87 100644 --- a/cmd/restic/cmd_init.go +++ b/cmd/restic/cmd_init.go @@ -92,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 } @@ -140,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 } diff --git a/cmd/restic/cmd_key_add.go b/cmd/restic/cmd_key_add.go index 196c4a8de..455969d6e 100644 --- a/cmd/restic/cmd_key_add.go +++ b/cmd/restic/cmd_key_add.go @@ -70,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 } @@ -93,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 } @@ -125,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 { diff --git a/cmd/restic/cmd_key_passwd.go b/cmd/restic/cmd_key_passwd.go index 5eacd11b3..98cdfc43f 100644 --- a/cmd/restic/cmd_key_passwd.go +++ b/cmd/restic/cmd_key_passwd.go @@ -65,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 } diff --git a/cmd/restic/global.go b/cmd/restic/global.go index bcd7d52fc..cd17dccc7 100644 --- a/cmd/restic/global.go +++ b/cmd/restic/global.go @@ -230,7 +230,7 @@ func loadPasswordFromFile(pwdFile string) (string, error) { // ReadPassword reads the password from a password file, the environment // variable RESTIC_PASSWORD or prompts the user. If the context is canceled, // the function leaks the password reading goroutine. -func ReadPassword(ctx context.Context, gopts GlobalOptions, prompt string, printer progress.Printer) (string, error) { +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") @@ -257,13 +257,13 @@ func ReadPassword(ctx context.Context, gopts GlobalOptions, prompt string, print // 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 gopts.term.InputIsTerminal() { - pw2, err := ReadPassword(ctx, gopts, prompt2, printer) + pw2, err := ReadPassword(ctx, gopts, prompt2) if err != nil { return "", err } @@ -330,7 +330,7 @@ func OpenRepository(ctx context.Context, gopts GlobalOptions, printer progress.P } for ; passwordTriesLeft > 0; passwordTriesLeft-- { - gopts.password, err = ReadPassword(ctx, gopts, "enter password for repository: ", printer) + gopts.password, err = ReadPassword(ctx, gopts, "enter password for repository: ") if ctx.Err() != nil { return nil, ctx.Err() } diff --git a/cmd/restic/global_test.go b/cmd/restic/global_test.go index de8275876..e8def7b94 100644 --- a/cmd/restic/global_test.go +++ b/cmd/restic/global_test.go @@ -8,7 +8,6 @@ import ( "testing" rtest "github.com/restic/restic/internal/test" - "github.com/restic/restic/internal/ui/progress" ) func TestReadRepo(t *testing.T) { @@ -42,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) } diff --git a/cmd/restic/secondary_repo.go b/cmd/restic/secondary_repo.go index 16c75f1ab..db4c93bad 100644 --- a/cmd/restic/secondary_repo.go +++ b/cmd/restic/secondary_repo.go @@ -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 } diff --git a/cmd/restic/secondary_repo_test.go b/cmd/restic/secondary_repo_test.go index 32206318a..c6837ada4 100644 --- a/cmd/restic/secondary_repo_test.go +++ b/cmd/restic/secondary_repo_test.go @@ -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 @@ -166,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) @@ -174,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") } } From f2b9ea64550b493c935f8d923f16d6b6773563d3 Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Sun, 21 Sep 2025 19:16:16 +0200 Subject: [PATCH 22/25] termstatus: use errWriter if terminal commands fail --- internal/terminal/terminal_posix.go | 13 ++++++------- internal/terminal/terminal_unix.go | 4 ++-- internal/terminal/terminal_windows.go | 10 ++++++---- internal/ui/termstatus/status.go | 17 ++++++++++++----- 4 files changed, 26 insertions(+), 18 deletions(-) diff --git a/internal/terminal/terminal_posix.go b/internal/terminal/terminal_posix.go index e8a5abc59..08527b777 100644 --- a/internal/terminal/terminal_posix.go +++ b/internal/terminal/terminal_posix.go @@ -4,7 +4,6 @@ import ( "bytes" "fmt" "io" - "os" ) const ( @@ -18,22 +17,22 @@ const ( // PosixClearCurrentLine removes all characters from the current line and resets the // cursor position to the first column. -func PosixClearCurrentLine(wr io.Writer, _ uintptr) { +func PosixClearCurrentLine(wr io.Writer, _ uintptr) error { // clear current line _, err := wr.Write([]byte(PosixControlMoveCursorHome + PosixControlClearLine)) if err != nil { - fmt.Fprintf(os.Stderr, "write failed: %v\n", err) - return + return fmt.Errorf("write failed: %w", err) } + return nil } // PosixMoveCursorUp moves the cursor to the line n lines above the current one. -func PosixMoveCursorUp(wr io.Writer, _ uintptr, n int) { +func PosixMoveCursorUp(wr io.Writer, _ uintptr, n int) error { data := []byte(PosixControlMoveCursorHome) data = append(data, bytes.Repeat([]byte(PosixControlMoveCursorUp), n)...) _, err := wr.Write(data) if err != nil { - fmt.Fprintf(os.Stderr, "write failed: %v\n", err) - return + return fmt.Errorf("write failed: %w", err) } + return nil } diff --git a/internal/terminal/terminal_unix.go b/internal/terminal/terminal_unix.go index 65e353d9f..732219bb4 100644 --- a/internal/terminal/terminal_unix.go +++ b/internal/terminal/terminal_unix.go @@ -12,12 +12,12 @@ import ( // ClearCurrentLine removes all characters from the current line and resets the // cursor position to the first column. -func ClearCurrentLine(_ uintptr) func(io.Writer, uintptr) { +func ClearCurrentLine(_ uintptr) func(io.Writer, uintptr) error { return PosixClearCurrentLine } // MoveCursorUp moves the cursor to the line n lines above the current one. -func MoveCursorUp(_ uintptr) func(io.Writer, uintptr, int) { +func MoveCursorUp(_ uintptr) func(io.Writer, uintptr, int) error { return PosixMoveCursorUp } diff --git a/internal/terminal/terminal_windows.go b/internal/terminal/terminal_windows.go index d68d0197e..fffabc5ee 100644 --- a/internal/terminal/terminal_windows.go +++ b/internal/terminal/terminal_windows.go @@ -15,7 +15,7 @@ import ( // clearCurrentLine removes all characters from the current line and resets the // cursor position to the first column. -func ClearCurrentLine(fd uintptr) func(io.Writer, uintptr) { +func ClearCurrentLine(fd uintptr) func(io.Writer, uintptr) error { // easy case, the terminal is cmd or psh, without redirection if isWindowsTerminal(fd) { return windowsClearCurrentLine @@ -26,7 +26,7 @@ func ClearCurrentLine(fd uintptr) func(io.Writer, uintptr) { } // moveCursorUp moves the cursor to the line n lines above the current one. -func MoveCursorUp(fd uintptr) func(io.Writer, uintptr, int) { +func MoveCursorUp(fd uintptr) func(io.Writer, uintptr, int) error { // easy case, the terminal is cmd or psh, without redirection if isWindowsTerminal(fd) { return windowsMoveCursorUp @@ -45,7 +45,7 @@ var ( // windowsClearCurrentLine removes all characters from the current line and // resets the cursor position to the first column. -func windowsClearCurrentLine(_ io.Writer, fd uintptr) { +func windowsClearCurrentLine(_ io.Writer, fd uintptr) error { var info windows.ConsoleScreenBufferInfo windows.GetConsoleScreenBufferInfo(windows.Handle(fd), &info) @@ -58,10 +58,11 @@ func windowsClearCurrentLine(_ io.Writer, fd uintptr) { count = uint32(info.Size.X) procFillConsoleOutputAttribute.Call(fd, uintptr(info.Attributes), uintptr(count), *(*uintptr)(unsafe.Pointer(&cursor)), uintptr(unsafe.Pointer(&w))) procFillConsoleOutputCharacter.Call(fd, uintptr(' '), uintptr(count), *(*uintptr)(unsafe.Pointer(&cursor)), uintptr(unsafe.Pointer(&w))) + return nil } // windowsMoveCursorUp moves the cursor to the line n lines above the current one. -func windowsMoveCursorUp(_ io.Writer, fd uintptr, n int) { +func windowsMoveCursorUp(_ io.Writer, fd uintptr, n int) error { var info windows.ConsoleScreenBufferInfo windows.GetConsoleScreenBufferInfo(windows.Handle(fd), &info) @@ -70,6 +71,7 @@ func windowsMoveCursorUp(_ io.Writer, fd uintptr, n int) { X: 0, Y: info.CursorPosition.Y - int16(n), }) + return nil } // isWindowsTerminal return true if the file descriptor is a windows terminal (cmd, psh). diff --git a/internal/ui/termstatus/status.go b/internal/ui/termstatus/status.go index e654f5cac..3803882e6 100644 --- a/internal/ui/termstatus/status.go +++ b/internal/ui/termstatus/status.go @@ -37,8 +37,8 @@ type Terminal struct { // yield a default value immediately closed chan struct{} - clearCurrentLine func(io.Writer, uintptr) - moveCursorUp func(io.Writer, uintptr, int) + clearCurrentLine func(io.Writer, uintptr) error + moveCursorUp func(io.Writer, uintptr, int) error } type message struct { @@ -214,7 +214,10 @@ func (t *Terminal) run(ctx context.Context) { // ignore all messages, do nothing, we are in the background process group continue } - t.clearCurrentLine(t.wr, t.fd) + if err := t.clearCurrentLine(t.wr, t.fd); err != nil { + _, _ = fmt.Fprintf(t.errWriter, "write failed: %v\n", err) + continue + } var dst io.Writer if msg.err { @@ -256,7 +259,9 @@ func (t *Terminal) writeStatus(status []string) { t.lastStatusLen = statusLen for _, line := range status { - t.clearCurrentLine(t.wr, t.fd) + if err := t.clearCurrentLine(t.wr, t.fd); err != nil { + _, _ = fmt.Fprintf(t.errWriter, "write failed: %v\n", err) + } _, err := t.wr.Write([]byte(line)) if err != nil { @@ -265,7 +270,9 @@ func (t *Terminal) writeStatus(status []string) { } if len(status) > 0 { - t.moveCursorUp(t.wr, t.fd, len(status)-1) + if err := t.moveCursorUp(t.wr, t.fd, len(status)-1); err != nil { + _, _ = fmt.Fprintf(t.errWriter, "write failed: %v\n", err) + } } } From df7924f4df388ba83f425d2d1d19d5da2bbb7d37 Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Sun, 21 Sep 2025 19:24:48 +0200 Subject: [PATCH 23/25] node: report error on xattr retrieval using standard error logging --- internal/archiver/archiver.go | 6 ++++-- internal/archiver/archiver_test.go | 10 +++++----- internal/archiver/file_saver_test.go | 2 +- internal/fs/fs_local.go | 4 ++-- internal/fs/fs_local_test.go | 2 +- internal/fs/fs_local_vss_test.go | 2 +- internal/fs/fs_reader.go | 2 +- internal/fs/interface.go | 2 +- internal/fs/node.go | 4 ++-- internal/fs/node_noxattr.go | 2 +- internal/fs/node_test.go | 6 +++--- internal/fs/node_unix_test.go | 2 +- internal/fs/node_windows.go | 2 +- internal/fs/node_windows_test.go | 2 +- internal/fs/node_xattr.go | 5 ++--- internal/fs/node_xattr_all_test.go | 4 ++-- internal/restic/tree_test.go | 2 +- 17 files changed, 30 insertions(+), 29 deletions(-) diff --git a/internal/archiver/archiver.go b/internal/archiver/archiver.go index 95facb16f..7e71a543b 100644 --- a/internal/archiver/archiver.go +++ b/internal/archiver/archiver.go @@ -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 { diff --git a/internal/archiver/archiver_test.go b/internal/archiver/archiver_test.go index b0f338092..b52e47b18 100644 --- a/internal/archiver/archiver_test.go +++ b/internal/archiver/archiver_test.go @@ -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 } diff --git a/internal/archiver/file_saver_test.go b/internal/archiver/file_saver_test.go index af4ed0157..a5d090c05 100644 --- a/internal/archiver/file_saver_test.go +++ b/internal/archiver/file_saver_test.go @@ -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 diff --git a/internal/fs/fs_local.go b/internal/fs/fs_local.go index fc6c69cf2..dfbdab3b0 100644 --- a/internal/fs/fs_local.go +++ b/internal/fs/fs_local.go @@ -152,11 +152,11 @@ func (f *localFile) Stat() (*ExtendedFileInfo, error) { return f.fi, err } -func (f *localFile) ToNode(ignoreXattrListError bool) (*restic.Node, error) { +func (f *localFile) ToNode(ignoreXattrListError bool, warnf func(format string, args ...any)) (*restic.Node, error) { if err := f.cacheFI(); err != nil { return nil, err } - return nodeFromFileInfo(f.name, f.fi, ignoreXattrListError) + return nodeFromFileInfo(f.name, f.fi, ignoreXattrListError, warnf) } func (f *localFile) Read(p []byte) (n int, err error) { diff --git a/internal/fs/fs_local_test.go b/internal/fs/fs_local_test.go index 8fd8eb136..aff0b7a50 100644 --- a/internal/fs/fs_local_test.go +++ b/internal/fs/fs_local_test.go @@ -86,7 +86,7 @@ func checkMetadata(t *testing.T, f File, path string, follow bool, nodeType rest rtest.OK(t, err) assertFIEqual(t, fi2, fi) - node, err := f.ToNode(false) + node, err := f.ToNode(false, t.Logf) rtest.OK(t, err) // ModTime is likely unique per file, thus it provides a good indication that it is from the correct file diff --git a/internal/fs/fs_local_vss_test.go b/internal/fs/fs_local_vss_test.go index 32f9f4cfd..bae08bd10 100644 --- a/internal/fs/fs_local_vss_test.go +++ b/internal/fs/fs_local_vss_test.go @@ -333,7 +333,7 @@ func TestVSSFS(t *testing.T) { rtest.OK(t, err) rtest.Equals(t, "example", string(data), "unexpected file content") - node, err := f.ToNode(false) + node, err := f.ToNode(false, t.Logf) rtest.OK(t, err) rtest.Equals(t, node.Mode, lstatFi.Mode) diff --git a/internal/fs/fs_reader.go b/internal/fs/fs_reader.go index c4e98be0f..32a4e8fee 100644 --- a/internal/fs/fs_reader.go +++ b/internal/fs/fs_reader.go @@ -267,7 +267,7 @@ func (f fakeFile) Stat() (*ExtendedFileInfo, error) { return f.fi, nil } -func (f fakeFile) ToNode(_ bool) (*restic.Node, error) { +func (f fakeFile) ToNode(_ bool, _ func(format string, args ...any)) (*restic.Node, error) { node := buildBasicNode(f.name, f.fi) // fill minimal info with current values for uid, gid diff --git a/internal/fs/interface.go b/internal/fs/interface.go index d75b0a91d..19d2eb1f7 100644 --- a/internal/fs/interface.go +++ b/internal/fs/interface.go @@ -48,5 +48,5 @@ type File interface { // ToNode returns a restic.Node for the File. The internally used os.FileInfo // must be consistent with that returned by Stat(). In particular, the metadata // returned by consecutive calls to Stat() and ToNode() must match. - ToNode(ignoreXattrListError bool) (*restic.Node, error) + ToNode(ignoreXattrListError bool, warnf func(format string, args ...any)) (*restic.Node, error) } diff --git a/internal/fs/node.go b/internal/fs/node.go index ab2aca957..ac8e58203 100644 --- a/internal/fs/node.go +++ b/internal/fs/node.go @@ -15,7 +15,7 @@ import ( // nodeFromFileInfo returns a new node from the given path and FileInfo. It // returns the first error that is encountered, together with a node. -func nodeFromFileInfo(path string, fi *ExtendedFileInfo, ignoreXattrListError bool) (*restic.Node, error) { +func nodeFromFileInfo(path string, fi *ExtendedFileInfo, ignoreXattrListError bool, warnf func(format string, args ...any)) (*restic.Node, error) { node := buildBasicNode(path, fi) if err := nodeFillExtendedStat(node, path, fi); err != nil { @@ -23,7 +23,7 @@ func nodeFromFileInfo(path string, fi *ExtendedFileInfo, ignoreXattrListError bo } err := nodeFillGenericAttributes(node, path, fi) - err = errors.Join(err, nodeFillExtendedAttributes(node, path, ignoreXattrListError)) + err = errors.Join(err, nodeFillExtendedAttributes(node, path, ignoreXattrListError, warnf)) return node, err } diff --git a/internal/fs/node_noxattr.go b/internal/fs/node_noxattr.go index 2dbd72c9d..2923aea74 100644 --- a/internal/fs/node_noxattr.go +++ b/internal/fs/node_noxattr.go @@ -13,6 +13,6 @@ func nodeRestoreExtendedAttributes(_ *restic.Node, _ string, _ func(xattrName st } // nodeFillExtendedAttributes is a no-op -func nodeFillExtendedAttributes(_ *restic.Node, _ string, _ bool) error { +func nodeFillExtendedAttributes(_ *restic.Node, _ string, _ bool, _ func(format string, args ...any)) error { return nil } diff --git a/internal/fs/node_test.go b/internal/fs/node_test.go index 176abc382..2b4c76c92 100644 --- a/internal/fs/node_test.go +++ b/internal/fs/node_test.go @@ -31,7 +31,7 @@ func BenchmarkNodeFromFileInfo(t *testing.B) { t.ResetTimer() for i := 0; i < t.N; i++ { - _, err := f.ToNode(false) + _, err := f.ToNode(false, t.Logf) rtest.OK(t, err) } @@ -223,9 +223,9 @@ func TestNodeRestoreAt(t *testing.T) { fs := &Local{} meta, err := fs.OpenFile(nodePath, O_NOFOLLOW, true) rtest.OK(t, err) - n2, err := meta.ToNode(false) + n2, err := meta.ToNode(false, t.Logf) rtest.OK(t, err) - n3, err := meta.ToNode(true) + n3, err := meta.ToNode(true, t.Logf) rtest.OK(t, err) rtest.OK(t, meta.Close()) rtest.Assert(t, n2.Equals(*n3), "unexpected node info mismatch %v", cmp.Diff(n2, n3)) diff --git a/internal/fs/node_unix_test.go b/internal/fs/node_unix_test.go index 1eb1ee506..7d5f7fa98 100644 --- a/internal/fs/node_unix_test.go +++ b/internal/fs/node_unix_test.go @@ -117,7 +117,7 @@ func TestNodeFromFileInfo(t *testing.T) { fs := &Local{} meta, err := fs.OpenFile(test.filename, 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()) diff --git a/internal/fs/node_windows.go b/internal/fs/node_windows.go index df0a7ea65..416b5c7f8 100644 --- a/internal/fs/node_windows.go +++ b/internal/fs/node_windows.go @@ -91,7 +91,7 @@ func nodeRestoreExtendedAttributes(node *restic.Node, path string, xattrSelectFi // fill extended attributes in the node // It also checks if the volume supports extended attributes and stores the result in a map // so that it does not have to be checked again for subsequent calls for paths in the same volume. -func nodeFillExtendedAttributes(node *restic.Node, path string, _ bool) (err error) { +func nodeFillExtendedAttributes(node *restic.Node, path string, _ bool, _ func(format string, args ...any)) (err error) { if strings.Contains(filepath.Base(path), ":") { // Do not process for Alternate Data Streams in Windows return nil diff --git a/internal/fs/node_windows_test.go b/internal/fs/node_windows_test.go index 458a7bcb1..2d71e178e 100644 --- a/internal/fs/node_windows_test.go +++ b/internal/fs/node_windows_test.go @@ -224,7 +224,7 @@ func restoreAndGetNode(t *testing.T, tempDir string, testNode *restic.Node, warn fs := &Local{} meta, err := fs.OpenFile(testPath, O_NOFOLLOW, true) test.OK(t, err) - nodeFromFileInfo, err := meta.ToNode(false) + nodeFromFileInfo, err := meta.ToNode(false, t.Logf) test.OK(t, errors.Wrapf(err, "Could not get NodeFromFileInfo for path: %s", testPath)) test.OK(t, meta.Close()) diff --git a/internal/fs/node_xattr.go b/internal/fs/node_xattr.go index f1119fe51..9ebd3524b 100644 --- a/internal/fs/node_xattr.go +++ b/internal/fs/node_xattr.go @@ -4,7 +4,6 @@ package fs import ( - "fmt" "os" "syscall" @@ -97,7 +96,7 @@ func nodeRestoreExtendedAttributes(node *restic.Node, path string, xattrSelectFi return nil } -func nodeFillExtendedAttributes(node *restic.Node, path string, ignoreListError bool) error { +func nodeFillExtendedAttributes(node *restic.Node, path string, ignoreListError bool, warnf func(format string, args ...any)) error { xattrs, err := listxattr(path) debug.Log("fillExtendedAttributes(%v) %v %v", path, xattrs, err) if err != nil { @@ -111,7 +110,7 @@ func nodeFillExtendedAttributes(node *restic.Node, path string, ignoreListError for _, attr := range xattrs { attrVal, err := getxattr(path, attr) if err != nil { - fmt.Fprintf(os.Stderr, "can not obtain extended attribute %v for %v:\n", attr, path) + warnf("can not obtain extended attribute %v for %v:\n", attr, path) continue } attr := restic.ExtendedAttribute{ diff --git a/internal/fs/node_xattr_all_test.go b/internal/fs/node_xattr_all_test.go index 6a9a2e4bf..9e35582ba 100644 --- a/internal/fs/node_xattr_all_test.go +++ b/internal/fs/node_xattr_all_test.go @@ -34,7 +34,7 @@ func setAndVerifyXattr(t *testing.T, file string, attrs []restic.ExtendedAttribu nodeActual := &restic.Node{ Type: restic.NodeTypeFile, } - rtest.OK(t, nodeFillExtendedAttributes(nodeActual, file, false)) + rtest.OK(t, nodeFillExtendedAttributes(nodeActual, file, false, t.Logf)) rtest.Assert(t, nodeActual.Equals(*node), "xattr mismatch got %v expected %v", nodeActual.ExtendedAttributes, node.ExtendedAttributes) } @@ -59,7 +59,7 @@ func setAndVerifyXattrWithSelectFilter(t *testing.T, file string, testAttr []tes nodeActual := &restic.Node{ Type: restic.NodeTypeFile, } - rtest.OK(t, nodeFillExtendedAttributes(nodeActual, file, false)) + rtest.OK(t, nodeFillExtendedAttributes(nodeActual, file, false, t.Logf)) // Check nodeActual to make sure only xattrs we expect are there for _, testAttr := range testAttr { diff --git a/internal/restic/tree_test.go b/internal/restic/tree_test.go index 5c9c0739c..08ebe3984 100644 --- a/internal/restic/tree_test.go +++ b/internal/restic/tree_test.go @@ -86,7 +86,7 @@ func TestNodeMarshal(t *testing.T) { func nodeForFile(t *testing.T, name string) *restic.Node { f, err := (&fs.Local{}).OpenFile(name, fs.O_NOFOLLOW, true) rtest.OK(t, err) - node, err := f.ToNode(false) + node, err := f.ToNode(false, t.Logf) rtest.OK(t, err) rtest.OK(t, f.Close()) return node From d8da3d2f2d24b904ff75d1ad4ac103aaf980c445 Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Sun, 21 Sep 2025 20:16:29 +0200 Subject: [PATCH 24/25] termstatus: increase test coverage --- internal/ui/termstatus/status_test.go | 90 ++++++++++++++++++++++++--- 1 file changed, 80 insertions(+), 10 deletions(-) diff --git a/internal/ui/termstatus/status_test.go b/internal/ui/termstatus/status_test.go index bddb7c5d1..f6b885cee 100644 --- a/internal/ui/termstatus/status_test.go +++ b/internal/ui/termstatus/status_test.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "io" + "strings" "testing" "github.com/restic/restic/internal/terminal" @@ -13,16 +14,7 @@ import ( ) func TestSetStatus(t *testing.T) { - var buf bytes.Buffer - term := New(nil, &buf, io.Discard, false) - - term.canUpdateStatus = true - term.fd = ^uintptr(0) - term.clearCurrentLine = terminal.PosixClearCurrentLine - term.moveCursorUp = terminal.PosixMoveCursorUp - - ctx, cancel := context.WithCancel(context.Background()) - go term.Run(ctx) + buf, term, cancel := setupStatusTest() const ( cl = terminal.PosixControlClearLine @@ -58,6 +50,39 @@ func TestSetStatus(t *testing.T) { rtest.Equals(t, exp, buf.String()) } +func setupStatusTest() (*bytes.Buffer, *Terminal, context.CancelFunc) { + buf := &bytes.Buffer{} + term := New(nil, buf, buf, false) + + term.canUpdateStatus = true + term.fd = ^uintptr(0) + term.clearCurrentLine = terminal.PosixClearCurrentLine + term.moveCursorUp = terminal.PosixMoveCursorUp + + ctx, cancel := context.WithCancel(context.Background()) + go term.Run(ctx) + return buf, term, cancel +} + +func TestPrint(t *testing.T) { + buf, term, cancel := setupStatusTest() + + const ( + cl = terminal.PosixControlClearLine + home = terminal.PosixControlMoveCursorHome + ) + + term.Print("test") + exp := home + cl + "test\n" + term.Error("error") + exp += home + cl + "error\n" + + cancel() + + <-term.closed + rtest.Equals(t, exp, buf.String()) +} + func TestSanitizeLines(t *testing.T) { var tests = []struct { input []string @@ -87,3 +112,48 @@ func TestReadPassword(t *testing.T) { _, err := readPassword(&errorReader{want}) rtest.Assert(t, errors.Is(err, want), "wrong error %v", err) } + +func TestReadPasswordTerminal(t *testing.T) { + expected := "password" + term := New(io.NopCloser(strings.NewReader(expected)), io.Discard, io.Discard, false) + pw, err := term.ReadPassword(context.Background(), "test") + rtest.OK(t, err) + rtest.Equals(t, expected, pw) +} + +func TestRawInputOutput(t *testing.T) { + input := io.NopCloser(strings.NewReader("password")) + var output bytes.Buffer + term := New(input, &output, io.Discard, false) + rtest.Equals(t, input, term.InputRaw()) + rtest.Equals(t, false, term.InputIsTerminal()) + rtest.Equals(t, &output, term.OutputRaw()) + rtest.Equals(t, false, term.OutputIsTerminal()) + rtest.Equals(t, false, term.CanUpdateStatus()) +} + +func TestDisableStatus(t *testing.T) { + var output bytes.Buffer + term, cancel := Setup(nil, &output, &output, true) + rtest.Equals(t, false, term.CanUpdateStatus()) + + term.Print("test") + term.Error("error") + term.SetStatus([]string{"status"}) + + cancel() + rtest.Equals(t, "test\nerror\nstatus\n", output.String()) +} + +func TestOutputWriter(t *testing.T) { + var output bytes.Buffer + term, cancel := Setup(nil, &output, &output, true) + + _, err := term.OutputWriter().Write([]byte("output\npartial")) + rtest.OK(t, err) + term.Print("test") + term.Error("error") + + cancel() + rtest.Equals(t, "output\ntest\nerror\npartial\n", output.String()) +} From 3335f62a8f8d952215d71915b628d27e44678bd2 Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Fri, 3 Oct 2025 18:38:01 +0200 Subject: [PATCH 25/25] Fix linter warnings --- cmd/restic/cmd_backup.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/restic/cmd_backup.go b/cmd/restic/cmd_backup.go index 9cd22ae01..5e884de87 100644 --- a/cmd/restic/cmd_backup.go +++ b/cmd/restic/cmd_backup.go @@ -219,7 +219,7 @@ func readLines(filename string, stdin io.ReadCloser) ([]string, error) { // or stdin if filename is "-". Each filename is terminated by a zero byte, // which is stripped off. func readFilenamesFromFileRaw(filename string, stdin io.ReadCloser) (names []string, err error) { - var f io.ReadCloser = stdin + f := stdin if filename != "-" { if f, err = os.Open(filename); err != nil { return nil, err @@ -580,7 +580,7 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, ter printer.V("read data from stdin") } filename := path.Join("/", opts.StdinFilename) - var source io.ReadCloser = term.InputRaw() + source := term.InputRaw() if opts.StdinCommand { source, err = fs.NewCommandReader(ctx, args, printer.E) if err != nil {