From 4d1b9cef63a6667756f0e51e91d63778d4f76f36 Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Sun, 14 Jun 2026 10:50:28 +0200 Subject: [PATCH 1/6] internal/fileio: extract low-level file I/O from internal/fs Move TempFile and PreallocateFile into internal/fileio. internal/fs primarily focuses on converting between data.Node and the actual filesystem state. Extract the two methods to not pull in unnecessary dependencies. --- internal/backend/local/local.go | 4 +- internal/fileio/file_unix.go | 22 +++++++ internal/fileio/file_windows.go | 60 +++++++++++++++++++ internal/{fs => fileio}/file_windows_test.go | 8 +-- internal/{fs => fileio}/preallocate_darwin.go | 2 +- internal/{fs => fileio}/preallocate_linux.go | 2 +- internal/{fs => fileio}/preallocate_other.go | 2 +- internal/{fs => fileio}/preallocate_test.go | 5 +- internal/fs/file_unix.go | 15 ----- internal/fs/file_windows.go | 44 -------------- internal/repository/packer_manager.go | 4 +- internal/restorer/fileswriter.go | 3 +- 12 files changed, 98 insertions(+), 73 deletions(-) create mode 100644 internal/fileio/file_unix.go create mode 100644 internal/fileio/file_windows.go rename internal/{fs => fileio}/file_windows_test.go (82%) rename internal/{fs => fileio}/preallocate_darwin.go (97%) rename internal/{fs => fileio}/preallocate_linux.go (97%) rename internal/{fs => fileio}/preallocate_other.go (93%) rename internal/{fs => fileio}/preallocate_test.go (89%) diff --git a/internal/backend/local/local.go b/internal/backend/local/local.go index 25ea4f47c..3a1237ee8 100644 --- a/internal/backend/local/local.go +++ b/internal/backend/local/local.go @@ -16,7 +16,7 @@ import ( "github.com/restic/restic/internal/backend/util" "github.com/restic/restic/internal/debug" "github.com/restic/restic/internal/errors" - "github.com/restic/restic/internal/fs" + "github.com/restic/restic/internal/fileio" "github.com/cenkalti/backoff/v4" ) @@ -151,7 +151,7 @@ func (b *Local) Save(_ context.Context, h backend.Handle, rd backend.RewindReade // preallocate disk space if size := rd.Length(); size > 0 { - if err := fs.PreallocateFile(f, size); err != nil { + if err := fileio.PreallocateFile(f, size); err != nil { debug.Log("Failed to preallocate %v with size %v: %v", finalname, size, err) } } diff --git a/internal/fileio/file_unix.go b/internal/fileio/file_unix.go new file mode 100644 index 000000000..c32952dca --- /dev/null +++ b/internal/fileio/file_unix.go @@ -0,0 +1,22 @@ +//go:build !windows + +package fileio + +import ( + "os" +) + +// TempFile creates a temporary file which has already been deleted (on +// supported platforms) +func TempFile(dir, prefix string) (f *os.File, err error) { + f, err = os.CreateTemp(dir, prefix) + if err != nil { + return nil, err + } + + if err = os.Remove(f.Name()); err != nil { + return nil, err + } + + return f, nil +} diff --git a/internal/fileio/file_windows.go b/internal/fileio/file_windows.go new file mode 100644 index 000000000..0dba2d2af --- /dev/null +++ b/internal/fileio/file_windows.go @@ -0,0 +1,60 @@ +package fileio + +import ( + "math/rand" + "os" + "path/filepath" + "strconv" + "syscall" + + "golang.org/x/sys/windows" +) + +// TempFile creates a temporary file which is marked as delete-on-close +func TempFile(dir, prefix string) (f *os.File, err error) { + // slightly modified implementation of os.CreateTemp(dir, prefix) to allow us to add + // the FILE_ATTRIBUTE_TEMPORARY | FILE_FLAG_DELETE_ON_CLOSE flags. + // These provide two large benefits: + // FILE_ATTRIBUTE_TEMPORARY tells Windows to keep the file in memory only if possible + // which reduces the amount of unnecessary disk writes. + // FILE_FLAG_DELETE_ON_CLOSE instructs Windows to automatically delete the file once + // all file descriptors are closed. + + if dir == "" { + dir = os.TempDir() + } + + access := uint32(windows.GENERIC_READ | windows.GENERIC_WRITE) + creation := uint32(windows.CREATE_NEW) + share := uint32(0) // prevent other processes from accessing the file + flags := uint32(windows.FILE_ATTRIBUTE_TEMPORARY | windows.FILE_FLAG_DELETE_ON_CLOSE) + + for i := 0; i < 10000; i++ { + randSuffix := strconv.Itoa(int(1e9 + rand.Intn(1e9)%1e9))[1:] + path := filepath.Join(dir, prefix+randSuffix) + + ptr, err := windows.UTF16PtrFromString(path) + if err != nil { + return nil, err + } + h, err := windows.CreateFile(ptr, access, share, nil, creation, flags, 0) + if os.IsExist(err) { + continue + } + // Access denied error can occur if the tmp files conflict with each other. + if isAccessDeniedError(err) { + continue + } + return os.NewFile(uintptr(h), path), err + } + + // Proper error handling is still to do + return nil, os.ErrExist +} + +func isAccessDeniedError(err error) bool { + if errno, ok := err.(syscall.Errno); ok { + return errno == windows.ERROR_ACCESS_DENIED + } + return false +} diff --git a/internal/fs/file_windows_test.go b/internal/fileio/file_windows_test.go similarity index 82% rename from internal/fs/file_windows_test.go rename to internal/fileio/file_windows_test.go index 71077709b..4351f44b4 100644 --- a/internal/fs/file_windows_test.go +++ b/internal/fileio/file_windows_test.go @@ -1,21 +1,21 @@ -package fs_test +package fileio_test import ( "errors" "os" "testing" - "github.com/restic/restic/internal/fs" + "github.com/restic/restic/internal/fileio" rtest "github.com/restic/restic/internal/test" ) func TestTempFile(t *testing.T) { // create two temp files at the same time to check that the // collision avoidance works - f, err := fs.TempFile("", "test") + f, err := fileio.TempFile("", "test") fn := f.Name() rtest.OK(t, err) - f2, err := fs.TempFile("", "test") + f2, err := fileio.TempFile("", "test") fn2 := f2.Name() rtest.OK(t, err) rtest.Assert(t, fn != fn2, "filenames don't differ %s", fn) diff --git a/internal/fs/preallocate_darwin.go b/internal/fileio/preallocate_darwin.go similarity index 97% rename from internal/fs/preallocate_darwin.go rename to internal/fileio/preallocate_darwin.go index af46e971b..f0e62a6d0 100644 --- a/internal/fs/preallocate_darwin.go +++ b/internal/fileio/preallocate_darwin.go @@ -1,4 +1,4 @@ -package fs +package fileio import ( "os" diff --git a/internal/fs/preallocate_linux.go b/internal/fileio/preallocate_linux.go similarity index 97% rename from internal/fs/preallocate_linux.go rename to internal/fileio/preallocate_linux.go index 7b0449507..07fa0940e 100644 --- a/internal/fs/preallocate_linux.go +++ b/internal/fileio/preallocate_linux.go @@ -1,4 +1,4 @@ -package fs +package fileio import ( "os" diff --git a/internal/fs/preallocate_other.go b/internal/fileio/preallocate_other.go similarity index 93% rename from internal/fs/preallocate_other.go rename to internal/fileio/preallocate_other.go index b04d68738..e471e699e 100644 --- a/internal/fs/preallocate_other.go +++ b/internal/fileio/preallocate_other.go @@ -1,6 +1,6 @@ //go:build !linux && !darwin -package fs +package fileio import "os" diff --git a/internal/fs/preallocate_test.go b/internal/fileio/preallocate_test.go similarity index 89% rename from internal/fs/preallocate_test.go rename to internal/fileio/preallocate_test.go index 9dabd2f36..a87872877 100644 --- a/internal/fs/preallocate_test.go +++ b/internal/fileio/preallocate_test.go @@ -1,4 +1,4 @@ -package fs +package fileio import ( "os" @@ -7,6 +7,7 @@ import ( "syscall" "testing" + "github.com/restic/restic/internal/fs" "github.com/restic/restic/internal/test" ) @@ -31,7 +32,7 @@ func TestPreallocate(t *testing.T) { fi, err := wr.Stat() test.OK(t, err) - efi := ExtendedStat(fi) + efi := fs.ExtendedStat(fi) test.Assert(t, efi.Size == i || efi.Blocks > 0, "Preallocated size of %v, got size %v block %v", i, efi.Size, efi.Blocks) }) } diff --git a/internal/fs/file_unix.go b/internal/fs/file_unix.go index 9b33f1ab7..fe3de064d 100644 --- a/internal/fs/file_unix.go +++ b/internal/fs/file_unix.go @@ -13,21 +13,6 @@ func fixpath(name string) string { return name } -// TempFile creates a temporary file which has already been deleted (on -// supported platforms) -func TempFile(dir, prefix string) (f *os.File, err error) { - f, err = os.CreateTemp(dir, prefix) - if err != nil { - return nil, err - } - - if err = os.Remove(f.Name()); err != nil { - return nil, err - } - - return f, nil -} - // isNotSupported returns true if the error is caused by an unsupported file system feature. func isNotSupported(err error) bool { if perr, ok := err.(*os.PathError); ok && perr.Err == syscall.ENOTSUP { diff --git a/internal/fs/file_windows.go b/internal/fs/file_windows.go index 30cb75da5..f655e731d 100644 --- a/internal/fs/file_windows.go +++ b/internal/fs/file_windows.go @@ -1,10 +1,8 @@ package fs import ( - "math/rand" "os" "path/filepath" - "strconv" "strings" "github.com/restic/restic/internal/data" @@ -43,48 +41,6 @@ func fixpath(name string) string { return name } -// TempFile creates a temporary file which is marked as delete-on-close -func TempFile(dir, prefix string) (f *os.File, err error) { - // slightly modified implementation of os.CreateTemp(dir, prefix) to allow us to add - // the FILE_ATTRIBUTE_TEMPORARY | FILE_FLAG_DELETE_ON_CLOSE flags. - // These provide two large benefits: - // FILE_ATTRIBUTE_TEMPORARY tells Windows to keep the file in memory only if possible - // which reduces the amount of unnecessary disk writes. - // FILE_FLAG_DELETE_ON_CLOSE instructs Windows to automatically delete the file once - // all file descriptors are closed. - - if dir == "" { - dir = os.TempDir() - } - - access := uint32(windows.GENERIC_READ | windows.GENERIC_WRITE) - creation := uint32(windows.CREATE_NEW) - share := uint32(0) // prevent other processes from accessing the file - flags := uint32(windows.FILE_ATTRIBUTE_TEMPORARY | windows.FILE_FLAG_DELETE_ON_CLOSE) - - for i := 0; i < 10000; i++ { - randSuffix := strconv.Itoa(int(1e9 + rand.Intn(1e9)%1e9))[1:] - path := filepath.Join(dir, prefix+randSuffix) - - ptr, err := windows.UTF16PtrFromString(path) - if err != nil { - return nil, err - } - h, err := windows.CreateFile(ptr, access, share, nil, creation, flags, 0) - if os.IsExist(err) { - continue - } - // Access denied error can occur if the tmp files conflict with each other. - if isAccessDeniedError(err) { - continue - } - return os.NewFile(uintptr(h), path), err - } - - // Proper error handling is still to do - return nil, os.ErrExist -} - // Chmod changes the mode of the named file to mode. func chmod(name string, mode os.FileMode) error { return os.Chmod(fixpath(name), mode) diff --git a/internal/repository/packer_manager.go b/internal/repository/packer_manager.go index ea69b6b65..54455d33e 100644 --- a/internal/repository/packer_manager.go +++ b/internal/repository/packer_manager.go @@ -16,7 +16,7 @@ import ( "github.com/restic/restic/internal/restic" "github.com/restic/restic/internal/debug" - "github.com/restic/restic/internal/fs" + "github.com/restic/restic/internal/fileio" "github.com/restic/restic/internal/repository/crypto" "github.com/restic/restic/internal/repository/pack" ) @@ -201,7 +201,7 @@ func (r *packerManager) forgetPacker(packer *packer) { // created or one is returned that already has some blobs. func (r *packerManager) newPacker() (pck *packer, err error) { debug.Log("create new pack") - tmpfile, err := fs.TempFile("", "restic-temp-pack-") + tmpfile, err := fileio.TempFile("", "restic-temp-pack-") if err != nil { return nil, errors.WithStack(err) } diff --git a/internal/restorer/fileswriter.go b/internal/restorer/fileswriter.go index 59871324c..20d458343 100644 --- a/internal/restorer/fileswriter.go +++ b/internal/restorer/fileswriter.go @@ -10,6 +10,7 @@ import ( "github.com/hashicorp/golang-lru/v2/simplelru" "github.com/restic/restic/internal/debug" "github.com/restic/restic/internal/errors" + "github.com/restic/restic/internal/fileio" "github.com/restic/restic/internal/fs" ) @@ -166,7 +167,7 @@ func ensureSize(f *os.File, fi os.FileInfo, createSize int64, sparse bool) (*os. return nil, err } } else if createSize > 0 { - err := fs.PreallocateFile(f, createSize) + err := fileio.PreallocateFile(f, createSize) if err != nil { // Just log the preallocate error but don't let it cause the restore process to fail. // Preallocate might return an error if the filesystem (implementation) does not From 0f4236cb396d0e63c00c39335459577afca6b52f Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Sun, 14 Jun 2026 10:50:38 +0200 Subject: [PATCH 2/6] repository: return unlock func from LockRepo Drop the Unlocker interface and return the unlock callback directly from LockRepo, simplifying callers that only need to defer unlock(). --- cmd/restic/lock.go | 6 +---- internal/repository/lock.go | 12 +++------- internal/repository/lock_test.go | 38 ++++++++++++++++---------------- 3 files changed, 23 insertions(+), 33 deletions(-) diff --git a/cmd/restic/lock.go b/cmd/restic/lock.go index 8264c6b9e..eb2fc47ed 100644 --- a/cmd/restic/lock.go +++ b/cmd/restic/lock.go @@ -16,9 +16,7 @@ func internalOpenWithLocked(ctx context.Context, gopts global.Options, dryRun bo unlock := func() {} if !dryRun { - var lock repository.Unlocker - - lock, ctx, err = repository.LockRepo(ctx, repo, exclusive, gopts.RetryLock, func(msg string) { + unlock, ctx, err = repository.LockRepo(ctx, repo, exclusive, gopts.RetryLock, func(msg string) { if !gopts.JSON { printer.P("%s", msg) } @@ -26,8 +24,6 @@ func internalOpenWithLocked(ctx context.Context, gopts global.Options, dryRun bo if err != nil { return nil, nil, nil, err } - - unlock = lock.Unlock } else { repo.SetDryRun() } diff --git a/internal/repository/lock.go b/internal/repository/lock.go index 6f43b5e6a..9a3e8aba5 100644 --- a/internal/repository/lock.go +++ b/internal/repository/lock.go @@ -13,10 +13,6 @@ import ( "github.com/restic/restic/internal/restic" ) -type Unlocker interface { - Unlock() -} - type unlocker struct { lock *lockHandle cancel context.CancelFunc @@ -28,8 +24,6 @@ func (l *unlocker) Unlock() { l.refreshWG.Wait() } -var _ Unlocker = &unlocker{} - type locker struct { retrySleepStart time.Duration retrySleepMax time.Duration @@ -50,11 +44,11 @@ var lockerInst = &locker{ // LockRepo acquires a repository lock. The returned context is cancelled when // Unlock is called; cancelling the original context stops lock refresh. -func LockRepo(ctx context.Context, repo *Repository, exclusive bool, retryLock time.Duration, printRetry func(msg string), logger func(format string, args ...interface{})) (Unlocker, context.Context, error) { +func LockRepo(ctx context.Context, repo *Repository, exclusive bool, retryLock time.Duration, printRetry func(msg string), logger func(format string, args ...interface{})) (func(), context.Context, error) { return lockerInst.Lock(ctx, repo, exclusive, retryLock, printRetry, logger) } -func (l *locker) Lock(ctx context.Context, r *Repository, exclusive bool, retryLock time.Duration, printRetry func(msg string), logger func(format string, args ...interface{})) (*unlocker, context.Context, error) { +func (l *locker) Lock(ctx context.Context, r *Repository, exclusive bool, retryLock time.Duration, printRetry func(msg string), logger func(format string, args ...interface{})) (func(), context.Context, error) { var lock *lockHandle var err error @@ -113,7 +107,7 @@ retryLoop: go l.refreshLocks(ctx, repo.be, unlocker, refreshChan, forceRefreshChan, logger) go l.monitorLockRefresh(ctx, unlocker, refreshChan, forceRefreshChan, logger) - return unlocker, ctx, nil + return unlocker.Unlock, ctx, nil } func minDuration(a, b time.Duration) time.Duration { diff --git a/internal/repository/lock_test.go b/internal/repository/lock_test.go index 6f29b7a18..847be76e5 100644 --- a/internal/repository/lock_test.go +++ b/internal/repository/lock_test.go @@ -34,19 +34,19 @@ func openLockTestRepo(t *testing.T, wrapper backendWrapper) (*Repository, backen return TestOpenBackend(t, be), be } -func checkedLockRepo(ctx context.Context, t *testing.T, repo *Repository, lockerInst *locker, retryLock time.Duration) (Unlocker, context.Context) { - lock, wrappedCtx, err := lockerInst.Lock(ctx, repo, false, retryLock, func(msg string) {}, func(format string, args ...interface{}) {}) +func checkedLockRepo(ctx context.Context, t *testing.T, repo *Repository, lockerInst *locker, retryLock time.Duration) (func(), context.Context) { + unlock, wrappedCtx, err := lockerInst.Lock(ctx, repo, false, retryLock, func(msg string) {}, func(format string, args ...interface{}) {}) rtest.OK(t, err) rtest.OK(t, wrappedCtx.Err()) - return lock, wrappedCtx + return unlock, wrappedCtx } func TestLock(t *testing.T) { t.Parallel() repo, _ := openLockTestRepo(t, nil) - lock, wrappedCtx := checkedLockRepo(context.Background(), t, repo, lockerInst, 0) - lock.Unlock() + unlock, wrappedCtx := checkedLockRepo(context.Background(), t, repo, lockerInst, 0) + unlock() if wrappedCtx.Err() == nil { t.Fatal("unlock did not cancel context") } @@ -65,7 +65,7 @@ func TestLockCancel(t *testing.T) { } // Unlock should not crash - lock.Unlock() + lock() } func TestLockConflict(t *testing.T) { @@ -73,9 +73,9 @@ func TestLockConflict(t *testing.T) { repo, be := openLockTestRepo(t, nil) repo2 := TestOpenBackend(t, be) - lock, _, err := LockRepo(context.Background(), repo, true, 0, func(msg string) {}, func(format string, args ...interface{}) {}) + unlock, _, err := LockRepo(context.Background(), repo, true, 0, func(msg string) {}, func(format string, args ...interface{}) {}) rtest.OK(t, err) - defer lock.Unlock() + defer unlock() _, _, err = LockRepo(context.Background(), repo2, false, 0, func(msg string) {}, func(format string, args ...interface{}) {}) if err == nil { t.Fatal("second lock should have failed") @@ -109,7 +109,7 @@ func TestLockFailedRefresh(t *testing.T) { refreshInterval: 20 * time.Millisecond, refreshabilityTimeout: 100 * time.Millisecond, } - lock, wrappedCtx := checkedLockRepo(context.Background(), t, repo, li, 0) + unlock, wrappedCtx := checkedLockRepo(context.Background(), t, repo, li, 0) select { case <-wrappedCtx.Done(): @@ -118,7 +118,7 @@ func TestLockFailedRefresh(t *testing.T) { t.Fatal("failed lock refresh did not cause context cancellation") } // Unlock should not crash - lock.Unlock() + unlock() } type loggingBackend struct { @@ -150,7 +150,7 @@ func TestLockSuccessfulRefresh(t *testing.T) { refreshInterval: 60 * time.Millisecond, refreshabilityTimeout: 500 * time.Millisecond, } - lock, wrappedCtx := checkedLockRepo(context.Background(), t, repo, li, 0) + unlock, wrappedCtx := checkedLockRepo(context.Background(), t, repo, li, 0) select { case <-wrappedCtx.Done(): @@ -167,7 +167,7 @@ func TestLockSuccessfulRefresh(t *testing.T) { // expected lock refresh to work } // Unlock should not crash - lock.Unlock() + unlock() } type slowBackend struct { @@ -201,7 +201,7 @@ func TestLockSuccessfulStaleRefresh(t *testing.T) { refreshabilityTimeout: 50 * time.Millisecond, } - lock, wrappedCtx := checkedLockRepo(context.Background(), t, repo, li, 0) + unlock, wrappedCtx := checkedLockRepo(context.Background(), t, repo, li, 0) // delay lock refreshing long enough that the lock would expire sb.m.Lock() sb.sleep = li.refreshabilityTimeout + li.refreshInterval @@ -230,7 +230,7 @@ func TestLockSuccessfulStaleRefresh(t *testing.T) { } // Unlock should not crash - lock.Unlock() + unlock() } func TestLockWaitTimeout(t *testing.T) { @@ -239,7 +239,7 @@ func TestLockWaitTimeout(t *testing.T) { elock, _, err := LockRepo(context.TODO(), repo, true, 0, func(msg string) {}, func(format string, args ...interface{}) {}) rtest.OK(t, err) - defer elock.Unlock() + defer elock() retryLock := 200 * time.Millisecond @@ -261,7 +261,7 @@ func TestLockWaitCancel(t *testing.T) { elock, _, err := LockRepo(context.TODO(), repo, true, 0, func(msg string) {}, func(format string, args ...interface{}) {}) rtest.OK(t, err) - defer elock.Unlock() + defer elock() retryLock := 200 * time.Millisecond cancelAfter := 40 * time.Millisecond @@ -292,12 +292,12 @@ func TestLockWaitSuccess(t *testing.T) { unlockAfter := 40 * time.Millisecond time.AfterFunc(unlockAfter, func() { - elock.Unlock() + elock() }) - lock, _, err := LockRepo(context.TODO(), repo, false, retryLock, func(msg string) {}, func(format string, args ...interface{}) {}) + unlock, _, err := LockRepo(context.TODO(), repo, false, retryLock, func(msg string) {}, func(format string, args ...interface{}) {}) rtest.OK(t, err) - lock.Unlock() + unlock() } func createFakeLock(repo *Repository, t time.Time, pid int) (restic.ID, error) { From d9d54a505e12f58b93ee6a6754ebaee26fa73c09 Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Sun, 14 Jun 2026 10:50:42 +0200 Subject: [PATCH 3/6] restic: move Printer interface from internal/ui/progress Move Printer and NewNoopPrinter to internal/restic so repository does not have to import the ui packages. --- cmd/restic/cmd_check.go | 6 +- cmd/restic/cmd_check_test.go | 5 +- cmd/restic/cmd_copy.go | 10 ++-- cmd/restic/cmd_generate.go | 5 +- cmd/restic/cmd_init.go | 2 +- cmd/restic/cmd_init_integration_test.go | 5 +- cmd/restic/cmd_key_add.go | 3 +- cmd/restic/cmd_key_integration_test.go | 6 +- cmd/restic/cmd_key_list.go | 2 +- cmd/restic/cmd_key_passwd.go | 3 +- cmd/restic/cmd_key_remove.go | 2 +- cmd/restic/cmd_list.go | 2 +- cmd/restic/cmd_migrate.go | 4 +- cmd/restic/cmd_prune.go | 6 +- cmd/restic/cmd_recover.go | 2 +- cmd/restic/cmd_restore.go | 4 +- cmd/restic/cmd_rewrite.go | 8 +-- cmd/restic/cmd_stats.go | 2 +- cmd/restic/find.go | 3 +- cmd/restic/lock.go | 10 ++-- doc/090_participating.rst | 3 +- internal/global/global.go | 17 +++--- internal/repository/debug.go | 15 +++-- internal/repository/prune.go | 11 ++-- internal/repository/prune_internal_test.go | 5 +- internal/repository/prune_test.go | 9 ++- internal/repository/repack_test.go | 3 +- internal/repository/repair_index.go | 5 +- internal/repository/repair_index_test.go | 3 +- internal/repository/repair_pack.go | 5 +- internal/repository/repair_pack_test.go | 5 +- internal/restic/progress.go | 64 +++++++++++++++++++++ internal/restorer/restorer_test.go | 6 +- internal/restorer/restorer_unix_test.go | 3 +- internal/ui/backup/json.go | 2 +- internal/ui/backup/progress.go | 2 +- internal/ui/backup/progress_test.go | 5 +- internal/ui/backup/text.go | 2 +- internal/ui/progress/printer.go | 67 ---------------------- internal/ui/progress/terminal.go | 2 +- internal/ui/restore/json.go | 3 +- internal/ui/restore/progress.go | 3 +- internal/ui/restore/progress_test.go | 5 +- internal/ui/restore/text.go | 3 +- 44 files changed, 165 insertions(+), 173 deletions(-) delete mode 100644 internal/ui/progress/printer.go diff --git a/cmd/restic/cmd_check.go b/cmd/restic/cmd_check.go index b2741eb75..c2ac03860 100644 --- a/cmd/restic/cmd_check.go +++ b/cmd/restic/cmd_check.go @@ -179,7 +179,7 @@ func parsePercentage(s string) (float64, error) { // - if the user explicitly requested --no-cache, we don't use any cache // - if the user provides --cache-dir, we use a cache in a temporary sub-directory of the specified directory and the sub-directory is deleted after the check // - by default, we use a cache in a temporary directory that is deleted after the check -func prepareCheckCache(opts CheckOptions, gopts *global.Options, printer progress.Printer) (cleanup func()) { +func prepareCheckCache(opts CheckOptions, gopts *global.Options, printer restic.Printer) (cleanup func()) { cleanup = func() {} if opts.WithCache { // use the default cache, no setup needed @@ -229,7 +229,7 @@ func prepareCheckCache(opts CheckOptions, gopts *global.Options, printer progres func runCheck(ctx context.Context, opts CheckOptions, gopts global.Options, args []string, term ui.Terminal) (checkSummary, error) { summary := checkSummary{MessageType: "summary"} - var printer progress.Printer + var printer restic.Printer if !gopts.JSON { printer = progress.NewTerminalPrinter(gopts.JSON, gopts.Verbosity, term) } else { @@ -428,7 +428,7 @@ func runCheck(ctx context.Context, opts CheckOptions, gopts global.Options, args return summary, nil } -func buildPacksFilter(opts CheckOptions, printer progress.Printer, +func buildPacksFilter(opts CheckOptions, printer restic.Printer, filteredStatus bool) (func(packs map[restic.ID]int64) map[restic.ID]int64, error) { typeData := "" if filteredStatus { diff --git a/cmd/restic/cmd_check_test.go b/cmd/restic/cmd_check_test.go index db05051f7..74f1fbc8e 100644 --- a/cmd/restic/cmd_check_test.go +++ b/cmd/restic/cmd_check_test.go @@ -12,7 +12,6 @@ import ( "github.com/restic/restic/internal/global" "github.com/restic/restic/internal/restic" rtest "github.com/restic/restic/internal/test" - "github.com/restic/restic/internal/ui/progress" ) func TestParsePercentage(t *testing.T) { @@ -204,7 +203,7 @@ func TestPrepareCheckCache(t *testing.T) { rtest.OK(t, err) } gopts := global.Options{CacheDir: tmpDirBase} - cleanup := prepareCheckCache(testCase.opts, &gopts, progress.NewNoopPrinter()) + cleanup := prepareCheckCache(testCase.opts, &gopts, restic.NewNoopPrinter()) files, err := os.ReadDir(tmpDirBase) rtest.OK(t, err) @@ -234,7 +233,7 @@ func TestPrepareCheckCache(t *testing.T) { func TestPrepareDefaultCheckCache(t *testing.T) { gopts := global.Options{CacheDir: ""} - cleanup := prepareCheckCache(CheckOptions{}, &gopts, progress.NewNoopPrinter()) + cleanup := prepareCheckCache(CheckOptions{}, &gopts, restic.NewNoopPrinter()) _, err := os.ReadDir(gopts.CacheDir) rtest.OK(t, err) diff --git a/cmd/restic/cmd_copy.go b/cmd/restic/cmd_copy.go index 039019760..90605e498 100644 --- a/cmd/restic/cmd_copy.go +++ b/cmd/restic/cmd_copy.go @@ -75,7 +75,7 @@ func (opts *CopyOptions) AddFlags(f *pflag.FlagSet) { // collectAllSnapshots: select all snapshot trees to be copied func collectAllSnapshots(ctx context.Context, opts CopyOptions, srcSnapshotLister restic.Lister, srcRepo restic.Repository, - dstSnapshotByOriginal map[restic.ID][]*data.Snapshot, args []string, printer progress.Printer, + dstSnapshotByOriginal map[restic.ID][]*data.Snapshot, args []string, printer restic.Printer, ) iter.Seq[*data.Snapshot] { return func(yield func(*data.Snapshot) bool) { for sn := range FindFilteredSnapshots(ctx, srcSnapshotLister, srcRepo, &opts.SnapshotFilter, args, printer) { @@ -190,7 +190,7 @@ func similarSnapshots(sna *data.Snapshot, snb *data.Snapshot) bool { // copyTreeBatched copies multiple snapshots in one go. Snapshots are written after // data equivalent to at least 10 packfiles was written. func copyTreeBatched(ctx context.Context, srcRepo *repository.Repository, dstRepo restic.Repository, - selectedSnapshots iter.Seq[*data.Snapshot], printer progress.Printer) error { + selectedSnapshots iter.Seq[*data.Snapshot], printer restic.Printer) error { // remember already processed trees across all snapshots visitedTrees := srcRepo.NewAssociatedBlobSet() @@ -255,7 +255,7 @@ func copyTreeBatched(ctx context.Context, srcRepo *repository.Repository, dstRep } func copyTree(ctx context.Context, srcRepo *repository.Repository, dstRepo restic.Repository, - visitedTrees restic.AssociatedBlobSet, rootTreeID restic.ID, printer progress.Printer, uploader restic.BlobSaverWithAsync) (uint64, error) { + visitedTrees restic.AssociatedBlobSet, rootTreeID restic.ID, printer restic.Printer, uploader restic.BlobSaverWithAsync) (uint64, error) { copyBlobs := srcRepo.NewAssociatedBlobSet() packList := restic.NewIDSet() @@ -312,7 +312,7 @@ func copyTree(ctx context.Context, srcRepo *repository.Repository, dstRepo resti } // copyStats: print statistics for the blobs to be copied -func copyStats(srcRepo restic.Repository, copyBlobs restic.AssociatedBlobSet, packList restic.IDSet, printer progress.Printer) uint64 { +func copyStats(srcRepo restic.Repository, copyBlobs restic.AssociatedBlobSet, packList restic.IDSet, printer restic.Printer) uint64 { // count and size countBlobs := 0 sizeBlobs := uint64(0) @@ -329,7 +329,7 @@ func copyStats(srcRepo restic.Repository, copyBlobs restic.AssociatedBlobSet, pa return sizeBlobs } -func copySaveSnapshot(ctx context.Context, sn *data.Snapshot, dstRepo restic.Repository, printer progress.Printer) error { +func copySaveSnapshot(ctx context.Context, sn *data.Snapshot, dstRepo restic.Repository, printer restic.Printer) error { sn.Parent = nil // Parent does not have relevance in the new repo. // Use Original as a persistent snapshot ID if sn.Original == nil { diff --git a/cmd/restic/cmd_generate.go b/cmd/restic/cmd_generate.go index 0a491518d..887dc487d 100644 --- a/cmd/restic/cmd_generate.go +++ b/cmd/restic/cmd_generate.go @@ -7,6 +7,7 @@ import ( "github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/global" + "github.com/restic/restic/internal/restic" "github.com/restic/restic/internal/ui" "github.com/restic/restic/internal/ui/progress" "github.com/spf13/cobra" @@ -55,7 +56,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, printer progress.Printer) error { +func writeManpages(root *cobra.Command, dir string, printer restic.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 { @@ -73,7 +74,7 @@ func writeManpages(root *cobra.Command, dir string, printer progress.Printer) er return doc.GenManTree(root, header, dir) } -func writeCompletion(filename string, shell string, generate func(w io.Writer) error, printer progress.Printer, gopts global.Options) (err error) { +func writeCompletion(filename string, shell string, generate func(w io.Writer) error, printer restic.Printer, gopts global.Options) (err error) { printer.PT("writing %s completion file to %v", shell, filename) var outWriter io.Writer if filename != "-" { diff --git a/cmd/restic/cmd_init.go b/cmd/restic/cmd_init.go index 9bf765353..842db2c84 100644 --- a/cmd/restic/cmd_init.go +++ b/cmd/restic/cmd_init.go @@ -108,7 +108,7 @@ func runInit(ctx context.Context, opts InitOptions, gopts global.Options, args [ return nil } -func maybeReadChunkerPolynomial(ctx context.Context, opts InitOptions, gopts global.Options, printer progress.Printer) (*chunker.Pol, error) { +func maybeReadChunkerPolynomial(ctx context.Context, opts InitOptions, gopts global.Options, printer restic.Printer) (*chunker.Pol, error) { if opts.CopyChunkerParameters { otherGopts, _, err := opts.SecondaryRepoOptions.FillGlobalOpts(ctx, gopts, "secondary") if err != nil { diff --git a/cmd/restic/cmd_init_integration_test.go b/cmd/restic/cmd_init_integration_test.go index d30a7c078..d2e96a613 100644 --- a/cmd/restic/cmd_init_integration_test.go +++ b/cmd/restic/cmd_init_integration_test.go @@ -10,7 +10,6 @@ import ( "github.com/restic/restic/internal/repository" "github.com/restic/restic/internal/restic" rtest "github.com/restic/restic/internal/test" - "github.com/restic/restic/internal/ui/progress" ) func testRunInit(t testing.TB, gopts global.Options) { @@ -57,14 +56,14 @@ func TestInitCopyChunkerParams(t *testing.T) { var repo *repository.Repository err = withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error { - repo, err = global.OpenRepository(ctx, gopts, progress.NewNoopPrinter()) + repo, err = global.OpenRepository(ctx, gopts, restic.NewNoopPrinter()) return err }) rtest.OK(t, err) var otherRepo *repository.Repository err = withTermStatus(t, env2.gopts, func(ctx context.Context, gopts global.Options) error { - otherRepo, err = global.OpenRepository(ctx, gopts, progress.NewNoopPrinter()) + otherRepo, err = global.OpenRepository(ctx, gopts, restic.NewNoopPrinter()) return err }) rtest.OK(t, err) diff --git a/cmd/restic/cmd_key_add.go b/cmd/restic/cmd_key_add.go index c0d156189..878a8f1a6 100644 --- a/cmd/restic/cmd_key_add.go +++ b/cmd/restic/cmd_key_add.go @@ -7,6 +7,7 @@ import ( "github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/global" "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" @@ -70,7 +71,7 @@ func runKeyAdd(ctx context.Context, gopts global.Options, opts KeyAddOptions, ar return addKey(ctx, repo, gopts, opts, printer) } -func addKey(ctx context.Context, repo *repository.Repository, gopts global.Options, opts KeyAddOptions, printer progress.Printer) error { +func addKey(ctx context.Context, repo *repository.Repository, gopts global.Options, opts KeyAddOptions, printer restic.Printer) error { pw, err := getNewPassword(ctx, gopts, opts.NewPasswordFile, opts.InsecureNoPassword) if err != nil { return err diff --git a/cmd/restic/cmd_key_integration_test.go b/cmd/restic/cmd_key_integration_test.go index eaa3fd085..c349fd5c4 100644 --- a/cmd/restic/cmd_key_integration_test.go +++ b/cmd/restic/cmd_key_integration_test.go @@ -3,6 +3,7 @@ package main import ( "bufio" "context" + "github.com/restic/restic/internal/restic" "os" "path/filepath" "regexp" @@ -13,7 +14,6 @@ import ( "github.com/restic/restic/internal/global" "github.com/restic/restic/internal/repository" rtest "github.com/restic/restic/internal/test" - "github.com/restic/restic/internal/ui/progress" ) func testRunKeyListOtherIDs(t testing.TB, gopts global.Options) []string { @@ -63,7 +63,7 @@ func testRunKeyAddNewKeyUserHost(t testing.TB, gopts global.Options) { rtest.OK(t, err) _ = withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error { - repo, err := global.OpenRepository(ctx, gopts, progress.NewNoopPrinter()) + repo, err := global.OpenRepository(ctx, gopts, restic.NewNoopPrinter()) rtest.OK(t, err) err = repo.SearchKey(ctx, testKeyNewPassword, 2, "") rtest.OK(t, err) @@ -107,7 +107,7 @@ func testRunKeyPasswdUserHost(t testing.TB, newPassword string, gopts global.Opt gopts.Password = testKeyNewPassword _ = withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error { - repo, err := global.OpenRepository(ctx, gopts, progress.NewNoopPrinter()) + repo, err := global.OpenRepository(ctx, gopts, restic.NewNoopPrinter()) rtest.OK(t, err) err = repo.SearchKey(ctx, testKeyNewPassword, 1, "") rtest.OK(t, err) diff --git a/cmd/restic/cmd_key_list.go b/cmd/restic/cmd_key_list.go index 77da22e34..1d86e3e79 100644 --- a/cmd/restic/cmd_key_list.go +++ b/cmd/restic/cmd_key_list.go @@ -56,7 +56,7 @@ func runKeyList(ctx context.Context, gopts global.Options, args []string, term u return listKeys(ctx, repo, gopts, printer) } -func listKeys(ctx context.Context, s *repository.Repository, gopts global.Options, printer progress.Printer) error { +func listKeys(ctx context.Context, s *repository.Repository, gopts global.Options, printer restic.Printer) error { type keyInfo struct { Current bool `json:"current"` ID string `json:"id"` diff --git a/cmd/restic/cmd_key_passwd.go b/cmd/restic/cmd_key_passwd.go index b46a12db2..dd698ce50 100644 --- a/cmd/restic/cmd_key_passwd.go +++ b/cmd/restic/cmd_key_passwd.go @@ -7,6 +7,7 @@ import ( "github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/global" "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" @@ -65,7 +66,7 @@ func runKeyPasswd(ctx context.Context, gopts global.Options, opts KeyPasswdOptio return changePassword(ctx, repo, gopts, opts, printer) } -func changePassword(ctx context.Context, repo *repository.Repository, gopts global.Options, opts KeyPasswdOptions, printer progress.Printer) error { +func changePassword(ctx context.Context, repo *repository.Repository, gopts global.Options, opts KeyPasswdOptions, printer restic.Printer) error { pw, err := getNewPassword(ctx, gopts, opts.NewPasswordFile, opts.InsecureNoPassword) if err != nil { return err diff --git a/cmd/restic/cmd_key_remove.go b/cmd/restic/cmd_key_remove.go index 03eb8f313..e8300daa0 100644 --- a/cmd/restic/cmd_key_remove.go +++ b/cmd/restic/cmd_key_remove.go @@ -53,7 +53,7 @@ func runKeyRemove(ctx context.Context, gopts global.Options, args []string, term return deleteKey(ctx, repo, args[0], printer) } -func deleteKey(ctx context.Context, repo *repository.Repository, idPrefix string, printer progress.Printer) error { +func deleteKey(ctx context.Context, repo *repository.Repository, idPrefix string, printer restic.Printer) error { id, err := restic.Find(ctx, repo, restic.KeyFile, idPrefix) if err != nil { return err diff --git a/cmd/restic/cmd_list.go b/cmd/restic/cmd_list.go index 677f85a12..86687ed72 100644 --- a/cmd/restic/cmd_list.go +++ b/cmd/restic/cmd_list.go @@ -96,7 +96,7 @@ func runList(ctx context.Context, gopts global.Options, args []string, term ui.T // packfileList handles the list packs variant. // It prints a sorted list of packfiles belonging to this snapshot. -func packfileList(ctx context.Context, repo restic.Repository, snapshotID string, printer progress.Printer) error { +func packfileList(ctx context.Context, repo restic.Repository, snapshotID string, printer restic.Printer) error { sn, _, err := (&data.SnapshotFilter{}).FindLatest(ctx, repo, repo, snapshotID) if err != nil { return fmt.Errorf("required snapshot ID %q not found", snapshotID) diff --git a/cmd/restic/cmd_migrate.go b/cmd/restic/cmd_migrate.go index 1832b8b12..b0862c620 100644 --- a/cmd/restic/cmd_migrate.go +++ b/cmd/restic/cmd_migrate.go @@ -53,7 +53,7 @@ func (opts *MigrateOptions) AddFlags(f *pflag.FlagSet) { f.BoolVarP(&opts.Force, "force", "f", false, `apply a migration a second time`) } -func checkMigrations(ctx context.Context, repo restic.Repository, printer progress.Printer) error { +func checkMigrations(ctx context.Context, repo restic.Repository, printer restic.Printer) error { printer.P("available migrations:\n") found := false @@ -76,7 +76,7 @@ func checkMigrations(ctx context.Context, repo restic.Repository, printer progre return nil } -func applyMigrations(ctx context.Context, opts MigrateOptions, gopts global.Options, repo restic.Repository, args []string, term ui.Terminal, printer progress.Printer) error { +func applyMigrations(ctx context.Context, opts MigrateOptions, gopts global.Options, repo restic.Repository, args []string, term ui.Terminal, printer restic.Printer) error { var firsterr error for _, name := range args { found := false diff --git a/cmd/restic/cmd_prune.go b/cmd/restic/cmd_prune.go index eab66b839..14275ac0f 100644 --- a/cmd/restic/cmd_prune.go +++ b/cmd/restic/cmd_prune.go @@ -193,7 +193,7 @@ func runPrune(ctx context.Context, opts PruneOptions, gopts global.Options, term return runPruneWithRepo(ctx, opts, gopts, repo, restic.NewIDSet(), printer) } -func runPruneWithRepo(ctx context.Context, opts PruneOptions, gopts global.Options, repo *repository.Repository, ignoreSnapshots restic.IDSet, printer progress.Printer) error { +func runPruneWithRepo(ctx context.Context, opts PruneOptions, gopts global.Options, repo *repository.Repository, ignoreSnapshots restic.IDSet, printer restic.Printer) error { if repo.Cache() == nil && !gopts.JSON { printer.S("warning: running prune without a cache, this may be very slow!") } @@ -246,7 +246,7 @@ func runPruneWithRepo(ctx context.Context, opts PruneOptions, gopts global.Optio } // printPruneStats prints out the statistics -func printPruneStats(printer progress.Printer, stats repository.PruneStats) error { +func printPruneStats(printer restic.Printer, stats repository.PruneStats) error { printer.V("\nused: %10d blobs / %s", stats.Blobs.Used, ui.FormatBytes(stats.Size.Used)) if stats.Blobs.Duplicate > 0 { printer.V("duplicates: %10d blobs / %s", stats.Blobs.Duplicate, ui.FormatBytes(stats.Size.Duplicate)) @@ -282,7 +282,7 @@ func printPruneStats(printer progress.Printer, stats repository.PruneStats) erro return nil } -func getUsedBlobs(ctx context.Context, repo restic.Repository, usedBlobs restic.FindBlobSet, ignoreSnapshots restic.IDSet, printer progress.Printer) error { +func getUsedBlobs(ctx context.Context, repo restic.Repository, usedBlobs restic.FindBlobSet, ignoreSnapshots restic.IDSet, printer restic.Printer) error { var snapshotTrees restic.IDs printer.P("loading all snapshots...") err := data.ForAllSnapshots(ctx, repo, repo, ignoreSnapshots, diff --git a/cmd/restic/cmd_recover.go b/cmd/restic/cmd_recover.go index e716d80e4..cc62fa319 100644 --- a/cmd/restic/cmd_recover.go +++ b/cmd/restic/cmd_recover.go @@ -174,7 +174,7 @@ func runRecover(ctx context.Context, gopts global.Options, term ui.Terminal) err } -func createSnapshot(ctx context.Context, printer progress.Printer, name, hostname string, tags []string, repo restic.SaverUnpacked[restic.WriteableFileType], tree *restic.ID) error { +func createSnapshot(ctx context.Context, printer restic.Printer, name, hostname string, tags []string, repo restic.SaverUnpacked[restic.WriteableFileType], tree *restic.ID) error { sn, err := data.NewSnapshot([]string{name}, tags, hostname, time.Now()) if err != nil { return errors.Fatalf("unable to save snapshot: %v", err) diff --git a/cmd/restic/cmd_restore.go b/cmd/restic/cmd_restore.go index 9648ac7f7..31081f011 100644 --- a/cmd/restic/cmd_restore.go +++ b/cmd/restic/cmd_restore.go @@ -11,9 +11,9 @@ import ( "github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/filter" "github.com/restic/restic/internal/global" + "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" restoreui "github.com/restic/restic/internal/ui/restore" "github.com/spf13/cobra" @@ -281,7 +281,7 @@ func runRestore(ctx context.Context, opts RestoreOptions, gopts global.Options, return nil } -func getXattrSelectFilter(opts RestoreOptions, printer progress.Printer) (func(xattrName string) bool, error) { +func getXattrSelectFilter(opts RestoreOptions, printer restic.Printer) (func(xattrName string) bool, error) { hasXattrExcludes := len(opts.ExcludeXattrPattern) > 0 hasXattrIncludes := len(opts.IncludeXattrPattern) > 0 diff --git a/cmd/restic/cmd_rewrite.go b/cmd/restic/cmd_rewrite.go index 1050730d2..fe51a5d10 100644 --- a/cmd/restic/cmd_rewrite.go +++ b/cmd/restic/cmd_rewrite.go @@ -129,7 +129,7 @@ func (opts *RewriteOptions) AddFlags(f *pflag.FlagSet) { // be updated accordingly. type rewriteFilterFunc func(ctx context.Context, sn *data.Snapshot, uploader restic.BlobSaver) (restic.ID, *data.SnapshotSummary, error) -func rewriteSnapshot(ctx context.Context, repo *repository.Repository, sn *data.Snapshot, opts RewriteOptions, printer progress.Printer) (bool, error) { +func rewriteSnapshot(ctx context.Context, repo *repository.Repository, sn *data.Snapshot, opts RewriteOptions, printer restic.Printer) (bool, error) { if sn.Tree == nil { return false, errors.Errorf("snapshot %v has nil tree", sn.ID().Str()) } @@ -191,7 +191,7 @@ func rewriteSnapshot(ctx context.Context, repo *repository.Repository, sn *data. } func filterAndReplaceSnapshot(ctx context.Context, repo restic.Repository, sn *data.Snapshot, - filter rewriteFilterFunc, dryRun bool, forget bool, newMetadata *snapshotMetadata, addTag string, printer progress.Printer, + filter rewriteFilterFunc, dryRun bool, forget bool, newMetadata *snapshotMetadata, addTag string, printer restic.Printer, keepEmptySnapshot bool) (bool, error) { var filteredTree restic.ID @@ -359,7 +359,7 @@ func runRewrite(ctx context.Context, opts RewriteOptions, gopts global.Options, return nil } -func gatherIncludeFilters(includeByNameFuncs []filter.IncludeByNameFunc, printer progress.Printer) (rewriteNode walker.NodeRewriteFunc, keepEmptyDirectory walker.NodeKeepEmptyDirectoryFunc) { +func gatherIncludeFilters(includeByNameFuncs []filter.IncludeByNameFunc, printer restic.Printer) (rewriteNode walker.NodeRewriteFunc, keepEmptyDirectory walker.NodeKeepEmptyDirectoryFunc) { inSelectByName := func(nodepath string, node *data.Node) bool { for _, include := range includeByNameFuncs { matched, childMayMatch := include(nodepath) @@ -406,7 +406,7 @@ func gatherIncludeFilters(includeByNameFuncs []filter.IncludeByNameFunc, printer return rewriteNode, keepEmptyDirectory } -func gatherExcludeFilters(excludeByNameFuncs []filter.RejectByNameFunc, printer progress.Printer) (rewriteNode walker.NodeRewriteFunc) { +func gatherExcludeFilters(excludeByNameFuncs []filter.RejectByNameFunc, printer restic.Printer) (rewriteNode walker.NodeRewriteFunc) { exSelectByName := func(nodepath string) bool { for _, reject := range excludeByNameFuncs { if reject(nodepath) { diff --git a/cmd/restic/cmd_stats.go b/cmd/restic/cmd_stats.go index 90b9259b7..408579cf7 100644 --- a/cmd/restic/cmd_stats.go +++ b/cmd/restic/cmd_stats.go @@ -376,7 +376,7 @@ const ( countModeDebug = "debug" ) -func statsDebug(ctx context.Context, repo restic.Repository, printer progress.Printer) error { +func statsDebug(ctx context.Context, repo restic.Repository, printer restic.Printer) error { printer.E("Collecting size statistics\n\n") for _, t := range []restic.FileType{restic.KeyFile, restic.LockFile, restic.IndexFile, restic.SnapshotFile, restic.PackFile} { hist, err := statsDebugFileType(ctx, repo, t) diff --git a/cmd/restic/find.go b/cmd/restic/find.go index cc61a6ba2..c8049ba04 100644 --- a/cmd/restic/find.go +++ b/cmd/restic/find.go @@ -6,7 +6,6 @@ import ( "github.com/restic/restic/internal/data" "github.com/restic/restic/internal/restic" - "github.com/restic/restic/internal/ui/progress" "github.com/spf13/pflag" ) @@ -49,7 +48,7 @@ func finalizeSnapshotFilter(filt *data.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 *data.SnapshotFilter, snapshotIDs []string, printer progress.Printer) <-chan *data.Snapshot { +func FindFilteredSnapshots(ctx context.Context, be restic.Lister, loader restic.LoaderUnpacked, f *data.SnapshotFilter, snapshotIDs []string, printer restic.Printer) <-chan *data.Snapshot { out := make(chan *data.Snapshot) go func() { defer close(out) diff --git a/cmd/restic/lock.go b/cmd/restic/lock.go index eb2fc47ed..57e831908 100644 --- a/cmd/restic/lock.go +++ b/cmd/restic/lock.go @@ -5,10 +5,10 @@ import ( "github.com/restic/restic/internal/global" "github.com/restic/restic/internal/repository" - "github.com/restic/restic/internal/ui/progress" + "github.com/restic/restic/internal/restic" ) -func internalOpenWithLocked(ctx context.Context, gopts global.Options, dryRun bool, exclusive bool, printer progress.Printer) (context.Context, *repository.Repository, func(), error) { +func internalOpenWithLocked(ctx context.Context, gopts global.Options, dryRun bool, exclusive bool, printer restic.Printer) (context.Context, *repository.Repository, func(), error) { repo, err := global.OpenRepository(ctx, gopts, printer) if err != nil { return nil, nil, nil, err @@ -31,18 +31,18 @@ func internalOpenWithLocked(ctx context.Context, gopts global.Options, dryRun bo return ctx, repo, unlock, nil } -func openWithReadLock(ctx context.Context, gopts global.Options, noLock bool, printer progress.Printer) (context.Context, *repository.Repository, func(), error) { +func openWithReadLock(ctx context.Context, gopts global.Options, noLock bool, printer restic.Printer) (context.Context, *repository.Repository, func(), error) { // TODO enforce read-only operations once the locking code has moved to the repository // As in-depth hardening, put the repository into read-only mode if noLock is true // Not possible if the repository has to be locked. return internalOpenWithLocked(ctx, gopts, noLock, false, printer) } -func openWithAppendLock(ctx context.Context, gopts global.Options, dryRun bool, printer progress.Printer) (context.Context, *repository.Repository, func(), error) { +func openWithAppendLock(ctx context.Context, gopts global.Options, dryRun bool, printer restic.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, printer) } -func openWithExclusiveLock(ctx context.Context, gopts global.Options, dryRun bool, printer progress.Printer) (context.Context, *repository.Repository, func(), error) { +func openWithExclusiveLock(ctx context.Context, gopts global.Options, dryRun bool, printer restic.Printer) (context.Context, *repository.Repository, func(), error) { return internalOpenWithLocked(ctx, gopts, dryRun, true, printer) } diff --git a/doc/090_participating.rst b/doc/090_participating.rst index 30c885308..7542efc29 100644 --- a/doc/090_participating.rst +++ b/doc/090_participating.rst @@ -184,7 +184,8 @@ default backup, to be found at ``cmd/restic/testdata/backup-data.tar.gz``. In this compressed archive you will find files, hardlinked files, symlinked files, an empty directory and a simple directory structure which is good for testing purposes. -Commands that require a ``progress.Printer`` should either be wrapped in ``withTermStatus`` or ``withCaptureStdout``. +Commands that require a ``restic.Printer`` should either be wrapped in ``withTermStatus`` or ``withCaptureStdout``. +The interface is defined in ``internal/restic``; ``progress.NewTerminalPrinter`` returns a ``restic.Printer``. If you want to analyze JSON output, you use ``withCaptureStdout()``. It returns the generated output in a ``*bytes.Buffer``. JSON output can be unmarshalled to produce the appropriate go structures; see diff --git a/internal/global/global.go b/internal/global/global.go index 73a964a60..2448670f3 100644 --- a/internal/global/global.go +++ b/internal/global/global.go @@ -25,7 +25,6 @@ import ( "github.com/restic/restic/internal/restic" "github.com/restic/restic/internal/textfile" "github.com/restic/restic/internal/ui" - "github.com/restic/restic/internal/ui/progress" "github.com/spf13/pflag" "github.com/restic/restic/internal/errors" @@ -298,7 +297,7 @@ func readRepo(gopts Options) (string, error) { const maxKeys = 20 // OpenRepository reads the password and opens the repository. -func OpenRepository(ctx context.Context, gopts Options, printer progress.Printer) (*repository.Repository, error) { +func OpenRepository(ctx context.Context, gopts Options, printer restic.Printer) (*repository.Repository, error) { repo, err := readRepo(gopts) if err != nil { return nil, err @@ -369,7 +368,7 @@ func createRepositoryInstance(be backend.Backend, gopts Options) (*repository.Re } // decryptRepository handles password reading and decrypts the repository. -func decryptRepository(ctx context.Context, s *repository.Repository, gopts *Options, printer progress.Printer) error { +func decryptRepository(ctx context.Context, s *repository.Repository, gopts *Options, printer restic.Printer) error { passwordTriesLeft := 1 if gopts.Term.InputIsTerminal() && gopts.Password == "" && !gopts.InsecureNoPassword { passwordTriesLeft = 3 @@ -406,7 +405,7 @@ func decryptRepository(ctx context.Context, s *repository.Repository, gopts *Opt } // printRepositoryInfo displays the repository ID, version and compression level. -func printRepositoryInfo(s *repository.Repository, gopts Options, printer progress.Printer) { +func printRepositoryInfo(s *repository.Repository, gopts Options, printer restic.Printer) { id := s.Config().ID if len(id) > 8 { id = id[:8] @@ -419,7 +418,7 @@ func printRepositoryInfo(s *repository.Repository, gopts Options, printer progre } // setupCache creates a new cache and removes old cache directories if instructed to do so. -func setupCache(s *repository.Repository, gopts Options, printer progress.Printer) error { +func setupCache(s *repository.Repository, gopts Options, printer restic.Printer) error { c, err := cache.New(s.Config().ID, gopts.CacheDir) if err != nil { printer.E("unable to open cache: %v", err) @@ -461,7 +460,7 @@ func setupCache(s *repository.Repository, gopts Options, printer progress.Printe } // CreateRepository a repository with the given version and chunker polynomial. -func CreateRepository(ctx context.Context, gopts Options, version uint, chunkerPolynomial *chunker.Pol, printer progress.Printer) (*repository.Repository, error) { +func CreateRepository(ctx context.Context, gopts Options, version uint, chunkerPolynomial *chunker.Pol, printer restic.Printer) (*repository.Repository, error) { if version < restic.MinRepoVersion || version > restic.MaxRepoVersion { return nil, errors.Fatalf("only repository versions between %v and %v are allowed", restic.MinRepoVersion, restic.MaxRepoVersion) } @@ -496,7 +495,7 @@ func CreateRepository(ctx context.Context, gopts Options, version uint, chunkerP return s, nil } -func innerOpenBackend(ctx context.Context, s string, gopts Options, opts options.Options, create bool, printer progress.Printer) (backend.Backend, error) { +func innerOpenBackend(ctx context.Context, s string, gopts Options, opts options.Options, create bool, printer restic.Printer) (backend.Backend, error) { debug.Log("parsing location %v", location.StripPassword(gopts.Backends, s)) scheme, cfg, err := parseConfig(gopts.Backends, s, opts) @@ -559,7 +558,7 @@ func setupTransport(gopts Options) (http.RoundTripper, limiter.Limiter, error) { } // createOrOpenBackend creates or opens a backend using the appropriate factory method. -func createOrOpenBackend(ctx context.Context, scheme string, cfg interface{}, rt http.RoundTripper, lim limiter.Limiter, gopts Options, s string, create bool, printer progress.Printer) (backend.Backend, error) { +func createOrOpenBackend(ctx context.Context, scheme string, cfg interface{}, rt http.RoundTripper, lim limiter.Limiter, gopts Options, s string, create bool, printer restic.Printer) (backend.Backend, error) { factory := gopts.Backends.Lookup(scheme) if factory == nil { return nil, errors.Fatalf("invalid backend: %q", scheme) @@ -589,7 +588,7 @@ func createOrOpenBackend(ctx context.Context, scheme string, cfg interface{}, rt } // wrapBackend applies debug logging, test hooks, and retry wrapper to the backend. -func wrapBackend(be backend.Backend, gopts Options, printer progress.Printer) (backend.Backend, error) { +func wrapBackend(be backend.Backend, gopts Options, printer restic.Printer) (backend.Backend, error) { // wrap with debug logging and connection limiting be = logger.New(sema.NewBackend(be)) diff --git a/internal/repository/debug.go b/internal/repository/debug.go index 4f422409c..4d1e8267c 100644 --- a/internal/repository/debug.go +++ b/internal/repository/debug.go @@ -21,7 +21,6 @@ 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/progress" ) type packDumpEntry struct { @@ -46,7 +45,7 @@ func writePackDumpJSON(wr io.Writer, item any) error { } // DumpPacks lists each pack file and writes its header blob layout as JSON to wr. -func DumpPacks(ctx context.Context, repo *Repository, wr io.Writer, printer progress.Printer) error { +func DumpPacks(ctx context.Context, repo *Repository, wr io.Writer, printer restic.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) @@ -75,7 +74,7 @@ func DumpPacks(ctx context.Context, repo *Repository, wr io.Writer, printer prog } // DumpIndexes loads each on-disk index file and writes its debug dump to wr. -func DumpIndexes(ctx context.Context, repo restic.ListerLoaderUnpacked, wr io.Writer, printer progress.Printer) error { +func DumpIndexes(ctx context.Context, repo restic.ListerLoaderUnpacked, wr io.Writer, printer restic.Printer) error { return index.ForAllIndexes(ctx, repo, repo, func(id restic.ID, idx *index.Index, err error) error { printer.S("index_id: %v", id) if err != nil { @@ -95,7 +94,7 @@ type ExaminePackOptions struct { } // ExaminePack loads and inspects a pack file and its index entries. -func ExaminePack(ctx context.Context, repo *Repository, id restic.ID, opts ExaminePackOptions, printer progress.Printer) error { +func ExaminePack(ctx context.Context, repo *Repository, id restic.ID, opts ExaminePackOptions, printer restic.Printer) error { printer.S("examine %v", id) buf, err := repo.LoadRaw(ctx, restic.PackFile, id) @@ -146,7 +145,7 @@ func ExaminePack(ctx context.Context, repo *Repository, id restic.ID, opts Exami return nil } -func checkPackSize(blobs pack.Blobs, fileSize int, printer progress.Printer) { +func checkPackSize(blobs pack.Blobs, fileSize int, printer restic.Printer) { // track current size and offset var size, offset uint64 @@ -169,7 +168,7 @@ func checkPackSize(blobs pack.Blobs, fileSize int, printer progress.Printer) { } } -func tryRepairWithBitflip(key *crypto.Key, input []byte, bytewise bool, printer progress.Printer) []byte { +func tryRepairWithBitflip(key *crypto.Key, input []byte, bytewise bool, printer restic.Printer) []byte { if bytewise { printer.S(" trying to repair blob by finding a broken byte") } else { @@ -284,7 +283,7 @@ func decryptUnsigned(k *crypto.Key, buf []byte) []byte { return out } -func loadBlobs(ctx context.Context, opts ExaminePackOptions, repo *Repository, packID restic.ID, list pack.Blobs, printer progress.Printer) error { +func loadBlobs(ctx context.Context, opts ExaminePackOptions, repo *Repository, packID restic.ID, list pack.Blobs, printer restic.Printer) error { dec, err := zstd.NewReader(nil) if err != nil { panic(err) @@ -366,7 +365,7 @@ func loadBlobs(ctx context.Context, opts ExaminePackOptions, repo *Repository, p return err } -func storePlainBlob(id restic.ID, prefix string, plain []byte, printer progress.Printer) error { +func storePlainBlob(id restic.ID, prefix string, plain []byte, printer restic.Printer) error { filename := fmt.Sprintf("%s%s.bin", prefix, id) f, err := os.Create(filename) if err != nil { diff --git a/internal/repository/prune.go b/internal/repository/prune.go index 20459aed0..451267fd3 100644 --- a/internal/repository/prune.go +++ b/internal/repository/prune.go @@ -13,7 +13,6 @@ 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/progress" ) var ErrIndexIncomplete = errors.Fatal("index is not complete") @@ -104,7 +103,7 @@ type packInfoWithID struct { // PlanPrune selects which files to rewrite and which to delete and which blobs to keep. // Also some summary statistics are returned. -func PlanPrune(ctx context.Context, opts PruneOptions, repo *Repository, getUsedBlobs func(ctx context.Context, repo restic.Repository, usedBlobs restic.FindBlobSet) error, printer progress.Printer) (*PrunePlan, error) { +func PlanPrune(ctx context.Context, opts PruneOptions, repo *Repository, getUsedBlobs func(ctx context.Context, repo restic.Repository, usedBlobs restic.FindBlobSet) error, printer restic.Printer) (*PrunePlan, error) { stats := PruneStats{MessageType: "summary"} if opts.UnsafeRecovery { @@ -176,7 +175,7 @@ func PlanPrune(ctx context.Context, opts PruneOptions, repo *Repository, getUsed return &plan, nil } -func packInfoFromIndex(ctx context.Context, idx restic.ListBlobser, usedBlobs *index.AssociatedSet[uint8], stats *PruneStats, printer progress.Printer) (*index.AssociatedSet[uint8], map[restic.ID]packInfo, error) { +func packInfoFromIndex(ctx context.Context, idx restic.ListBlobser, usedBlobs *index.AssociatedSet[uint8], stats *PruneStats, printer restic.Printer) (*index.AssociatedSet[uint8], map[restic.ID]packInfo, error) { // iterate over all blobs in index to find out which blobs are duplicates // The counter in usedBlobs describes how many instances of the blob exist in the repository index // Thus 0 == blob is missing, 1 == blob exists once, >= 2 == duplicates exist @@ -384,7 +383,7 @@ func calculateTargetPacksize(opts PruneOptions, indexPack map[restic.ID]packInfo return targetPackSize } -func decidePackAction(ctx context.Context, opts PruneOptions, repo *Repository, indexPack map[restic.ID]packInfo, stats *PruneStats, printer progress.Printer) (PrunePlan, error) { +func decidePackAction(ctx context.Context, opts PruneOptions, repo *Repository, indexPack map[restic.ID]packInfo, stats *PruneStats, printer restic.Printer) (PrunePlan, error) { removePacksFirst := restic.NewIDSet() removePacks := restic.NewIDSet() repackPacks := restic.NewIDSet() @@ -592,7 +591,7 @@ func (plan *PrunePlan) Stats() PruneStats { // - rebuild the index while ignoring all files that will be deleted // - delete the files // plan.removePacks and plan.ignorePacks are modified in this function. -func (plan *PrunePlan) Execute(ctx context.Context, printer progress.Printer) error { +func (plan *PrunePlan) Execute(ctx context.Context, printer restic.Printer) error { if plan.opts.DryRun { printer.V("Repeated prune dry-runs can report slightly different amounts of data to keep or repack. This is expected behavior.\n\n") if len(plan.removePacksFirst) > 0 { @@ -692,7 +691,7 @@ func (plan *PrunePlan) Execute(ctx context.Context, printer progress.Printer) er // deleteFiles deletes the given fileList of fileType in parallel // if ignoreError=true, it will print a warning if there was an error, else it will abort. -func deleteFiles(ctx context.Context, ignoreError bool, repo restic.RemoverUnpacked[restic.FileType], fileList restic.IDSet, fileType restic.FileType, printer progress.Printer) error { +func deleteFiles(ctx context.Context, ignoreError bool, repo restic.RemoverUnpacked[restic.FileType], fileList restic.IDSet, fileType restic.FileType, printer restic.Printer) error { bar := printer.NewCounter("files deleted") defer bar.Done() diff --git a/internal/repository/prune_internal_test.go b/internal/repository/prune_internal_test.go index 94fcdad41..e49009813 100644 --- a/internal/repository/prune_internal_test.go +++ b/internal/repository/prune_internal_test.go @@ -9,7 +9,6 @@ import ( "github.com/restic/restic/internal/restic" rtest "github.com/restic/restic/internal/test" - "github.com/restic/restic/internal/ui/progress" ) // TestPruneMaxUnusedDuplicate checks that MaxUnused correctly accounts for duplicates. @@ -70,10 +69,10 @@ func TestPruneMaxUnusedDuplicate(t *testing.T) { usedBlobs.Insert(blob) } return nil - }, progress.NewNoopPrinter()) + }, restic.NewNoopPrinter()) rtest.OK(t, err) - rtest.OK(t, plan.Execute(context.TODO(), progress.NewNoopPrinter())) + rtest.OK(t, plan.Execute(context.TODO(), restic.NewNoopPrinter())) rsize := plan.Stats().Size remainingUnusedSize := rsize.Duplicate + rsize.Unused - rsize.Remove - rsize.Repackrm diff --git a/internal/repository/prune_test.go b/internal/repository/prune_test.go index 261c7deed..78d039b4e 100644 --- a/internal/repository/prune_test.go +++ b/internal/repository/prune_test.go @@ -12,7 +12,6 @@ import ( "github.com/restic/restic/internal/repository/pack" "github.com/restic/restic/internal/restic" rtest "github.com/restic/restic/internal/test" - "github.com/restic/restic/internal/ui/progress" ) func testPrune(t *testing.T, opts repository.PruneOptions, errOnUnused bool) { @@ -41,10 +40,10 @@ func testPrune(t *testing.T, opts repository.PruneOptions, errOnUnused bool) { usedBlobs.Insert(blob) } return nil - }, progress.NewNoopPrinter()) + }, restic.NewNoopPrinter()) rtest.OK(t, err) - rtest.OK(t, plan.Execute(context.TODO(), progress.NewNoopPrinter())) + rtest.OK(t, plan.Execute(context.TODO(), restic.NewNoopPrinter())) repo = repository.TestOpenBackend(t, be) repository.TestCheckRepo(t, repo) @@ -165,9 +164,9 @@ func TestPruneSmall(t *testing.T) { usedBlobs.Insert(blob) } return nil - }, progress.NewNoopPrinter()) + }, restic.NewNoopPrinter()) rtest.OK(t, err) - rtest.OK(t, plan.Execute(context.TODO(), progress.NewNoopPrinter())) + rtest.OK(t, plan.Execute(context.TODO(), restic.NewNoopPrinter())) stats := plan.Stats() rtest.Equals(t, stats.Size.Used/blobSize, uint64(numBlobsCreated), fmt.Sprintf("total size of blobs should be %d but is %d", diff --git a/internal/repository/repack_test.go b/internal/repository/repack_test.go index f20605c4a..d98811a7f 100644 --- a/internal/repository/repack_test.go +++ b/internal/repository/repack_test.go @@ -10,7 +10,6 @@ import ( "github.com/restic/restic/internal/repository" "github.com/restic/restic/internal/restic" rtest "github.com/restic/restic/internal/test" - "github.com/restic/restic/internal/ui/progress" ) func randomSize(random *rand.Rand, min, max int) int { @@ -161,7 +160,7 @@ func repack(t *testing.T, repo *repository.Repository, be backend.Backend, packs func rebuildAndReloadIndex(t *testing.T, repo *repository.Repository) { rtest.OK(t, repository.RepairIndex(context.TODO(), repo, repository.RepairIndexOptions{ ReadAllPacks: true, - }, progress.NewNoopPrinter())) + }, restic.NewNoopPrinter())) rtest.OK(t, repo.LoadIndex(context.TODO(), restic.NoopTerminalCounterFactory)) } diff --git a/internal/repository/repair_index.go b/internal/repository/repair_index.go index 1f1691b50..1df9b31b9 100644 --- a/internal/repository/repair_index.go +++ b/internal/repository/repair_index.go @@ -6,14 +6,13 @@ 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/progress" ) type RepairIndexOptions struct { ReadAllPacks bool } -func RepairIndex(ctx context.Context, repo *Repository, opts RepairIndexOptions, printer progress.Printer) error { +func RepairIndex(ctx context.Context, repo *Repository, opts RepairIndexOptions, printer restic.Printer) error { var obsoleteIndexes restic.IDs packSizeFromList := make(map[restic.ID]int64) packSizeFromIndex := make(map[restic.ID]int64) @@ -103,7 +102,7 @@ func RepairIndex(ctx context.Context, repo *Repository, opts RepairIndexOptions, return nil } -func rewriteIndexFiles(ctx context.Context, repo *Repository, removePacks restic.IDSet, oldIndexes restic.IDSet, extraObsolete restic.IDs, printer progress.Printer) error { +func rewriteIndexFiles(ctx context.Context, repo *Repository, removePacks restic.IDSet, oldIndexes restic.IDSet, extraObsolete restic.IDs, printer restic.Printer) error { printer.P("rebuilding index\n") bar := printer.NewCounter("indexes processed") diff --git a/internal/repository/repair_index_test.go b/internal/repository/repair_index_test.go index 3a39f4fa5..5838f37ee 100644 --- a/internal/repository/repair_index_test.go +++ b/internal/repository/repair_index_test.go @@ -10,7 +10,6 @@ import ( "github.com/restic/restic/internal/repository" "github.com/restic/restic/internal/restic" rtest "github.com/restic/restic/internal/test" - "github.com/restic/restic/internal/ui/progress" ) func listIndex(t *testing.T, repo restic.Lister) restic.IDSet { @@ -33,7 +32,7 @@ func testRebuildIndex(t *testing.T, readAllPacks bool, damage func(t *testing.T, repo = repository.TestOpenBackend(t, be) rtest.OK(t, repository.RepairIndex(context.TODO(), repo, repository.RepairIndexOptions{ ReadAllPacks: readAllPacks, - }, progress.NewNoopPrinter())) + }, restic.NewNoopPrinter())) repository.TestCheckRepo(t, repo) } diff --git a/internal/repository/repair_pack.go b/internal/repository/repair_pack.go index bbcd52617..b958914f4 100644 --- a/internal/repository/repair_pack.go +++ b/internal/repository/repair_pack.go @@ -8,10 +8,9 @@ import ( "github.com/restic/restic/internal/repository/pack" "github.com/restic/restic/internal/restic" - "github.com/restic/restic/internal/ui/progress" ) -func RepairPacks(ctx context.Context, repo *Repository, ids restic.IDSet, printer progress.Printer) error { +func RepairPacks(ctx context.Context, repo *Repository, ids restic.IDSet, printer restic.Printer) error { printer.P("salvaging intact data from specified pack files") bar := printer.NewCounter("pack files") bar.SetMax(uint64(len(ids))) @@ -92,7 +91,7 @@ func resolveBlobsForPacks(ctx context.Context, repo *Repository, ids restic.IDSe return packToBlobs, nil } -func reuploadBlobsFromPack(ctx context.Context, repo *Repository, packID restic.ID, blobs pack.Blobs, printer progress.Printer, uploader restic.BlobSaverWithAsync) error { +func reuploadBlobsFromPack(ctx context.Context, repo *Repository, packID restic.ID, blobs pack.Blobs, printer restic.Printer, uploader restic.BlobSaverWithAsync) error { err := repo.loadBlobsFromPack(ctx, packID, blobs, func(blob restic.BlobHandle, buf []byte, err error) error { if err != nil { printer.E("failed to load blob %v: %v", blob.ID, err) diff --git a/internal/repository/repair_pack_test.go b/internal/repository/repair_pack_test.go index 756cc73a1..814a31439 100644 --- a/internal/repository/repair_pack_test.go +++ b/internal/repository/repair_pack_test.go @@ -11,7 +11,6 @@ import ( "github.com/restic/restic/internal/repository" "github.com/restic/restic/internal/restic" rtest "github.com/restic/restic/internal/test" - "github.com/restic/restic/internal/ui/progress" ) func listBlobs(repo restic.Repository) restic.BlobSet { @@ -105,7 +104,7 @@ func testRepairBrokenPack(t *testing.T, version uint) { buf, err := backendtest.LoadAll(context.TODO(), be, h) rtest.OK(t, err) rtest.OK(t, be.Remove(context.TODO(), h)) - rtest.OK(t, repository.RepairIndex(context.TODO(), repo, repository.RepairIndexOptions{}, progress.NewNoopPrinter())) + rtest.OK(t, repository.RepairIndex(context.TODO(), repo, repository.RepairIndexOptions{}, restic.NewNoopPrinter())) rtest.OK(t, be.Save(context.TODO(), h, backend.NewByteReader(buf, be.Hasher()))) @@ -129,7 +128,7 @@ func testRepairBrokenPack(t *testing.T, version uint) { toRepair, damagedBlobs := test.damage(t, random, repo, be, packsBefore) - rtest.OK(t, repository.RepairPacks(context.TODO(), repo, toRepair, progress.NewNoopPrinter())) + rtest.OK(t, repository.RepairPacks(context.TODO(), repo, toRepair, restic.NewNoopPrinter())) // reload index rtest.OK(t, repo.LoadIndex(context.TODO(), restic.NoopTerminalCounterFactory)) diff --git a/internal/restic/progress.go b/internal/restic/progress.go index cdaae9eb1..e9ce10d58 100644 --- a/internal/restic/progress.go +++ b/internal/restic/progress.go @@ -26,3 +26,67 @@ func (noopTerminalCounterFactory) NewCounterTerminalOnly(string) Counter { // NoopTerminalCounterFactory is a TerminalCounterFactory that returns NoopCounter. var NoopTerminalCounterFactory TerminalCounterFactory = noopTerminalCounterFactory{} + +// Printer can return a new counter or print messages 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 reports an error. This message is always printed to stderr. + // Appends a newline if not present. + E(msg string, args ...interface{}) + // 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{}) + // PT prints a message if verbosity >= 1 (neither --quiet nor --verbose is specified) + // and stdout points to a terminal. + // This is used for informational messages. + PT(msg string, args ...interface{}) + // P prints a message if verbosity >= 1 (neither --quiet nor --verbose is specified), + // this is used for normal messages which are not errors. Appends a newline if not present. + P(msg string, args ...interface{}) + // 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 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{}) +} + +// noopPrinter discards all messages. +type noopPrinter struct{} + +var ( + _ Printer = (*noopPrinter)(nil) + _ TerminalCounterFactory = (*noopPrinter)(nil) +) + +// NewNoopPrinter returns a Printer that discards all messages. +func NewNoopPrinter() Printer { + return &noopPrinter{} +} + +func (*noopPrinter) NewCounter(_ string) Counter { + return NoopCounter +} + +func (*noopPrinter) NewCounterTerminalOnly(_ string) Counter { + return NoopCounter +} + +func (*noopPrinter) E(_ string, _ ...interface{}) {} + +func (*noopPrinter) S(_ string, _ ...interface{}) {} + +func (*noopPrinter) PT(_ string, _ ...interface{}) {} + +func (*noopPrinter) P(_ string, _ ...interface{}) {} + +func (*noopPrinter) V(_ string, _ ...interface{}) {} + +func (*noopPrinter) VV(_ string, _ ...interface{}) {} diff --git a/internal/restorer/restorer_test.go b/internal/restorer/restorer_test.go index 71aa52c85..80fe0f631 100644 --- a/internal/restorer/restorer_test.go +++ b/internal/restorer/restorer_test.go @@ -989,7 +989,7 @@ func TestRestorerSparseOverwrite(t *testing.T) { type printerMock struct { s restoreui.State - progress.Printer + restic.Printer } func (p *printerMock) Update(_ restoreui.State, _ time.Duration) { @@ -1102,7 +1102,7 @@ func TestRestorerOverwriteBehavior(t *testing.T) { for _, test := range tests { t.Run("", func(t *testing.T) { - mock := &printerMock{Printer: progress.NewNoopPrinter()} + mock := &printerMock{Printer: restic.NewNoopPrinter()} progress := restoreui.NewProgress(mock, true, false, true) tempdir := saveSnapshotsAndOverwrite(t, baseSnapshot, overwriteSnapshot, Options{}, Options{Overwrite: test.Overwrite, Progress: progress}) @@ -1154,7 +1154,7 @@ func TestRestorerOverwritePartial(t *testing.T) { }, } - mock := &printerMock{Printer: progress.NewNoopPrinter()} + mock := &printerMock{Printer: restic.NewNoopPrinter()} progress := restoreui.NewProgress(mock, true, false, true) saveSnapshotsAndOverwrite(t, baseSnapshot, overwriteSnapshot, Options{}, Options{Overwrite: OverwriteAlways, Progress: progress}) progress.Finish() diff --git a/internal/restorer/restorer_unix_test.go b/internal/restorer/restorer_unix_test.go index 0ee6b885a..7b94ee212 100644 --- a/internal/restorer/restorer_unix_test.go +++ b/internal/restorer/restorer_unix_test.go @@ -4,6 +4,7 @@ package restorer import ( "context" + "github.com/restic/restic/internal/restic" "io/fs" "os" "path/filepath" @@ -88,7 +89,7 @@ func testRestorerProgressBar(t *testing.T, dryRun bool) { }, }, noopGetGenericAttributes) - mock := &printerMock{Printer: progress.NewNoopPrinter()} + mock := &printerMock{Printer: restic.NewNoopPrinter()} progress := restoreui.NewProgress(mock, true, false, true) res := NewRestorer(repo, sn, Options{Progress: progress, DryRun: dryRun}) diff --git a/internal/ui/backup/json.go b/internal/ui/backup/json.go index de91eff50..4996e57b0 100644 --- a/internal/ui/backup/json.go +++ b/internal/ui/backup/json.go @@ -12,7 +12,7 @@ import ( // jsonProgress reports progress for the `backup` command in JSON. type jsonProgress struct { - progress.Printer + restic.Printer term ui.Terminal v uint diff --git a/internal/ui/backup/progress.go b/internal/ui/backup/progress.go index 3675a9d49..c5af04413 100644 --- a/internal/ui/backup/progress.go +++ b/internal/ui/backup/progress.go @@ -21,7 +21,7 @@ type ProgressPrinter interface { Finish(snapshotID restic.ID, summary *archiver.Summary, dryRun bool) Reset() - progress.Printer + restic.Printer } type Counter struct { diff --git a/internal/ui/backup/progress_test.go b/internal/ui/backup/progress_test.go index 6fa8a7ce9..ef396ef80 100644 --- a/internal/ui/backup/progress_test.go +++ b/internal/ui/backup/progress_test.go @@ -8,12 +8,11 @@ import ( "github.com/restic/restic/internal/archiver" "github.com/restic/restic/internal/data" "github.com/restic/restic/internal/restic" - "github.com/restic/restic/internal/ui/progress" ) type mockPrinter struct { sync.Mutex - progress.Printer + restic.Printer dirUnchanged, fileNew bool id restic.ID } @@ -48,7 +47,7 @@ func (p *mockPrinter) Reset() {} func TestProgress(t *testing.T) { t.Parallel() - prnt := &mockPrinter{Printer: progress.NewNoopPrinter()} + prnt := &mockPrinter{Printer: restic.NewNoopPrinter()} prog := newProgress(prnt, time.Millisecond) prog.StartFile("foo") diff --git a/internal/ui/backup/text.go b/internal/ui/backup/text.go index b402e211a..9e91977c2 100644 --- a/internal/ui/backup/text.go +++ b/internal/ui/backup/text.go @@ -13,7 +13,7 @@ import ( // textProgress reports progress for the `backup` command. type textProgress struct { - progress.Printer + restic.Printer term ui.Terminal verbosity uint diff --git a/internal/ui/progress/printer.go b/internal/ui/progress/printer.go deleted file mode 100644 index 3a96ed568..000000000 --- a/internal/ui/progress/printer.go +++ /dev/null @@ -1,67 +0,0 @@ -package progress - -import ( - "github.com/restic/restic/internal/restic" -) - -// A Printer can can return a new counter or print messages -// 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) restic.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) restic.Counter - - // E reports an error. This message is always printed to stderr. - // Appends a newline if not present. - E(msg string, args ...interface{}) - // 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{}) - // PT prints a message if verbosity >= 1 (neither --quiet nor --verbose is specified) - // and stdout points to a terminal. - // This is used for informational messages. - PT(msg string, args ...interface{}) - // P prints a message if verbosity >= 1 (neither --quiet nor --verbose is specified), - // this is used for normal messages which are not errors. Appends a newline if not present. - P(msg string, args ...interface{}) - // 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 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{}) -} - -// noopPrinter discards all messages. -type noopPrinter struct{} - -var _ Printer = (*noopPrinter)(nil) - -// NewNoopPrinter returns a Printer that discards all messages. -func NewNoopPrinter() Printer { - return &noopPrinter{} -} - -func (*noopPrinter) NewCounter(_ string) restic.Counter { - return restic.NoopCounter -} - -func (*noopPrinter) NewCounterTerminalOnly(_ string) restic.Counter { - return restic.NoopCounter -} - -func (*noopPrinter) E(_ string, _ ...interface{}) {} - -func (*noopPrinter) S(_ string, _ ...interface{}) {} - -func (*noopPrinter) PT(_ string, _ ...interface{}) {} - -func (*noopPrinter) P(_ string, _ ...interface{}) {} - -func (*noopPrinter) V(_ string, _ ...interface{}) {} - -func (*noopPrinter) VV(_ string, _ ...interface{}) {} diff --git a/internal/ui/progress/terminal.go b/internal/ui/progress/terminal.go index 13c188141..7f5428968 100644 --- a/internal/ui/progress/terminal.go +++ b/internal/ui/progress/terminal.go @@ -98,7 +98,7 @@ func (t *terminalPrinter) VV(msg string, args ...interface{}) { } } -func NewTerminalPrinter(json bool, verbosity uint, term ui.Terminal) Printer { +func NewTerminalPrinter(json bool, verbosity uint, term ui.Terminal) restic.Printer { if json { verbosity = 0 } diff --git a/internal/ui/restore/json.go b/internal/ui/restore/json.go index 86fc492d4..cc04700b4 100644 --- a/internal/ui/restore/json.go +++ b/internal/ui/restore/json.go @@ -3,12 +3,13 @@ package restore import ( "time" + "github.com/restic/restic/internal/restic" "github.com/restic/restic/internal/ui" "github.com/restic/restic/internal/ui/progress" ) type jsonPrinter struct { - progress.Printer + restic.Printer terminal ui.Terminal verbosity uint diff --git a/internal/ui/restore/progress.go b/internal/ui/restore/progress.go index 5a79d3825..e6a544a1b 100644 --- a/internal/ui/restore/progress.go +++ b/internal/ui/restore/progress.go @@ -4,6 +4,7 @@ import ( "sync" "time" + "github.com/restic/restic/internal/restic" "github.com/restic/restic/internal/ui/progress" ) @@ -38,7 +39,7 @@ type ProgressPrinter interface { Error(item string, err error) error CompleteItem(action ItemAction, item string, size uint64) Finish(progress State, duration time.Duration) - progress.Printer + restic.Printer } type ItemAction string diff --git a/internal/ui/restore/progress_test.go b/internal/ui/restore/progress_test.go index 407d2dfba..1259636ab 100644 --- a/internal/ui/restore/progress_test.go +++ b/internal/ui/restore/progress_test.go @@ -1,6 +1,7 @@ package restore import ( + "github.com/restic/restic/internal/restic" "testing" "time" @@ -37,7 +38,7 @@ type mockPrinter struct { trace printerTrace items itemTrace errors errorTrace - progress.Printer + restic.Printer } const mockFinishDuration = 42 * time.Second @@ -57,7 +58,7 @@ func (p *mockPrinter) Finish(progress State, _ time.Duration) { } func testProgress(fn func(progress *Progress) bool) (printerTrace, itemTrace, errorTrace) { - printer := &mockPrinter{Printer: progress.NewNoopPrinter()} + printer := &mockPrinter{Printer: restic.NewNoopPrinter()} progress := newProgress(printer, 0) final := fn(progress) progress.update(0, final) diff --git a/internal/ui/restore/text.go b/internal/ui/restore/text.go index 5eee78bfd..5830ce9b4 100644 --- a/internal/ui/restore/text.go +++ b/internal/ui/restore/text.go @@ -4,12 +4,13 @@ import ( "fmt" "time" + "github.com/restic/restic/internal/restic" "github.com/restic/restic/internal/ui" "github.com/restic/restic/internal/ui/progress" ) type textPrinter struct { - progress.Printer + restic.Printer terminal ui.Terminal } From 61708772a2df82d723fa0843bce061e0b5733071 Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Sun, 14 Jun 2026 11:17:29 +0200 Subject: [PATCH 4/6] mount: use existing HasPathPrefix helper replaces the custom isInside helper to check path nesting --- cmd/restic/cmd_mount.go | 23 +++++------------------ 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/cmd/restic/cmd_mount.go b/cmd/restic/cmd_mount.go index 010d1a7e0..2c9decc7c 100644 --- a/cmd/restic/cmd_mount.go +++ b/cmd/restic/cmd_mount.go @@ -19,6 +19,7 @@ import ( "github.com/restic/restic/internal/data" "github.com/restic/restic/internal/debug" "github.com/restic/restic/internal/errors" + "github.com/restic/restic/internal/fs" "github.com/restic/restic/internal/global" "github.com/restic/restic/internal/ui" "github.com/restic/restic/internal/ui/progress" @@ -26,7 +27,7 @@ import ( "github.com/restic/restic/internal/fuse" systemFuse "github.com/anacrolix/fuse" - "github.com/anacrolix/fuse/fs" + fusefs "github.com/anacrolix/fuse/fs" ) func registerMountCommand(cmdRoot *cobra.Command, globalOptions *global.Options) { @@ -197,7 +198,7 @@ func runMount(ctx context.Context, opts MountOptions, gopts global.Options, args go func() { defer close(done) - err = fs.Serve(c, root) + err = fusefs.Serve(c, root) }() printer.S("Now serving the repository at %s", mountpoint) @@ -271,9 +272,9 @@ func checkMountpointOverlap(repoPath, mountpoint string) error { switch { case rp == mp: return errors.Fatal(fmt.Sprintf("mountpoint %s is the local repository directory%s", mp, tail)) - case isInside(rp, mp): + case fs.HasPathPrefix(rp, mp): return errors.Fatal(fmt.Sprintf("mountpoint %s is inside the local repository directory %s%s", mp, rp, tail)) - case isInside(mp, rp): + case fs.HasPathPrefix(mp, rp): return errors.Fatal(fmt.Sprintf("local repository directory %s is inside the mountpoint %s%s", rp, mp, tail)) } return nil @@ -294,17 +295,3 @@ func resolvePath(p string) (string, error) { } return resolved, nil } - -// isInside reports whether child is strictly nested inside parent. Both paths -// must already be cleaned and absolute. Equal paths return false; the caller -// handles equality separately so it can produce a distinct error message. -func isInside(parent, child string) bool { - rel, err := filepath.Rel(parent, child) - if err != nil { - return false - } - if rel == "." || rel == ".." { - return false - } - return !strings.HasPrefix(rel, ".."+string(filepath.Separator)) -} From 5cd39d01c110b41cc5646787d480f4a811b72e9b Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Sun, 14 Jun 2026 13:19:36 +0200 Subject: [PATCH 5/6] internal/data: extract HardlinkIndex from restorer The HardlinkIndex is also used by `cmd_stats`, thus place it in a shared package. --- cmd/restic/cmd_stats.go | 5 ++--- internal/{restorer => data}/hardlinks_index.go | 2 +- internal/{restorer => data}/hardlinks_index_test.go | 6 +++--- internal/restorer/restorer.go | 2 +- internal/restorer/restorer_unix_test.go | 3 +-- internal/ui/restore/progress_test.go | 3 +-- 6 files changed, 9 insertions(+), 12 deletions(-) rename internal/{restorer => data}/hardlinks_index.go (98%) rename internal/{restorer => data}/hardlinks_index_test.go (84%) diff --git a/cmd/restic/cmd_stats.go b/cmd/restic/cmd_stats.go index 408579cf7..265c03c5f 100644 --- a/cmd/restic/cmd_stats.go +++ b/cmd/restic/cmd_stats.go @@ -13,7 +13,6 @@ import ( "github.com/restic/restic/internal/global" "github.com/restic/restic/internal/repository" "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" statsui "github.com/restic/restic/internal/ui/stats" @@ -228,7 +227,7 @@ func statsWalkSnapshot(ctx context.Context, snapshot *data.Snapshot, repo restic return data.FindUsedBlobs(ctx, repo, restic.IDs{*snapshot.Tree}, stats.blobs, restic.NoopCounter) } - hardLinkIndex := restorer.NewHardlinkIndex[struct{}]() + hardLinkIndex := data.NewHardlinkIndex[struct{}]() err := walker.Walk(ctx, repo, *snapshot.Tree, walker.WalkVisitor{ ProcessNode: statsWalkTree(repo, opts, stats, hardLinkIndex, sp), }) @@ -239,7 +238,7 @@ func statsWalkSnapshot(ctx context.Context, snapshot *data.Snapshot, repo restic return nil } -func statsWalkTree(repo restic.Loader, opts StatsOptions, stats *statsContainer, hardLinkIndex *restorer.HardlinkIndex[struct{}], progress *statsui.Progress) walker.WalkFunc { +func statsWalkTree(repo restic.Loader, opts StatsOptions, stats *statsContainer, hardLinkIndex *data.HardlinkIndex[struct{}], progress *statsui.Progress) walker.WalkFunc { return func(parentTreeID restic.ID, npath string, node *data.Node, nodeErr error) error { if nodeErr != nil { return nodeErr diff --git a/internal/restorer/hardlinks_index.go b/internal/data/hardlinks_index.go similarity index 98% rename from internal/restorer/hardlinks_index.go rename to internal/data/hardlinks_index.go index ec6c33a50..2cd344810 100644 --- a/internal/restorer/hardlinks_index.go +++ b/internal/data/hardlinks_index.go @@ -1,4 +1,4 @@ -package restorer +package data import ( "sync" diff --git a/internal/restorer/hardlinks_index_test.go b/internal/data/hardlinks_index_test.go similarity index 84% rename from internal/restorer/hardlinks_index_test.go rename to internal/data/hardlinks_index_test.go index 31ce938f9..4aa868c07 100644 --- a/internal/restorer/hardlinks_index_test.go +++ b/internal/data/hardlinks_index_test.go @@ -1,16 +1,16 @@ -package restorer_test +package data_test import ( "testing" - "github.com/restic/restic/internal/restorer" + "github.com/restic/restic/internal/data" rtest "github.com/restic/restic/internal/test" ) // TestHardLinks contains various tests for HardlinkIndex. func TestHardLinks(t *testing.T) { - idx := restorer.NewHardlinkIndex[string]() + idx := data.NewHardlinkIndex[string]() idx.Add(1, 2, "inode1-file1-on-device2") idx.Add(2, 3, "inode2-file2-on-device3") diff --git a/internal/restorer/restorer.go b/internal/restorer/restorer.go index 8741522e4..9f2a13658 100644 --- a/internal/restorer/restorer.go +++ b/internal/restorer/restorer.go @@ -360,7 +360,7 @@ func (res *Restorer) RestoreTo(ctx context.Context, dst string) (uint64, error) } } - idx := NewHardlinkIndex[string]() + idx := data.NewHardlinkIndex[string]() filerestorer := newFileRestorer(dst, res.repo.LoadBlobsFromPack, res.repo.LookupBlob, res.repo.Connections(), res.opts.Sparse, res.opts.Delete, res.repo.StartWarmup, res.opts.Progress) filerestorer.Error = res.Error diff --git a/internal/restorer/restorer_unix_test.go b/internal/restorer/restorer_unix_test.go index 7b94ee212..bb31b8bf0 100644 --- a/internal/restorer/restorer_unix_test.go +++ b/internal/restorer/restorer_unix_test.go @@ -4,7 +4,6 @@ package restorer import ( "context" - "github.com/restic/restic/internal/restic" "io/fs" "os" "path/filepath" @@ -13,8 +12,8 @@ import ( "time" "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/progress" restoreui "github.com/restic/restic/internal/ui/restore" ) diff --git a/internal/ui/restore/progress_test.go b/internal/ui/restore/progress_test.go index 1259636ab..3107a72e3 100644 --- a/internal/ui/restore/progress_test.go +++ b/internal/ui/restore/progress_test.go @@ -1,13 +1,12 @@ package restore import ( - "github.com/restic/restic/internal/restic" "testing" "time" "github.com/restic/restic/internal/errors" + "github.com/restic/restic/internal/restic" "github.com/restic/restic/internal/test" - "github.com/restic/restic/internal/ui/progress" ) type printerTraceEntry struct { From 2f31cde5170da58e4bf5905f4454fdc137dff384 Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Sun, 14 Jun 2026 14:11:00 +0200 Subject: [PATCH 6/6] internal/restorer: invert dependency direction to ui/restorer The restorer imported ui/restorer which leaks ui logic into the restorer core. Swap the direction by letting the restorer use a ProgressReporter interface + associated constants. --- internal/restorer/filerestorer.go | 11 ++--- internal/restorer/progress.go | 38 +++++++++++++++ internal/restorer/progress_mock_test.go | 63 +++++++++++++++++++++++++ internal/restorer/restorer.go | 14 +++--- internal/restorer/restorer_test.go | 41 ++++------------ internal/restorer/restorer_unix_test.go | 10 ++-- internal/ui/restore/json.go | 15 +++--- internal/ui/restore/json_test.go | 13 ++--- internal/ui/restore/progress.go | 23 ++++----- internal/ui/restore/progress_test.go | 45 +++++++++--------- internal/ui/restore/text.go | 17 +++---- internal/ui/restore/text_test.go | 15 +++--- 12 files changed, 189 insertions(+), 116 deletions(-) create mode 100644 internal/restorer/progress.go create mode 100644 internal/restorer/progress_mock_test.go diff --git a/internal/restorer/filerestorer.go b/internal/restorer/filerestorer.go index 7bb849ad5..9bbe8fa39 100644 --- a/internal/restorer/filerestorer.go +++ b/internal/restorer/filerestorer.go @@ -13,7 +13,6 @@ import ( "github.com/restic/restic/internal/feature" "github.com/restic/restic/internal/repository" "github.com/restic/restic/internal/restic" - "github.com/restic/restic/internal/ui/restore" ) const ( @@ -56,7 +55,7 @@ type fileRestorer struct { filesWriter *filesWriter zeroChunk restic.ID sparse bool - progress *restore.Progress + progress ProgressReporter allowRecursiveDelete bool @@ -73,7 +72,7 @@ func newFileRestorer(dst string, sparse bool, allowRecursiveDelete bool, startWarmup startWarmupFn, - progress *restore.Progress) *fileRestorer { + progress ProgressReporter) *fileRestorer { // as packs are streamed the concurrency is limited by IO workerCount := int(connections) @@ -85,7 +84,7 @@ func newFileRestorer(dst string, filesWriter: newFilesWriter(workerCount, allowRecursiveDelete), zeroChunk: repository.ZeroChunk(), sparse: sparse, - progress: progress, + progress: progressOrNoop(progress), allowRecursiveDelete: allowRecursiveDelete, workerCount: workerCount, dst: dst, @@ -398,9 +397,9 @@ func (r *fileRestorer) downloadBlobs(ctx context.Context, packID restic.ID, } func (r *fileRestorer) reportBlobProgress(file *fileInfo, blobSize uint64) { - action := restore.ActionFileUpdated + action := ActionFileUpdated if file.state == nil { - action = restore.ActionFileRestored + action = ActionFileRestored } r.progress.AddProgress(file.location, action, blobSize, uint64(file.size)) } diff --git a/internal/restorer/progress.go b/internal/restorer/progress.go new file mode 100644 index 000000000..a94583a12 --- /dev/null +++ b/internal/restorer/progress.go @@ -0,0 +1,38 @@ +package restorer + +type ItemAction string + +// Constants for the different CompleteItem actions. +const ( + ActionDirRestored ItemAction = "dir restored" + ActionFileRestored ItemAction = "file restored" + ActionFileUpdated ItemAction = "file updated" + ActionFileUnchanged ItemAction = "file unchanged" + ActionOtherRestored ItemAction = "other restored" + ActionDeleted ItemAction = "deleted" +) + +// ProgressReporter reports restore progress. +type ProgressReporter interface { + AddFile(size uint64) + AddProgress(name string, action ItemAction, bytesWrittenPortion, bytesTotal uint64) + AddSkippedFile(name string, size uint64) + ReportDeletion(name string) +} + +type noopProgressReporter struct{} + +var _ ProgressReporter = (*noopProgressReporter)(nil) + +func (noopProgressReporter) AddFile(uint64) {} +func (noopProgressReporter) AddProgress(string, ItemAction, uint64, uint64) { +} +func (noopProgressReporter) AddSkippedFile(string, uint64) {} +func (noopProgressReporter) ReportDeletion(string) {} + +func progressOrNoop(p ProgressReporter) ProgressReporter { + if p == nil { + return noopProgressReporter{} + } + return p +} diff --git a/internal/restorer/progress_mock_test.go b/internal/restorer/progress_mock_test.go new file mode 100644 index 000000000..4d51a0d2d --- /dev/null +++ b/internal/restorer/progress_mock_test.go @@ -0,0 +1,63 @@ +package restorer + +type progressInfoEntry struct { + bytesWritten uint64 + bytesTotal uint64 +} + +// progressState mirrors the state used by the restorer ui. +type progressState struct { + FilesFinished uint64 + FilesTotal uint64 + FilesSkipped uint64 + FilesDeleted uint64 + AllBytesWritten uint64 + AllBytesTotal uint64 + AllBytesSkipped uint64 +} + +type testProgress struct { + progressInfoMap map[string]progressInfoEntry + s progressState +} + +var _ ProgressReporter = (*testProgress)(nil) + +func newTestProgress() *testProgress { + return &testProgress{ + progressInfoMap: make(map[string]progressInfoEntry), + } +} + +func (p *testProgress) AddFile(size uint64) { + p.s.FilesTotal++ + p.s.AllBytesTotal += size +} + +func (p *testProgress) AddProgress(name string, _ ItemAction, bytesWrittenPortion, bytesTotal uint64) { + entry, exists := p.progressInfoMap[name] + if !exists { + entry.bytesTotal = bytesTotal + } + entry.bytesWritten += bytesWrittenPortion + p.progressInfoMap[name] = entry + + p.s.AllBytesWritten += bytesWrittenPortion + if entry.bytesWritten == entry.bytesTotal { + delete(p.progressInfoMap, name) + p.s.FilesFinished++ + } +} + +func (p *testProgress) AddSkippedFile(_ string, size uint64) { + p.s.FilesSkipped++ + p.s.AllBytesSkipped += size +} + +func (p *testProgress) ReportDeletion(_ string) { + p.s.FilesDeleted++ +} + +func (p *testProgress) state() progressState { + return p.s +} diff --git a/internal/restorer/restorer.go b/internal/restorer/restorer.go index 9f2a13658..51555a6b2 100644 --- a/internal/restorer/restorer.go +++ b/internal/restorer/restorer.go @@ -13,7 +13,6 @@ import ( "github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/fs" "github.com/restic/restic/internal/restic" - restoreui "github.com/restic/restic/internal/ui/restore" "golang.org/x/sync/errgroup" ) @@ -41,7 +40,7 @@ var restorerAbortOnAllErrors = func(_ string, err error) error { return err } type Options struct { DryRun bool Sparse bool - Progress *restoreui.Progress + Progress ProgressReporter Overwrite OverwriteBehavior Delete bool OwnershipByName bool @@ -100,6 +99,7 @@ func (c *OverwriteBehavior) Type() string { // NewRestorer creates a restorer preloaded with the content from the snapshot id. func NewRestorer(repo restic.Repository, sn *data.Snapshot, opts Options) *Restorer { + opts.Progress = progressOrNoop(opts.Progress) r := &Restorer{ repo: repo, opts: opts, @@ -287,7 +287,7 @@ func (res *Restorer) restoreNodeTo(node *data.Node, target, location string) err } } - res.opts.Progress.AddProgress(location, restoreui.ActionOtherRestored, 0, 0) + res.opts.Progress.AddProgress(location, ActionOtherRestored, 0, 0) return res.restoreNodeMetadataTo(node, target, location) } @@ -314,7 +314,7 @@ func (res *Restorer) restoreHardlinkAt(node *data.Node, target, path, location s } } - res.opts.Progress.AddProgress(location, restoreui.ActionOtherRestored, 0, 0) + res.opts.Progress.AddProgress(location, ActionOtherRestored, 0, 0) // TODO investigate if hardlinks have separate metadata on any supported system return res.restoreNodeMetadataTo(node, path, location) } @@ -408,9 +408,9 @@ func (res *Restorer) RestoreTo(ctx context.Context, dst string) (uint64, error) if !res.opts.DryRun { filerestorer.addFile(location, node.Content, int64(node.Size), matches) } else { - action := restoreui.ActionFileUpdated + action := ActionFileUpdated if matches == nil { - action = restoreui.ActionFileRestored + action = ActionFileRestored } // immediately mark as completed res.opts.Progress.AddProgress(location, action, node.Size, node.Size) @@ -475,7 +475,7 @@ func (res *Restorer) RestoreTo(ctx context.Context, dst string) (uint64, error) err := res.restoreNodeMetadataTo(node, target, location) if err == nil { - res.opts.Progress.AddProgress(location, restoreui.ActionDirRestored, 0, 0) + res.opts.Progress.AddProgress(location, ActionDirRestored, 0, 0) } return err }, diff --git a/internal/restorer/restorer_test.go b/internal/restorer/restorer_test.go index 80fe0f631..225c70aa5 100644 --- a/internal/restorer/restorer_test.go +++ b/internal/restorer/restorer_test.go @@ -24,7 +24,6 @@ import ( "github.com/restic/restic/internal/restic" rtest "github.com/restic/restic/internal/test" "github.com/restic/restic/internal/ui/progress" - restoreui "github.com/restic/restic/internal/ui/restore" ) type Node interface{} @@ -987,22 +986,6 @@ func TestRestorerSparseOverwrite(t *testing.T) { saveSnapshotsAndOverwrite(t, baseSnapshot, sparseSnapshot, opts, opts) } -type printerMock struct { - s restoreui.State - restic.Printer -} - -func (p *printerMock) Update(_ restoreui.State, _ time.Duration) { -} -func (p *printerMock) Error(_ string, _ error) error { - return nil -} -func (p *printerMock) CompleteItem(_ restoreui.ItemAction, _ string, _ uint64) { -} -func (p *printerMock) Finish(s restoreui.State, _ time.Duration) { - p.s = s -} - func TestRestorerOverwriteBehavior(t *testing.T) { baseTime := time.Now() baseSnapshot := Snapshot{ @@ -1032,7 +1015,7 @@ func TestRestorerOverwriteBehavior(t *testing.T) { var tests = []struct { Overwrite OverwriteBehavior Files map[string]string - Progress restoreui.State + Progress progressState }{ { Overwrite: OverwriteAlways, @@ -1041,7 +1024,7 @@ func TestRestorerOverwriteBehavior(t *testing.T) { "dirtest/file": "content: file2\n", "dirtest/foo": "content: foo", }, - Progress: restoreui.State{ + Progress: progressState{ FilesFinished: 4, FilesTotal: 4, FilesSkipped: 0, @@ -1057,7 +1040,7 @@ func TestRestorerOverwriteBehavior(t *testing.T) { "dirtest/file": "content: file2\n", "dirtest/foo": "content: foo", }, - Progress: restoreui.State{ + Progress: progressState{ FilesFinished: 4, FilesTotal: 4, FilesSkipped: 0, @@ -1073,7 +1056,7 @@ func TestRestorerOverwriteBehavior(t *testing.T) { "dirtest/file": "content: file\n", "dirtest/foo": "content: foobar", }, - Progress: restoreui.State{ + Progress: progressState{ FilesFinished: 2, FilesTotal: 2, FilesSkipped: 2, @@ -1089,7 +1072,7 @@ func TestRestorerOverwriteBehavior(t *testing.T) { "dirtest/file": "content: file\n", "dirtest/foo": "content: foobar", }, - Progress: restoreui.State{ + Progress: progressState{ FilesFinished: 1, FilesTotal: 1, FilesSkipped: 3, @@ -1102,8 +1085,7 @@ func TestRestorerOverwriteBehavior(t *testing.T) { for _, test := range tests { t.Run("", func(t *testing.T) { - mock := &printerMock{Printer: restic.NewNoopPrinter()} - progress := restoreui.NewProgress(mock, true, false, true) + progress := newTestProgress() tempdir := saveSnapshotsAndOverwrite(t, baseSnapshot, overwriteSnapshot, Options{}, Options{Overwrite: test.Overwrite, Progress: progress}) for filename, content := range test.Files { @@ -1118,8 +1100,7 @@ func TestRestorerOverwriteBehavior(t *testing.T) { } } - progress.Finish() - rtest.Equals(t, test.Progress, mock.s) + rtest.Equals(t, test.Progress, progress.state()) }) } } @@ -1154,18 +1135,16 @@ func TestRestorerOverwritePartial(t *testing.T) { }, } - mock := &printerMock{Printer: restic.NewNoopPrinter()} - progress := restoreui.NewProgress(mock, true, false, true) + progress := newTestProgress() saveSnapshotsAndOverwrite(t, baseSnapshot, overwriteSnapshot, Options{}, Options{Overwrite: OverwriteAlways, Progress: progress}) - progress.Finish() - rtest.Equals(t, restoreui.State{ + rtest.Equals(t, progressState{ FilesFinished: 2, FilesTotal: 2, FilesSkipped: 0, AllBytesWritten: uint64(size), AllBytesTotal: uint64(size), AllBytesSkipped: 0, - }, mock.s) + }, progress.state()) } func TestRestorerOverwriteSpecial(t *testing.T) { diff --git a/internal/restorer/restorer_unix_test.go b/internal/restorer/restorer_unix_test.go index bb31b8bf0..6b1388af4 100644 --- a/internal/restorer/restorer_unix_test.go +++ b/internal/restorer/restorer_unix_test.go @@ -12,9 +12,7 @@ import ( "time" "github.com/restic/restic/internal/repository" - "github.com/restic/restic/internal/restic" rtest "github.com/restic/restic/internal/test" - restoreui "github.com/restic/restic/internal/ui/restore" ) func TestRestorerRestoreEmptyHardlinkedFields(t *testing.T) { @@ -88,8 +86,7 @@ func testRestorerProgressBar(t *testing.T, dryRun bool) { }, }, noopGetGenericAttributes) - mock := &printerMock{Printer: restic.NewNoopPrinter()} - progress := restoreui.NewProgress(mock, true, false, true) + progress := newTestProgress() res := NewRestorer(repo, sn, Options{Progress: progress, DryRun: dryRun}) tempdir := rtest.TempDir(t) @@ -98,16 +95,15 @@ func testRestorerProgressBar(t *testing.T, dryRun bool) { _, err := res.RestoreTo(ctx, tempdir) rtest.OK(t, err) - progress.Finish() - rtest.Equals(t, restoreui.State{ + rtest.Equals(t, progressState{ FilesFinished: 4, FilesTotal: 4, FilesSkipped: 0, AllBytesWritten: 10, AllBytesTotal: 10, AllBytesSkipped: 0, - }, mock.s) + }, progress.state()) } func TestRestorePermissions(t *testing.T) { diff --git a/internal/ui/restore/json.go b/internal/ui/restore/json.go index cc04700b4..4ce66fcbf 100644 --- a/internal/ui/restore/json.go +++ b/internal/ui/restore/json.go @@ -4,6 +4,7 @@ import ( "time" "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" ) @@ -61,24 +62,24 @@ func (t *jsonPrinter) Error(item string, err error) error { return nil } -func (t *jsonPrinter) CompleteItem(messageType ItemAction, item string, size uint64) { +func (t *jsonPrinter) CompleteItem(messageType restorer.ItemAction, item string, size uint64) { if t.verbosity < 3 { return } var action string switch messageType { - case ActionDirRestored: + case restorer.ActionDirRestored: action = "restored" - case ActionFileRestored: + case restorer.ActionFileRestored: action = "restored" - case ActionOtherRestored: + case restorer.ActionOtherRestored: action = "restored" - case ActionFileUpdated: + case restorer.ActionFileUpdated: action = "updated" - case ActionFileUnchanged: + case restorer.ActionFileUnchanged: action = "unchanged" - case ActionDeleted: + case restorer.ActionDeleted: action = "deleted" default: panic("unknown message type") diff --git a/internal/ui/restore/json_test.go b/internal/ui/restore/json_test.go index c7096c246..0db8e91b1 100644 --- a/internal/ui/restore/json_test.go +++ b/internal/ui/restore/json_test.go @@ -5,6 +5,7 @@ import ( "time" "github.com/restic/restic/internal/errors" + "github.com/restic/restic/internal/restorer" "github.com/restic/restic/internal/test" "github.com/restic/restic/internal/ui" ) @@ -47,15 +48,15 @@ func TestJSONPrintSummaryOnSuccessWithSkipped(t *testing.T) { func TestJSONPrintCompleteItem(t *testing.T) { for _, data := range []struct { - action ItemAction + action restorer.ItemAction size uint64 expected string }{ - {ActionDirRestored, 0, "{\"message_type\":\"verbose_status\",\"action\":\"restored\",\"item\":\"test\",\"size\":0}\n"}, - {ActionFileRestored, 123, "{\"message_type\":\"verbose_status\",\"action\":\"restored\",\"item\":\"test\",\"size\":123}\n"}, - {ActionFileUpdated, 123, "{\"message_type\":\"verbose_status\",\"action\":\"updated\",\"item\":\"test\",\"size\":123}\n"}, - {ActionFileUnchanged, 123, "{\"message_type\":\"verbose_status\",\"action\":\"unchanged\",\"item\":\"test\",\"size\":123}\n"}, - {ActionDeleted, 0, "{\"message_type\":\"verbose_status\",\"action\":\"deleted\",\"item\":\"test\",\"size\":0}\n"}, + {restorer.ActionDirRestored, 0, "{\"message_type\":\"verbose_status\",\"action\":\"restored\",\"item\":\"test\",\"size\":0}\n"}, + {restorer.ActionFileRestored, 123, "{\"message_type\":\"verbose_status\",\"action\":\"restored\",\"item\":\"test\",\"size\":123}\n"}, + {restorer.ActionFileUpdated, 123, "{\"message_type\":\"verbose_status\",\"action\":\"updated\",\"item\":\"test\",\"size\":123}\n"}, + {restorer.ActionFileUnchanged, 123, "{\"message_type\":\"verbose_status\",\"action\":\"unchanged\",\"item\":\"test\",\"size\":123}\n"}, + {restorer.ActionDeleted, 0, "{\"message_type\":\"verbose_status\",\"action\":\"deleted\",\"item\":\"test\",\"size\":0}\n"}, } { term, printer := createJSONProgress() printer.CompleteItem(data.action, "test", data.size) diff --git a/internal/ui/restore/progress.go b/internal/ui/restore/progress.go index e6a544a1b..1ba4a6076 100644 --- a/internal/ui/restore/progress.go +++ b/internal/ui/restore/progress.go @@ -5,6 +5,7 @@ import ( "time" "github.com/restic/restic/internal/restic" + "github.com/restic/restic/internal/restorer" "github.com/restic/restic/internal/ui/progress" ) @@ -29,6 +30,8 @@ type Progress struct { printer ProgressPrinter } +var _ restorer.ProgressReporter = (*Progress)(nil) + type progressInfoEntry struct { bytesWritten uint64 bytesTotal uint64 @@ -37,22 +40,12 @@ type progressInfoEntry struct { type ProgressPrinter interface { Update(progress State, duration time.Duration) Error(item string, err error) error - CompleteItem(action ItemAction, item string, size uint64) + CompleteItem(action restorer.ItemAction, item string, size uint64) Finish(progress State, duration time.Duration) restic.Printer } -type ItemAction string - -// Constants for the different CompleteItem actions. -const ( - ActionDirRestored ItemAction = "dir restored" - ActionFileRestored ItemAction = "file restored" - ActionFileUpdated ItemAction = "file updated" - ActionFileUnchanged ItemAction = "file unchanged" - ActionOtherRestored ItemAction = "other restored" - ActionDeleted ItemAction = "deleted" -) +var _ restorer.ProgressReporter = (*Progress)(nil) func NewProgress(printer ProgressPrinter, quiet, json, canUpdateStatus bool) *Progress { return newProgress(printer, progress.CalculateProgressInterval(!quiet, json, canUpdateStatus)) @@ -93,7 +86,7 @@ func (p *Progress) AddFile(size uint64) { } // AddProgress accumulates the number of bytes written for a file -func (p *Progress) AddProgress(name string, action ItemAction, bytesWrittenPortion uint64, bytesTotal uint64) { +func (p *Progress) AddProgress(name string, action restorer.ItemAction, bytesWrittenPortion uint64, bytesTotal uint64) { if p == nil { return } @@ -128,7 +121,7 @@ func (p *Progress) AddSkippedFile(name string, size uint64) { p.s.FilesSkipped++ p.s.AllBytesSkipped += size - p.printer.CompleteItem(ActionFileUnchanged, name, size) + p.printer.CompleteItem(restorer.ActionFileUnchanged, name, size) } func (p *Progress) ReportDeletion(name string) { @@ -141,7 +134,7 @@ func (p *Progress) ReportDeletion(name string) { p.s.FilesDeleted++ - p.printer.CompleteItem(ActionDeleted, name, 0) + p.printer.CompleteItem(restorer.ActionDeleted, name, 0) } func (p *Progress) Error(item string, err error) error { diff --git a/internal/ui/restore/progress_test.go b/internal/ui/restore/progress_test.go index 3107a72e3..fced2e993 100644 --- a/internal/ui/restore/progress_test.go +++ b/internal/ui/restore/progress_test.go @@ -6,6 +6,7 @@ import ( "github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/restic" + "github.com/restic/restic/internal/restorer" "github.com/restic/restic/internal/test" ) @@ -19,7 +20,7 @@ type printerTraceEntry struct { type printerTrace []printerTraceEntry type itemTraceEntry struct { - action ItemAction + action restorer.ItemAction item string size uint64 } @@ -49,7 +50,7 @@ func (p *mockPrinter) Error(item string, err error) error { p.errors = append(p.errors, errorTraceEntry{item, err}) return nil } -func (p *mockPrinter) CompleteItem(action ItemAction, item string, size uint64) { +func (p *mockPrinter) CompleteItem(action restorer.ItemAction, item string, size uint64) { p.items = append(p.items, itemTraceEntry{action, item, size}) } func (p *mockPrinter) Finish(progress State, _ time.Duration) { @@ -98,7 +99,7 @@ func TestFirstProgressOnAFile(t *testing.T) { result, items, _ := testProgress(func(progress *Progress) bool { progress.AddFile(expectedBytesTotal) - progress.AddProgress("test", ActionFileUpdated, expectedBytesWritten, expectedBytesTotal) + progress.AddProgress("test", restorer.ActionFileUpdated, expectedBytesWritten, expectedBytesTotal) return false }) test.Equals(t, printerTrace{ @@ -112,16 +113,16 @@ func TestLastProgressOnAFile(t *testing.T) { result, items, _ := testProgress(func(progress *Progress) bool { progress.AddFile(fileSize) - progress.AddProgress("test", ActionFileUpdated, 30, fileSize) - progress.AddProgress("test", ActionFileUpdated, 35, fileSize) - progress.AddProgress("test", ActionFileUpdated, 35, fileSize) + progress.AddProgress("test", restorer.ActionFileUpdated, 30, fileSize) + progress.AddProgress("test", restorer.ActionFileUpdated, 35, fileSize) + progress.AddProgress("test", restorer.ActionFileUpdated, 35, fileSize) return false }) test.Equals(t, printerTrace{ printerTraceEntry{State{1, 1, 0, 0, fileSize, fileSize, 0}, 0, false}, }, result) test.Equals(t, itemTrace{ - itemTraceEntry{action: ActionFileUpdated, item: "test", size: fileSize}, + itemTraceEntry{action: restorer.ActionFileUpdated, item: "test", size: fileSize}, }, items) } @@ -131,17 +132,17 @@ func TestLastProgressOnLastFile(t *testing.T) { result, items, _ := testProgress(func(progress *Progress) bool { progress.AddFile(fileSize) progress.AddFile(50) - progress.AddProgress("test1", ActionFileUpdated, 50, 50) - progress.AddProgress("test2", ActionFileUpdated, 50, fileSize) - progress.AddProgress("test2", ActionFileUpdated, 50, fileSize) + progress.AddProgress("test1", restorer.ActionFileUpdated, 50, 50) + progress.AddProgress("test2", restorer.ActionFileUpdated, 50, fileSize) + progress.AddProgress("test2", restorer.ActionFileUpdated, 50, fileSize) return false }) test.Equals(t, printerTrace{ printerTraceEntry{State{2, 2, 0, 0, 50 + fileSize, 50 + fileSize, 0}, 0, false}, }, result) test.Equals(t, itemTrace{ - itemTraceEntry{action: ActionFileUpdated, item: "test1", size: 50}, - itemTraceEntry{action: ActionFileUpdated, item: "test2", size: fileSize}, + itemTraceEntry{action: restorer.ActionFileUpdated, item: "test1", size: 50}, + itemTraceEntry{action: restorer.ActionFileUpdated, item: "test2", size: fileSize}, }, items) } @@ -151,8 +152,8 @@ func TestSummaryOnSuccess(t *testing.T) { result, _, _ := testProgress(func(progress *Progress) bool { progress.AddFile(fileSize) progress.AddFile(50) - progress.AddProgress("test1", ActionFileUpdated, 50, 50) - progress.AddProgress("test2", ActionFileUpdated, fileSize, fileSize) + progress.AddProgress("test1", restorer.ActionFileUpdated, 50, 50) + progress.AddProgress("test2", restorer.ActionFileUpdated, fileSize, fileSize) return true }) test.Equals(t, printerTrace{ @@ -166,8 +167,8 @@ func TestSummaryOnErrors(t *testing.T) { result, _, _ := testProgress(func(progress *Progress) bool { progress.AddFile(fileSize) progress.AddFile(50) - progress.AddProgress("test1", ActionFileUpdated, 50, 50) - progress.AddProgress("test2", ActionFileUpdated, fileSize/2, fileSize) + progress.AddProgress("test1", restorer.ActionFileUpdated, 50, 50) + progress.AddProgress("test2", restorer.ActionFileUpdated, fileSize/2, fileSize) return true }) test.Equals(t, printerTrace{ @@ -186,7 +187,7 @@ func TestSkipFile(t *testing.T) { printerTraceEntry{State{0, 0, 1, 0, 0, 0, fileSize}, mockFinishDuration, true}, }, result) test.Equals(t, itemTrace{ - itemTraceEntry{ActionFileUnchanged, "test", fileSize}, + itemTraceEntry{restorer.ActionFileUnchanged, "test", fileSize}, }, items) } @@ -196,15 +197,15 @@ func TestProgressTypes(t *testing.T) { _, items, _ := testProgress(func(progress *Progress) bool { progress.AddFile(fileSize) progress.AddFile(0) - progress.AddProgress("dir", ActionDirRestored, fileSize, fileSize) - progress.AddProgress("new", ActionFileRestored, 0, 0) + progress.AddProgress("dir", restorer.ActionDirRestored, fileSize, fileSize) + progress.AddProgress("new", restorer.ActionFileRestored, 0, 0) progress.ReportDeletion("del") return true }) test.Equals(t, itemTrace{ - itemTraceEntry{ActionDirRestored, "dir", fileSize}, - itemTraceEntry{ActionFileRestored, "new", 0}, - itemTraceEntry{ActionDeleted, "del", 0}, + itemTraceEntry{restorer.ActionDirRestored, "dir", fileSize}, + itemTraceEntry{restorer.ActionFileRestored, "new", 0}, + itemTraceEntry{restorer.ActionDeleted, "del", 0}, }, items) } diff --git a/internal/ui/restore/text.go b/internal/ui/restore/text.go index 5830ce9b4..da0fd0319 100644 --- a/internal/ui/restore/text.go +++ b/internal/ui/restore/text.go @@ -5,6 +5,7 @@ import ( "time" "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" ) @@ -44,26 +45,26 @@ func (t *textPrinter) Error(item string, err error) error { return nil } -func (t *textPrinter) CompleteItem(messageType ItemAction, item string, size uint64) { +func (t *textPrinter) CompleteItem(messageType restorer.ItemAction, item string, size uint64) { var action string switch messageType { - case ActionDirRestored: + case restorer.ActionDirRestored: action = "restored" - case ActionFileRestored: + case restorer.ActionFileRestored: action = "restored" - case ActionOtherRestored: + case restorer.ActionOtherRestored: action = "restored" - case ActionFileUpdated: + case restorer.ActionFileUpdated: action = "updated" - case ActionFileUnchanged: + case restorer.ActionFileUnchanged: action = "unchanged" - case ActionDeleted: + case restorer.ActionDeleted: action = "deleted" default: panic("unknown message type") } - if messageType == ActionDirRestored || messageType == ActionOtherRestored || messageType == ActionDeleted { + if messageType == restorer.ActionDirRestored || messageType == restorer.ActionOtherRestored || messageType == restorer.ActionDeleted { t.VV("%-9v %v", action, item) } else { t.VV("%-9v %v with size %v", action, item, ui.FormatBytes(size)) diff --git a/internal/ui/restore/text_test.go b/internal/ui/restore/text_test.go index 746700cd8..9f1790152 100644 --- a/internal/ui/restore/text_test.go +++ b/internal/ui/restore/text_test.go @@ -5,6 +5,7 @@ import ( "time" "github.com/restic/restic/internal/errors" + "github.com/restic/restic/internal/restorer" "github.com/restic/restic/internal/test" "github.com/restic/restic/internal/ui" ) @@ -47,16 +48,16 @@ func TestPrintSummaryOnSuccessWithSkipped(t *testing.T) { func TestPrintCompleteItem(t *testing.T) { for _, data := range []struct { - action ItemAction + action restorer.ItemAction size uint64 expected string }{ - {ActionDirRestored, 0, "restored test"}, - {ActionFileRestored, 123, "restored test with size 123 B"}, - {ActionOtherRestored, 0, "restored test"}, - {ActionFileUpdated, 123, "updated test with size 123 B"}, - {ActionFileUnchanged, 123, "unchanged test with size 123 B"}, - {ActionDeleted, 0, "deleted test"}, + {restorer.ActionDirRestored, 0, "restored test"}, + {restorer.ActionFileRestored, 123, "restored test with size 123 B"}, + {restorer.ActionOtherRestored, 0, "restored test"}, + {restorer.ActionFileUpdated, 123, "updated test with size 123 B"}, + {restorer.ActionFileUnchanged, 123, "unchanged test with size 123 B"}, + {restorer.ActionDeleted, 0, "deleted test"}, } { term, printer := createTextProgress() printer.CompleteItem(data.action, "test", data.size)