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.
This commit is contained in:
Michael Eischer
2026-06-20 17:49:20 +02:00
parent 5cd39d01c1
commit 2f31cde517
12 changed files with 189 additions and 116 deletions
+5 -6
View File
@@ -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))
}
+38
View File
@@ -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
}
+63
View File
@@ -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
}
+7 -7
View File
@@ -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
},
+10 -31
View File
@@ -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) {
+3 -7
View File
@@ -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) {