diff --git a/changelog/unreleased/issue-5092 b/changelog/unreleased/issue-5092 new file mode 100644 index 000000000..b6a32b68b --- /dev/null +++ b/changelog/unreleased/issue-5092 @@ -0,0 +1,8 @@ +Enhancement: Indicate the of deleted files/directories during restore + +Restic now indicates the number of deleted files/directories during restore. +The `--json` output now includes a `files_deleted` field that shows the number +of files and directories that were deleted during restore. + +https://github.com/restic/restic/issues/5092 +https://github.com/restic/restic/pull/5100 diff --git a/doc/075_scripting.rst b/doc/075_scripting.rst index 88fc1f35b..39a6dbc7f 100644 --- a/doc/075_scripting.rst +++ b/doc/075_scripting.rst @@ -563,6 +563,8 @@ Status +----------------------+------------------------------------------------------------+ |``files_skipped`` | Files skipped due to overwrite setting | +----------------------+------------------------------------------------------------+ +|``files_deleted`` | Files deleted | ++----------------------+------------------------------------------------------------+ |``total_bytes`` | Total number of bytes in restore set | +----------------------+------------------------------------------------------------+ |``bytes_restored`` | Number of bytes restored | @@ -615,6 +617,8 @@ Summary +----------------------+------------------------------------------------------------+ |``files_skipped`` | Files skipped due to overwrite setting | +----------------------+------------------------------------------------------------+ +|``files_deleted`` | Files deleted | ++----------------------+------------------------------------------------------------+ |``total_bytes`` | Total number of bytes in restore set | +----------------------+------------------------------------------------------------+ |``bytes_restored`` | Number of bytes restored | diff --git a/internal/restorer/restorer.go b/internal/restorer/restorer.go index f28cd0ba3..14a8edeac 100644 --- a/internal/restorer/restorer.go +++ b/internal/restorer/restorer.go @@ -511,12 +511,30 @@ func (res *Restorer) removeUnexpectedFiles(ctx context.Context, target, location selectedForRestore, _ := res.SelectFilter(nodeLocation, false) // only delete files that were selected for restore if selectedForRestore { - res.opts.Progress.ReportDeletedFile(nodeLocation) + // First collect all files that will be deleted + var filesToDelete []string + err := filepath.Walk(nodeTarget, func(path string, _ os.FileInfo, err error) error { + if err != nil { + return err + } + filesToDelete = append(filesToDelete, path) + return nil + }) + if err != nil { + return err + } + if !res.opts.DryRun { + // Perform the deletion if err := fs.RemoveAll(nodeTarget); err != nil { return err } } + + // Report paths as deleted only after successful removal + for i := len(filesToDelete) - 1; i >= 0; i-- { + res.opts.Progress.ReportDeletion(filesToDelete[i]) + } } } diff --git a/internal/ui/restore/json.go b/internal/ui/restore/json.go index 72cc38a6e..f7f7bdd1f 100644 --- a/internal/ui/restore/json.go +++ b/internal/ui/restore/json.go @@ -33,6 +33,7 @@ func (t *jsonPrinter) Update(p State, duration time.Duration) { TotalFiles: p.FilesTotal, FilesRestored: p.FilesFinished, FilesSkipped: p.FilesSkipped, + FilesDeleted: p.FilesDeleted, TotalBytes: p.AllBytesTotal, BytesRestored: p.AllBytesWritten, BytesSkipped: p.AllBytesSkipped, @@ -94,6 +95,7 @@ func (t *jsonPrinter) Finish(p State, duration time.Duration) { TotalFiles: p.FilesTotal, FilesRestored: p.FilesFinished, FilesSkipped: p.FilesSkipped, + FilesDeleted: p.FilesDeleted, TotalBytes: p.AllBytesTotal, BytesRestored: p.AllBytesWritten, BytesSkipped: p.AllBytesSkipped, @@ -108,6 +110,7 @@ type statusUpdate struct { TotalFiles uint64 `json:"total_files,omitempty"` FilesRestored uint64 `json:"files_restored,omitempty"` FilesSkipped uint64 `json:"files_skipped,omitempty"` + FilesDeleted uint64 `json:"files_deleted,omitempty"` TotalBytes uint64 `json:"total_bytes,omitempty"` BytesRestored uint64 `json:"bytes_restored,omitempty"` BytesSkipped uint64 `json:"bytes_skipped,omitempty"` @@ -137,6 +140,7 @@ type summaryOutput struct { TotalFiles uint64 `json:"total_files,omitempty"` FilesRestored uint64 `json:"files_restored,omitempty"` FilesSkipped uint64 `json:"files_skipped,omitempty"` + FilesDeleted uint64 `json:"files_deleted,omitempty"` TotalBytes uint64 `json:"total_bytes,omitempty"` BytesRestored uint64 `json:"bytes_restored,omitempty"` BytesSkipped uint64 `json:"bytes_skipped,omitempty"` diff --git a/internal/ui/restore/json_test.go b/internal/ui/restore/json_test.go index 917a48070..c7096c246 100644 --- a/internal/ui/restore/json_test.go +++ b/internal/ui/restore/json_test.go @@ -17,31 +17,31 @@ func createJSONProgress() (*ui.MockTerminal, ProgressPrinter) { func TestJSONPrintUpdate(t *testing.T) { term, printer := createJSONProgress() - printer.Update(State{3, 11, 0, 29, 47, 0}, 5*time.Second) + printer.Update(State{3, 11, 0, 0, 29, 47, 0}, 5*time.Second) test.Equals(t, []string{"{\"message_type\":\"status\",\"seconds_elapsed\":5,\"percent_done\":0.6170212765957447,\"total_files\":11,\"files_restored\":3,\"total_bytes\":47,\"bytes_restored\":29}\n"}, term.Output) } func TestJSONPrintUpdateWithSkipped(t *testing.T) { term, printer := createJSONProgress() - printer.Update(State{3, 11, 2, 29, 47, 59}, 5*time.Second) + printer.Update(State{3, 11, 2, 0, 29, 47, 59}, 5*time.Second) test.Equals(t, []string{"{\"message_type\":\"status\",\"seconds_elapsed\":5,\"percent_done\":0.6170212765957447,\"total_files\":11,\"files_restored\":3,\"files_skipped\":2,\"total_bytes\":47,\"bytes_restored\":29,\"bytes_skipped\":59}\n"}, term.Output) } func TestJSONPrintSummaryOnSuccess(t *testing.T) { term, printer := createJSONProgress() - printer.Finish(State{11, 11, 0, 47, 47, 0}, 5*time.Second) + printer.Finish(State{11, 11, 0, 0, 47, 47, 0}, 5*time.Second) test.Equals(t, []string{"{\"message_type\":\"summary\",\"seconds_elapsed\":5,\"total_files\":11,\"files_restored\":11,\"total_bytes\":47,\"bytes_restored\":47}\n"}, term.Output) } func TestJSONPrintSummaryOnErrors(t *testing.T) { term, printer := createJSONProgress() - printer.Finish(State{3, 11, 0, 29, 47, 0}, 5*time.Second) + printer.Finish(State{3, 11, 0, 0, 29, 47, 0}, 5*time.Second) test.Equals(t, []string{"{\"message_type\":\"summary\",\"seconds_elapsed\":5,\"total_files\":11,\"files_restored\":3,\"total_bytes\":47,\"bytes_restored\":29}\n"}, term.Output) } func TestJSONPrintSummaryOnSuccessWithSkipped(t *testing.T) { term, printer := createJSONProgress() - printer.Finish(State{11, 11, 2, 47, 47, 59}, 5*time.Second) + printer.Finish(State{11, 11, 2, 0, 47, 47, 59}, 5*time.Second) test.Equals(t, []string{"{\"message_type\":\"summary\",\"seconds_elapsed\":5,\"total_files\":11,\"files_restored\":11,\"files_skipped\":2,\"total_bytes\":47,\"bytes_restored\":47,\"bytes_skipped\":59}\n"}, term.Output) } diff --git a/internal/ui/restore/progress.go b/internal/ui/restore/progress.go index 06f4c86aa..41367f346 100644 --- a/internal/ui/restore/progress.go +++ b/internal/ui/restore/progress.go @@ -11,6 +11,7 @@ type State struct { FilesFinished uint64 FilesTotal uint64 FilesSkipped uint64 + FilesDeleted uint64 AllBytesWritten uint64 AllBytesTotal uint64 AllBytesSkipped uint64 @@ -124,11 +125,13 @@ func (p *Progress) AddSkippedFile(name string, size uint64) { p.printer.CompleteItem(ActionFileUnchanged, name, size) } -func (p *Progress) ReportDeletedFile(name string) { +func (p *Progress) ReportDeletion(name string) { if p == nil { return } + p.s.FilesDeleted++ + p.m.Lock() defer p.m.Unlock() diff --git a/internal/ui/restore/progress_test.go b/internal/ui/restore/progress_test.go index b01440bee..b6f72726c 100644 --- a/internal/ui/restore/progress_test.go +++ b/internal/ui/restore/progress_test.go @@ -72,7 +72,7 @@ func TestNew(t *testing.T) { return false }) test.Equals(t, printerTrace{ - printerTraceEntry{State{0, 0, 0, 0, 0, 0}, 0, false}, + printerTraceEntry{State{0, 0, 0, 0, 0, 0, 0}, 0, false}, }, result) test.Equals(t, itemTrace{}, items) } @@ -85,7 +85,7 @@ func TestAddFile(t *testing.T) { return false }) test.Equals(t, printerTrace{ - printerTraceEntry{State{0, 1, 0, 0, fileSize, 0}, 0, false}, + printerTraceEntry{State{0, 1, 0, 0, 0, fileSize, 0}, 0, false}, }, result) test.Equals(t, itemTrace{}, items) } @@ -100,7 +100,7 @@ func TestFirstProgressOnAFile(t *testing.T) { return false }) test.Equals(t, printerTrace{ - printerTraceEntry{State{0, 1, 0, expectedBytesWritten, expectedBytesTotal, 0}, 0, false}, + printerTraceEntry{State{0, 1, 0, 0, expectedBytesWritten, expectedBytesTotal, 0}, 0, false}, }, result) test.Equals(t, itemTrace{}, items) } @@ -116,7 +116,7 @@ func TestLastProgressOnAFile(t *testing.T) { return false }) test.Equals(t, printerTrace{ - printerTraceEntry{State{1, 1, 0, fileSize, fileSize, 0}, 0, false}, + printerTraceEntry{State{1, 1, 0, 0, fileSize, fileSize, 0}, 0, false}, }, result) test.Equals(t, itemTrace{ itemTraceEntry{action: ActionFileUpdated, item: "test", size: fileSize}, @@ -135,7 +135,7 @@ func TestLastProgressOnLastFile(t *testing.T) { return false }) test.Equals(t, printerTrace{ - printerTraceEntry{State{2, 2, 0, 50 + fileSize, 50 + fileSize, 0}, 0, false}, + 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}, @@ -154,7 +154,7 @@ func TestSummaryOnSuccess(t *testing.T) { return true }) test.Equals(t, printerTrace{ - printerTraceEntry{State{2, 2, 0, 50 + fileSize, 50 + fileSize, 0}, mockFinishDuration, true}, + printerTraceEntry{State{2, 2, 0, 0, 50 + fileSize, 50 + fileSize, 0}, mockFinishDuration, true}, }, result) } @@ -169,7 +169,7 @@ func TestSummaryOnErrors(t *testing.T) { return true }) test.Equals(t, printerTrace{ - printerTraceEntry{State{1, 2, 0, 50 + fileSize/2, 50 + fileSize, 0}, mockFinishDuration, true}, + printerTraceEntry{State{1, 2, 0, 0, 50 + fileSize/2, 50 + fileSize, 0}, mockFinishDuration, true}, }, result) } @@ -181,7 +181,7 @@ func TestSkipFile(t *testing.T) { return true }) test.Equals(t, printerTrace{ - printerTraceEntry{State{0, 0, 1, 0, 0, fileSize}, mockFinishDuration, true}, + printerTraceEntry{State{0, 0, 1, 0, 0, 0, fileSize}, mockFinishDuration, true}, }, result) test.Equals(t, itemTrace{ itemTraceEntry{ActionFileUnchanged, "test", fileSize}, @@ -196,7 +196,7 @@ func TestProgressTypes(t *testing.T) { progress.AddFile(0) progress.AddProgress("dir", ActionDirRestored, fileSize, fileSize) progress.AddProgress("new", ActionFileRestored, 0, 0) - progress.ReportDeletedFile("del") + progress.ReportDeletion("del") return true }) test.Equals(t, itemTrace{ diff --git a/internal/ui/restore/text.go b/internal/ui/restore/text.go index ba0dcd007..35c9db029 100644 --- a/internal/ui/restore/text.go +++ b/internal/ui/restore/text.go @@ -30,6 +30,9 @@ func (t *textPrinter) Update(p State, duration time.Duration) { if p.FilesSkipped > 0 { progress += fmt.Sprintf(", skipped %v files/dirs %v", p.FilesSkipped, ui.FormatBytes(p.AllBytesSkipped)) } + if p.FilesDeleted > 0 { + progress += fmt.Sprintf(", deleted %v files/dirs", p.FilesDeleted) + } t.terminal.SetStatus([]string{progress}) } @@ -82,6 +85,9 @@ func (t *textPrinter) Finish(p State, duration time.Duration) { if p.FilesSkipped > 0 { summary += fmt.Sprintf(", skipped %v files/dirs %v", p.FilesSkipped, ui.FormatBytes(p.AllBytesSkipped)) } + if p.FilesDeleted > 0 { + summary += fmt.Sprintf(", deleted %v files/dirs", p.FilesDeleted) + } t.terminal.Print(summary) } diff --git a/internal/ui/restore/text_test.go b/internal/ui/restore/text_test.go index 4ffb1615d..746700cd8 100644 --- a/internal/ui/restore/text_test.go +++ b/internal/ui/restore/text_test.go @@ -17,31 +17,31 @@ func createTextProgress() (*ui.MockTerminal, ProgressPrinter) { func TestPrintUpdate(t *testing.T) { term, printer := createTextProgress() - printer.Update(State{3, 11, 0, 29, 47, 0}, 5*time.Second) + printer.Update(State{3, 11, 0, 0, 29, 47, 0}, 5*time.Second) test.Equals(t, []string{"[0:05] 61.70% 3 files/dirs 29 B, total 11 files/dirs 47 B"}, term.Output) } func TestPrintUpdateWithSkipped(t *testing.T) { term, printer := createTextProgress() - printer.Update(State{3, 11, 2, 29, 47, 59}, 5*time.Second) + printer.Update(State{3, 11, 2, 0, 29, 47, 59}, 5*time.Second) test.Equals(t, []string{"[0:05] 61.70% 3 files/dirs 29 B, total 11 files/dirs 47 B, skipped 2 files/dirs 59 B"}, term.Output) } func TestPrintSummaryOnSuccess(t *testing.T) { term, printer := createTextProgress() - printer.Finish(State{11, 11, 0, 47, 47, 0}, 5*time.Second) + printer.Finish(State{11, 11, 0, 0, 47, 47, 0}, 5*time.Second) test.Equals(t, []string{"Summary: Restored 11 files/dirs (47 B) in 0:05"}, term.Output) } func TestPrintSummaryOnErrors(t *testing.T) { term, printer := createTextProgress() - printer.Finish(State{3, 11, 0, 29, 47, 0}, 5*time.Second) + printer.Finish(State{3, 11, 0, 0, 29, 47, 0}, 5*time.Second) test.Equals(t, []string{"Summary: Restored 3 / 11 files/dirs (29 B / 47 B) in 0:05"}, term.Output) } func TestPrintSummaryOnSuccessWithSkipped(t *testing.T) { term, printer := createTextProgress() - printer.Finish(State{11, 11, 2, 47, 47, 59}, 5*time.Second) + printer.Finish(State{11, 11, 2, 0, 47, 47, 59}, 5*time.Second) test.Equals(t, []string{"Summary: Restored 11 files/dirs (47 B) in 0:05, skipped 2 files/dirs 59 B"}, term.Output) }