From f9b63050eb36429bc550cf09b1d3a2684f27fd95 Mon Sep 17 00:00:00 2001 From: Winfried Plappert <18740761+wplapper@users.noreply.github.com> Date: Wed, 25 Feb 2026 20:24:31 +0000 Subject: [PATCH] Bugfix: ``restic find --pack `` did not produce output for tree packs (#5664) --- changelog/unreleased/pull-5664 | 6 +++ cmd/restic/cmd_find.go | 14 ++++- cmd/restic/cmd_find_integration_test.go | 70 +++++++++++++++++++++++++ 3 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 changelog/unreleased/pull-5664 diff --git a/changelog/unreleased/pull-5664 b/changelog/unreleased/pull-5664 new file mode 100644 index 000000000..3e480f0d3 --- /dev/null +++ b/changelog/unreleased/pull-5664 @@ -0,0 +1,6 @@ +Bugfix: restic find --pack did not produce output for tree packs + +`restic find --pack` now produces output for a tree related packfile. + +https://github.com/restic/restic/issues/5280 +https://github.com/restic/restic/pull/5664 diff --git a/cmd/restic/cmd_find.go b/cmd/restic/cmd_find.go index c4107222d..ae7782f85 100644 --- a/cmd/restic/cmd_find.go +++ b/cmd/restic/cmd_find.go @@ -450,6 +450,9 @@ func (f *Finder) packsToBlobs(ctx context.Context, packs []string) error { if f.blobIDs == nil { f.blobIDs = make(map[string]struct{}) } + if f.treeIDs == nil { + f.treeIDs = make(map[string]struct{}) + } debug.Log("Looking for packs...") err := f.repo.List(ctx, restic.PackFile, func(id restic.ID, size int64) error { @@ -470,7 +473,14 @@ func (f *Finder) packsToBlobs(ctx context.Context, packs []string) error { return err } for _, b := range blobs { - f.blobIDs[b.ID.String()] = struct{}{} + switch b.Type { + case restic.DataBlob: + f.blobIDs[b.ID.String()] = struct{}{} + case restic.TreeBlob: + f.treeIDs[b.ID.String()] = struct{}{} + default: + panic(fmt.Sprintf("unknown type %v in blob list", b.Type.String())) + } } // Stop searching when all packs have been found if len(packIDs) == 0 { @@ -557,7 +567,7 @@ func (f *Finder) findObjectPack(id string, t restic.BlobType) { blobs := f.repo.LookupBlob(t, rid) if len(blobs) == 0 { - f.printer.S("Object %s not found in the index", rid.Str()) + f.printer.S("Object %s with type %s not found in the index", t.String(), rid.Str()) return } diff --git a/cmd/restic/cmd_find_integration_test.go b/cmd/restic/cmd_find_integration_test.go index c7479a4f8..561d1329e 100644 --- a/cmd/restic/cmd_find_integration_test.go +++ b/cmd/restic/cmd_find_integration_test.go @@ -3,12 +3,15 @@ package main import ( "context" "encoding/json" + "path/filepath" "strings" "testing" "time" "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" ) func testRunFind(t testing.TB, wantJSON bool, opts FindOptions, gopts global.Options, pattern string) []byte { @@ -141,3 +144,70 @@ func TestFindInvalidTimeRange(t *testing.T) { rtest.Assert(t, err != nil && err.Error() == "Fatal: --oldest must specify a time before --newest", "unexpected error message: %v", err) } + +// JsonOutput is the struct `restic find --json` produces +type JSONOutput struct { + ObjectType string `json:"object_type"` + ID string `json:"id"` + Path string `json:"path"` + ParentTree string `json:"parent_tree,omitempty"` + SnapshotID string `json:"snapshot"` + Time time.Time `json:"time,omitempty"` +} + +func TestFindPackfile(t *testing.T) { + env, cleanup := withTestEnvironment(t) + defer cleanup() + + testSetupBackupData(t, env) + + // backup + backupPath := env.testdata + "/0/0/9" + testRunBackup(t, "", []string{backupPath}, BackupOptions{}, env.gopts) + sn1 := testListSnapshots(t, env.gopts, 1)[0] + + // do all the testing wrapped inside withTermStatus() + err := withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error { + printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, gopts.Term) + _, repo, unlock, err := openWithReadLock(ctx, gopts, false, printer) + rtest.OK(t, err) + defer unlock() + + // load master index + rtest.OK(t, repo.LoadIndex(ctx, printer)) + + packID := restic.ID{} + done := false + err = repo.ListBlobs(ctx, func(pb restic.PackedBlob) { + if !done && pb.Type == restic.TreeBlob { + packID = pb.PackID + done = true + } + }) + + rtest.OK(t, err) + rtest.Assert(t, !packID.IsNull(), "expected a tree packfile ID") + findOptions := FindOptions{PackID: true} + results := testRunFind(t, true, findOptions, env.gopts, packID.String()) + + // get the json records + jsonResult := []JSONOutput{} + rtest.OK(t, json.Unmarshal(results, &jsonResult)) + rtest.Assert(t, len(jsonResult) > 0, "expected at least one tree record in the packfile") + + // look at the last record + lastIndex := len(jsonResult) - 1 + record := jsonResult[lastIndex] + rtest.Assert(t, record.ObjectType == "tree" && record.SnapshotID == sn1.String(), + "expected a tree record with known snapshot id, but got type=%s and snapID=%s instead of %s", + record.ObjectType, record.SnapshotID, sn1.String()) + backupPath = filepath.ToSlash(backupPath)[2:] // take the offending drive mapping away + rtest.Assert(t, strings.Contains(record.Path, backupPath), "expected %q as part of %q", backupPath, record.Path) + // Windows response: + //expected "C:/Users/RUNNER~1/AppData/Local/Temp/restic-test-3529440698/testdata/0/0/9" as part of + // "/C/Users/RUNNER~1/AppData/Local/Temp/restic-test-3529440698/testdata/0/0/9" + + return nil + }) + rtest.OK(t, err) +}