1
0
Fork 0
mirror of https://github.com/restic/restic.git synced 2024-12-21 23:33:03 +00:00

ui: restore --delete indicates number of deleted files (#5100)

* ui: restore --delete indicates number of deleted files

* adds new field `FilesDeleted` to the State struct, JSON and text progress updaters
* increment FilesDeleted count when ReportedDeletedFile

* ui: collect the files to be deleted, delete, then update the count post deletion

* docs: update scripting output fields for restore command

ui: report deleted directories and refactor function name to ReportDeletion
This commit is contained in:
Srigovind Nayak 2024-12-01 19:59:11 +05:30 committed by GitHub
parent 2f0049cd6c
commit d7d9af4c9f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 64 additions and 21 deletions

View file

@ -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

View file

@ -563,6 +563,8 @@ Status
+----------------------+------------------------------------------------------------+ +----------------------+------------------------------------------------------------+
|``files_skipped`` | Files skipped due to overwrite setting | |``files_skipped`` | Files skipped due to overwrite setting |
+----------------------+------------------------------------------------------------+ +----------------------+------------------------------------------------------------+
|``files_deleted`` | Files deleted |
+----------------------+------------------------------------------------------------+
|``total_bytes`` | Total number of bytes in restore set | |``total_bytes`` | Total number of bytes in restore set |
+----------------------+------------------------------------------------------------+ +----------------------+------------------------------------------------------------+
|``bytes_restored`` | Number of bytes restored | |``bytes_restored`` | Number of bytes restored |
@ -615,6 +617,8 @@ Summary
+----------------------+------------------------------------------------------------+ +----------------------+------------------------------------------------------------+
|``files_skipped`` | Files skipped due to overwrite setting | |``files_skipped`` | Files skipped due to overwrite setting |
+----------------------+------------------------------------------------------------+ +----------------------+------------------------------------------------------------+
|``files_deleted`` | Files deleted |
+----------------------+------------------------------------------------------------+
|``total_bytes`` | Total number of bytes in restore set | |``total_bytes`` | Total number of bytes in restore set |
+----------------------+------------------------------------------------------------+ +----------------------+------------------------------------------------------------+
|``bytes_restored`` | Number of bytes restored | |``bytes_restored`` | Number of bytes restored |

View file

@ -511,12 +511,30 @@ func (res *Restorer) removeUnexpectedFiles(ctx context.Context, target, location
selectedForRestore, _ := res.SelectFilter(nodeLocation, false) selectedForRestore, _ := res.SelectFilter(nodeLocation, false)
// only delete files that were selected for restore // only delete files that were selected for restore
if selectedForRestore { 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 { if !res.opts.DryRun {
// Perform the deletion
if err := fs.RemoveAll(nodeTarget); err != nil { if err := fs.RemoveAll(nodeTarget); err != nil {
return err return err
} }
} }
// Report paths as deleted only after successful removal
for i := len(filesToDelete) - 1; i >= 0; i-- {
res.opts.Progress.ReportDeletion(filesToDelete[i])
}
} }
} }

View file

@ -33,6 +33,7 @@ func (t *jsonPrinter) Update(p State, duration time.Duration) {
TotalFiles: p.FilesTotal, TotalFiles: p.FilesTotal,
FilesRestored: p.FilesFinished, FilesRestored: p.FilesFinished,
FilesSkipped: p.FilesSkipped, FilesSkipped: p.FilesSkipped,
FilesDeleted: p.FilesDeleted,
TotalBytes: p.AllBytesTotal, TotalBytes: p.AllBytesTotal,
BytesRestored: p.AllBytesWritten, BytesRestored: p.AllBytesWritten,
BytesSkipped: p.AllBytesSkipped, BytesSkipped: p.AllBytesSkipped,
@ -94,6 +95,7 @@ func (t *jsonPrinter) Finish(p State, duration time.Duration) {
TotalFiles: p.FilesTotal, TotalFiles: p.FilesTotal,
FilesRestored: p.FilesFinished, FilesRestored: p.FilesFinished,
FilesSkipped: p.FilesSkipped, FilesSkipped: p.FilesSkipped,
FilesDeleted: p.FilesDeleted,
TotalBytes: p.AllBytesTotal, TotalBytes: p.AllBytesTotal,
BytesRestored: p.AllBytesWritten, BytesRestored: p.AllBytesWritten,
BytesSkipped: p.AllBytesSkipped, BytesSkipped: p.AllBytesSkipped,
@ -108,6 +110,7 @@ type statusUpdate struct {
TotalFiles uint64 `json:"total_files,omitempty"` TotalFiles uint64 `json:"total_files,omitempty"`
FilesRestored uint64 `json:"files_restored,omitempty"` FilesRestored uint64 `json:"files_restored,omitempty"`
FilesSkipped uint64 `json:"files_skipped,omitempty"` FilesSkipped uint64 `json:"files_skipped,omitempty"`
FilesDeleted uint64 `json:"files_deleted,omitempty"`
TotalBytes uint64 `json:"total_bytes,omitempty"` TotalBytes uint64 `json:"total_bytes,omitempty"`
BytesRestored uint64 `json:"bytes_restored,omitempty"` BytesRestored uint64 `json:"bytes_restored,omitempty"`
BytesSkipped uint64 `json:"bytes_skipped,omitempty"` BytesSkipped uint64 `json:"bytes_skipped,omitempty"`
@ -137,6 +140,7 @@ type summaryOutput struct {
TotalFiles uint64 `json:"total_files,omitempty"` TotalFiles uint64 `json:"total_files,omitempty"`
FilesRestored uint64 `json:"files_restored,omitempty"` FilesRestored uint64 `json:"files_restored,omitempty"`
FilesSkipped uint64 `json:"files_skipped,omitempty"` FilesSkipped uint64 `json:"files_skipped,omitempty"`
FilesDeleted uint64 `json:"files_deleted,omitempty"`
TotalBytes uint64 `json:"total_bytes,omitempty"` TotalBytes uint64 `json:"total_bytes,omitempty"`
BytesRestored uint64 `json:"bytes_restored,omitempty"` BytesRestored uint64 `json:"bytes_restored,omitempty"`
BytesSkipped uint64 `json:"bytes_skipped,omitempty"` BytesSkipped uint64 `json:"bytes_skipped,omitempty"`

View file

@ -17,31 +17,31 @@ func createJSONProgress() (*ui.MockTerminal, ProgressPrinter) {
func TestJSONPrintUpdate(t *testing.T) { func TestJSONPrintUpdate(t *testing.T) {
term, printer := createJSONProgress() 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) 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) { func TestJSONPrintUpdateWithSkipped(t *testing.T) {
term, printer := createJSONProgress() 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) 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) { func TestJSONPrintSummaryOnSuccess(t *testing.T) {
term, printer := createJSONProgress() 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) 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) { func TestJSONPrintSummaryOnErrors(t *testing.T) {
term, printer := createJSONProgress() 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) 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) { func TestJSONPrintSummaryOnSuccessWithSkipped(t *testing.T) {
term, printer := createJSONProgress() 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) 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)
} }

View file

@ -11,6 +11,7 @@ type State struct {
FilesFinished uint64 FilesFinished uint64
FilesTotal uint64 FilesTotal uint64
FilesSkipped uint64 FilesSkipped uint64
FilesDeleted uint64
AllBytesWritten uint64 AllBytesWritten uint64
AllBytesTotal uint64 AllBytesTotal uint64
AllBytesSkipped uint64 AllBytesSkipped uint64
@ -124,11 +125,13 @@ func (p *Progress) AddSkippedFile(name string, size uint64) {
p.printer.CompleteItem(ActionFileUnchanged, name, size) p.printer.CompleteItem(ActionFileUnchanged, name, size)
} }
func (p *Progress) ReportDeletedFile(name string) { func (p *Progress) ReportDeletion(name string) {
if p == nil { if p == nil {
return return
} }
p.s.FilesDeleted++
p.m.Lock() p.m.Lock()
defer p.m.Unlock() defer p.m.Unlock()

View file

@ -72,7 +72,7 @@ func TestNew(t *testing.T) {
return false return false
}) })
test.Equals(t, printerTrace{ 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) }, result)
test.Equals(t, itemTrace{}, items) test.Equals(t, itemTrace{}, items)
} }
@ -85,7 +85,7 @@ func TestAddFile(t *testing.T) {
return false return false
}) })
test.Equals(t, printerTrace{ 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) }, result)
test.Equals(t, itemTrace{}, items) test.Equals(t, itemTrace{}, items)
} }
@ -100,7 +100,7 @@ func TestFirstProgressOnAFile(t *testing.T) {
return false return false
}) })
test.Equals(t, printerTrace{ 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) }, result)
test.Equals(t, itemTrace{}, items) test.Equals(t, itemTrace{}, items)
} }
@ -116,7 +116,7 @@ func TestLastProgressOnAFile(t *testing.T) {
return false return false
}) })
test.Equals(t, printerTrace{ 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) }, result)
test.Equals(t, itemTrace{ test.Equals(t, itemTrace{
itemTraceEntry{action: ActionFileUpdated, item: "test", size: fileSize}, itemTraceEntry{action: ActionFileUpdated, item: "test", size: fileSize},
@ -135,7 +135,7 @@ func TestLastProgressOnLastFile(t *testing.T) {
return false return false
}) })
test.Equals(t, printerTrace{ 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) }, result)
test.Equals(t, itemTrace{ test.Equals(t, itemTrace{
itemTraceEntry{action: ActionFileUpdated, item: "test1", size: 50}, itemTraceEntry{action: ActionFileUpdated, item: "test1", size: 50},
@ -154,7 +154,7 @@ func TestSummaryOnSuccess(t *testing.T) {
return true return true
}) })
test.Equals(t, printerTrace{ 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) }, result)
} }
@ -169,7 +169,7 @@ func TestSummaryOnErrors(t *testing.T) {
return true return true
}) })
test.Equals(t, printerTrace{ 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) }, result)
} }
@ -181,7 +181,7 @@ func TestSkipFile(t *testing.T) {
return true return true
}) })
test.Equals(t, printerTrace{ 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) }, result)
test.Equals(t, itemTrace{ test.Equals(t, itemTrace{
itemTraceEntry{ActionFileUnchanged, "test", fileSize}, itemTraceEntry{ActionFileUnchanged, "test", fileSize},
@ -196,7 +196,7 @@ func TestProgressTypes(t *testing.T) {
progress.AddFile(0) progress.AddFile(0)
progress.AddProgress("dir", ActionDirRestored, fileSize, fileSize) progress.AddProgress("dir", ActionDirRestored, fileSize, fileSize)
progress.AddProgress("new", ActionFileRestored, 0, 0) progress.AddProgress("new", ActionFileRestored, 0, 0)
progress.ReportDeletedFile("del") progress.ReportDeletion("del")
return true return true
}) })
test.Equals(t, itemTrace{ test.Equals(t, itemTrace{

View file

@ -30,6 +30,9 @@ func (t *textPrinter) Update(p State, duration time.Duration) {
if p.FilesSkipped > 0 { if p.FilesSkipped > 0 {
progress += fmt.Sprintf(", skipped %v files/dirs %v", p.FilesSkipped, ui.FormatBytes(p.AllBytesSkipped)) 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}) t.terminal.SetStatus([]string{progress})
} }
@ -82,6 +85,9 @@ func (t *textPrinter) Finish(p State, duration time.Duration) {
if p.FilesSkipped > 0 { if p.FilesSkipped > 0 {
summary += fmt.Sprintf(", skipped %v files/dirs %v", p.FilesSkipped, ui.FormatBytes(p.AllBytesSkipped)) 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) t.terminal.Print(summary)
} }

View file

@ -17,31 +17,31 @@ func createTextProgress() (*ui.MockTerminal, ProgressPrinter) {
func TestPrintUpdate(t *testing.T) { func TestPrintUpdate(t *testing.T) {
term, printer := createTextProgress() 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) 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) { func TestPrintUpdateWithSkipped(t *testing.T) {
term, printer := createTextProgress() 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) 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) { func TestPrintSummaryOnSuccess(t *testing.T) {
term, printer := createTextProgress() 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) test.Equals(t, []string{"Summary: Restored 11 files/dirs (47 B) in 0:05"}, term.Output)
} }
func TestPrintSummaryOnErrors(t *testing.T) { func TestPrintSummaryOnErrors(t *testing.T) {
term, printer := createTextProgress() 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) test.Equals(t, []string{"Summary: Restored 3 / 11 files/dirs (29 B / 47 B) in 0:05"}, term.Output)
} }
func TestPrintSummaryOnSuccessWithSkipped(t *testing.T) { func TestPrintSummaryOnSuccessWithSkipped(t *testing.T) {
term, printer := createTextProgress() 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) test.Equals(t, []string{"Summary: Restored 11 files/dirs (47 B) in 0:05, skipped 2 files/dirs 59 B"}, term.Output)
} }