From d30d6b628192f38f8ed123759b65ae7cc4e00ae5 Mon Sep 17 00:00:00 2001 From: Winfried Plappert <18740761+wplapper@users.noreply.github.com> Date: Sun, 21 Jun 2026 17:11:42 +0100 Subject: [PATCH] backup - show excluded files and directories in verbose mode - text/JSON (#21887) --- changelog/unreleased/issue-21870 | 7 ++++ cmd/restic/cmd_backup.go | 1 + cmd/restic/cmd_backup_integration_test.go | 48 +++++++++++++++++++++++ doc/075_scripting.rst | 9 +++++ internal/archiver/archiver.go | 6 +++ internal/ui/backup/json.go | 15 +++++++ internal/ui/backup/progress.go | 5 +++ internal/ui/backup/progress_test.go | 3 +- internal/ui/backup/text.go | 4 ++ 9 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 changelog/unreleased/issue-21870 diff --git a/changelog/unreleased/issue-21870 b/changelog/unreleased/issue-21870 new file mode 100644 index 000000000..0273fb8d4 --- /dev/null +++ b/changelog/unreleased/issue-21870 @@ -0,0 +1,7 @@ +Enhancement: show excluded items during backup in verbose mode + +`restic backup -vv` now shows excluded items in verbose mode. Both text and JSON +output are supported. + +https://github.com/restic/restic/issues/21870 +https://github.com/restic/restic/pull/21887 diff --git a/cmd/restic/cmd_backup.go b/cmd/restic/cmd_backup.go index f0a1c3a19..86cdbe3a4 100644 --- a/cmd/restic/cmd_backup.go +++ b/cmd/restic/cmd_backup.go @@ -666,6 +666,7 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts global.Options, te arch.CompleteItem = progressReporter.CompleteItem arch.StartFile = progressReporter.StartFile arch.CompleteBlob = progressReporter.CompleteBlob + arch.ExcludedItem = progressReporter.ExcludedItem if opts.IgnoreInode { // --ignore-inode implies --ignore-ctime: on FUSE, the ctime is not diff --git a/cmd/restic/cmd_backup_integration_test.go b/cmd/restic/cmd_backup_integration_test.go index 840ef65b9..be35da909 100644 --- a/cmd/restic/cmd_backup_integration_test.go +++ b/cmd/restic/cmd_backup_integration_test.go @@ -1,11 +1,14 @@ package main import ( + "bytes" "context" + "encoding/json" "fmt" "os" "path/filepath" "runtime" + "strings" "testing" "time" @@ -15,6 +18,7 @@ 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/backup" ) func testRunBackupAssumeFailure(t testing.TB, dir string, target []string, opts BackupOptions, gopts global.Options) error { @@ -30,6 +34,13 @@ func testRunBackupAssumeFailure(t testing.TB, dir string, target []string, opts }) } +func testRunBackupOutput(t testing.TB, opts BackupOptions, gopts global.Options, target []string) ([]byte, error) { + buf, err := withCaptureStdout(t, gopts, func(ctx context.Context, gopts global.Options) error { + return runBackup(ctx, opts, gopts, gopts.Term, target) + }) + return buf.Bytes(), err +} + func testRunBackup(t testing.TB, dir string, target []string, opts BackupOptions, gopts global.Options) { err := testRunBackupAssumeFailure(t, dir, target, opts, gopts) rtest.Assert(t, err == nil, "Error while backing up: %v", err) @@ -730,3 +741,40 @@ func TestBackupSkipIfUnchanged(t *testing.T) { testRunCheck(t, env.gopts) } + +func TestBackupExcludeWithOutput(t *testing.T) { + env, cleanup := withTestEnvironment(t) + defer cleanup() + + testSetupBackupData(t, env) + backupOptions := BackupOptions{} + backupOptions.Excludes = []string{"*.py"} + + env.gopts.JSON = true + env.gopts.Verbosity = 2 + output, err := testRunBackupOutput(t, backupOptions, env.gopts, []string{filepath.Join(env.testdata, "0", "for_cmd_ls")}) + rtest.OK(t, err) + + foundExclude := false + for _, line := range bytes.Split(output, []byte("\n")) { + if len(line) == 0 { + continue + } + + type MessageType struct { + MessageType string `json:"message_type"` // any + } + var mType MessageType + rtest.OK(t, json.Unmarshal(line, &mType)) + if mType.MessageType != "excluded_item" { + continue + } + + var excludeLine backup.VerboseExclude + rtest.OK(t, json.Unmarshal(line, &excludeLine)) + rtest.Assert(t, strings.Contains(excludeLine.Item, ".py"), "expected excluded pathname to be ending in .py, but contains %q", + excludeLine.Item) + foundExclude = true + } + rtest.Assert(t, foundExclude, "expected at least one excluded item, but found none") +} diff --git a/doc/075_scripting.rst b/doc/075_scripting.rst index 1e3931411..a92e52896 100644 --- a/doc/075_scripting.rst +++ b/doc/075_scripting.rst @@ -311,6 +311,15 @@ Verbose status provides details about the progress, including details about back | ``total_files`` | Total number of files | uint64 | +---------------------------+----------------------------------------------------------+---------+ +Excluded backup items are detailed in this format: + ++---------------------------+----------------------------------------------------------+---------+ +| ``message_type`` | Always "excluded_item" | string | ++---------------------------+----------------------------------------------------------+---------+ +| ``item`` | The item in question | string | ++---------------------------+----------------------------------------------------------+---------+ + + Summary ^^^^^^^ diff --git a/internal/archiver/archiver.go b/internal/archiver/archiver.go index 095a79765..25d90a572 100644 --- a/internal/archiver/archiver.go +++ b/internal/archiver/archiver.go @@ -129,6 +129,9 @@ type Archiver struct { // Flags controlling change detection. See doc/040_backup.rst for details. ChangeIgnoreFlags uint + + // for excluded items + ExcludedItem func(path string) } // Flags for the ChangeIgnoreFlags bitfield. @@ -183,6 +186,7 @@ func New(repo archiverRepo, filesystem fs.FS, opts Options) *Archiver { CompleteItem: func(string, ItemAction, ItemStats, time.Duration) {}, StartFile: func(string) {}, CompleteBlob: func(uint64) {}, + ExcludedItem: func(string) {}, } return arch @@ -481,6 +485,7 @@ func (arch *Archiver) save(ctx context.Context, snPath, target string, previous // exclude files by path before running Lstat to reduce number of lstat calls if !explicit && !arch.SelectByName(abstarget) { debug.Log("%v is excluded by path", target) + arch.ExcludedItem(abstarget) return futureNode{}, true, nil } @@ -509,6 +514,7 @@ func (arch *Archiver) save(ctx context.Context, snPath, target string, previous } if !explicit && !arch.Select(abstarget, fi, arch.FS) { debug.Log("%v is excluded", target) + arch.ExcludedItem(abstarget) return futureNode{}, true, nil } diff --git a/internal/ui/backup/json.go b/internal/ui/backup/json.go index 4996e57b0..9c1b51599 100644 --- a/internal/ui/backup/json.go +++ b/internal/ui/backup/json.go @@ -251,3 +251,18 @@ type summaryOutput struct { SnapshotID string `json:"snapshot_id,omitempty"` DryRun bool `json:"dry_run,omitempty"` } + +type VerboseExclude struct { + MessageType string `json:"message_type"` // "excluded_item" + Item string `json:"item"` // file or directory name +} + +func (b *jsonProgress) ExcludedItem(path string) { + if b.v < 2 { + return + } + b.print(VerboseExclude{ + MessageType: "excluded_item", + Item: path, + }) +} diff --git a/internal/ui/backup/progress.go b/internal/ui/backup/progress.go index dafac2bdc..a50f48602 100644 --- a/internal/ui/backup/progress.go +++ b/internal/ui/backup/progress.go @@ -19,6 +19,7 @@ type ProgressPrinter interface { ReportTotal(start time.Time, s archiver.ScanStats) Finish(snapshotID restic.ID, summary *archiver.Summary, dryRun bool) Reset() + ExcludedItem(path string) restic.Printer } @@ -162,3 +163,7 @@ func (p *Progress) Finish(snapshotID restic.ID, summary *archiver.Summary, dryru p.Updater.Done() p.printer.Finish(snapshotID, summary, dryrun) } + +func (p *Progress) ExcludedItem(path string) { + p.printer.ExcludedItem(path) +} diff --git a/internal/ui/backup/progress_test.go b/internal/ui/backup/progress_test.go index 9940f6d44..fdf351557 100644 --- a/internal/ui/backup/progress_test.go +++ b/internal/ui/backup/progress_test.go @@ -41,7 +41,8 @@ func (p *mockPrinter) Finish(id restic.ID, _ *archiver.Summary, _ bool) { p.id = id } -func (p *mockPrinter) Reset() {} +func (p *mockPrinter) Reset() {} +func (p *mockPrinter) ExcludedItem(_ string) {} func TestProgress(t *testing.T) { t.Parallel() diff --git a/internal/ui/backup/text.go b/internal/ui/backup/text.go index 9e91977c2..32ff0effd 100644 --- a/internal/ui/backup/text.go +++ b/internal/ui/backup/text.go @@ -158,3 +158,7 @@ func (b *textProgress) Finish(id restic.ID, summary *archiver.Summary, dryRun bo } } } + +func (b *textProgress) ExcludedItem(path string) { + b.VV("excluded %s", path) +}