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