restic: change ListBlobs to return PackBlob

PackBlob is a limited interface that only exposes a part of the
information provided by PackedBlob. Most of the changes are switches
from direct value lookups to the interface methods, with a few larger
changes to let the tests still work.
This commit is contained in:
Michael Eischer committed 2026-06-13 17:54:38 +02:00
1 parent 97f1e99ed9
commit ccb5ae1592
15 files changed
+102 -73

No files matched your search

+6 -5
View File
@@ -526,22 +526,23 @@ func (f *Finder) indexPacksToBlobs(ctx context.Context, packIDs map[string]struc
// remember which packs were found in the index // remember which packs were found in the index
indexPackIDs := make(map[string]struct{}) indexPackIDs := make(map[string]struct{})
err := f.repo.ListBlobs(wctx, func(pb restic.PackedBlob) { err := f.repo.ListBlobs(wctx, func(pb restic.PackBlob) {
idStr := pb.PackID.String() packID := pb.PackID()
idStr := packID.String()
// keep entry in packIDs as Each() returns individual index entries // keep entry in packIDs as Each() returns individual index entries
matchingID := false matchingID := false
if _, ok := packIDs[idStr]; ok { if _, ok := packIDs[idStr]; ok {
matchingID = true matchingID = true
} else { } else {
if _, ok := packIDs[pb.PackID.Str()]; ok { if _, ok := packIDs[packID.Str()]; ok {
// expand id // expand id
delete(packIDs, pb.PackID.Str()) delete(packIDs, packID.Str())
packIDs[idStr] = struct{}{} packIDs[idStr] = struct{}{}
matchingID = true matchingID = true
} }
} }
if matchingID { if matchingID {
f.blobIDs[pb.ID.String()] = struct{}{} f.blobIDs[pb.Handle().ID.String()] = struct{}{}
indexPackIDs[idStr] = struct{}{} indexPackIDs[idStr] = struct{}{}
} }
}) })
+8 -7
View File
@@ -179,9 +179,10 @@ func TestFindPackfile(t *testing.T) {
packID := restic.ID{} packID := restic.ID{}
done := false done := false
err = repo.ListBlobs(ctx, func(pb restic.PackedBlob) { err = repo.ListBlobs(ctx, func(pb restic.PackBlob) {
if !done && pb.Type == restic.TreeBlob { h := pb.Handle()
packID = pb.PackID if !done && h.Type == restic.TreeBlob {
packID = pb.PackID()
done = true done = true
} }
}) })
@@ -236,12 +237,12 @@ func TestFindPackID(t *testing.T) {
// load Index // load Index
rtest.OK(t, repo.LoadIndex(ctx, nil)) rtest.OK(t, repo.LoadIndex(ctx, nil))
// go through all index entries and collect data and tree packfile(s) // go through all index entries and collect data and tree packfile(s)
rtest.OK(t, repo.ListBlobs(ctx, func(blob restic.PackedBlob) { rtest.OK(t, repo.ListBlobs(ctx, func(blob restic.PackBlob) {
switch blob.Type { switch blob.Handle().Type {
case restic.DataBlob: case restic.DataBlob:
dataPackID = blob.PackID dataPackID = blob.PackID()
case restic.TreeBlob: case restic.TreeBlob:
treePackID = blob.PackID treePackID = blob.PackID()
} }
})) }))
return nil return nil
+2 -2
View File
@@ -71,8 +71,8 @@ func testListBlobs(t testing.TB, gopts global.Options) (blobSetFromIndex restic.
// get blobs from index // get blobs from index
blobSetFromIndex = restic.NewIDSet() blobSetFromIndex = restic.NewIDSet()
rtest.OK(t, repo.ListBlobs(ctx, func(blob restic.PackedBlob) { rtest.OK(t, repo.ListBlobs(ctx, func(blob restic.PackBlob) {
blobSetFromIndex.Insert(blob.ID) blobSetFromIndex.Insert(blob.Handle().ID)
})) }))
return nil return nil
}) })
+4 -3
View File
@@ -75,9 +75,10 @@ func runRecover(ctx context.Context, gopts global.Options, term ui.Terminal) err
// tree. If it is not referenced, we have a root tree. // tree. If it is not referenced, we have a root tree.
trees := make(map[restic.ID]bool) trees := make(map[restic.ID]bool)
err = repo.ListBlobs(ctx, func(blob restic.PackedBlob) { err = repo.ListBlobs(ctx, func(blob restic.PackBlob) {
if blob.Type == restic.TreeBlob { h := blob.Handle()
trees[blob.Blob.ID] = false if h.Type == restic.TreeBlob {
trees[h.ID] = false
} }
}) })
if err != nil { if err != nil {
+2 -2
View File
@@ -414,8 +414,8 @@ func statsDebugBlobs(ctx context.Context, repo restic.Repository) ([restic.NumBl
hist[i] = newSizeHistogram(2 * chunker.MaxSize) hist[i] = newSizeHistogram(2 * chunker.MaxSize)
} }
err := repo.ListBlobs(ctx, func(pb restic.PackedBlob) { err := repo.ListBlobs(ctx, func(pb restic.PackBlob) {
hist[pb.Type].Add(uint64(pb.Length)) hist[pb.Handle().Type].Add(uint64(pb.CiphertextLength()))
}) })
return hist, err return hist, err
+6 -6
View File
@@ -269,9 +269,9 @@ func listTreePacks(gopts global.Options, t *testing.T) restic.IDSet {
rtest.OK(t, r.LoadIndex(ctx, nil)) rtest.OK(t, r.LoadIndex(ctx, nil))
treePacks = restic.NewIDSet() treePacks = restic.NewIDSet()
return r.ListBlobs(ctx, func(pb restic.PackedBlob) { return r.ListBlobs(ctx, func(pb restic.PackBlob) {
if pb.Type == restic.TreeBlob { if pb.Handle().Type == restic.TreeBlob {
treePacks.Insert(pb.PackID) treePacks.Insert(pb.PackID())
} }
}) })
}) })
@@ -318,9 +318,9 @@ func removePacksExcept(gopts global.Options, t testing.TB, keep restic.IDSet, re
rtest.OK(t, r.LoadIndex(ctx, nil)) rtest.OK(t, r.LoadIndex(ctx, nil))
treePacks := restic.NewIDSet() treePacks := restic.NewIDSet()
rtest.OK(t, r.ListBlobs(ctx, func(pb restic.PackedBlob) { rtest.OK(t, r.ListBlobs(ctx, func(pb restic.PackBlob) {
if pb.Type == restic.TreeBlob { if pb.Handle().Type == restic.TreeBlob {
treePacks.Insert(pb.PackID) treePacks.Insert(pb.PackID())
} }
})) }))
+2 -2
View File
@@ -277,8 +277,8 @@ func (c *Checker) UnusedBlobs(ctx context.Context) (blobs restic.BlobHandles, er
ctx, cancel := context.WithCancel(ctx) ctx, cancel := context.WithCancel(ctx)
defer cancel() defer cancel()
err = c.repo.ListBlobs(ctx, func(blob restic.PackedBlob) { err = c.repo.ListBlobs(ctx, func(blob restic.PackBlob) {
h := restic.BlobHandle{ID: blob.ID, Type: blob.Type} h := blob.Handle()
if !c.blobRefs.M.Has(h) { if !c.blobRefs.M.Has(h) {
debug.Log("blob %v not referenced", h) debug.Log("blob %v not referenced", h)
blobs = append(blobs, h) blobs = append(blobs, h)
+7 -5
View File
@@ -87,16 +87,18 @@ func NewChecker(repo *Repository) *Checker {
} }
func computePackTypes(ctx context.Context, idx restic.ListBlobser) (map[restic.ID]restic.BlobType, error) { func computePackTypes(ctx context.Context, idx restic.ListBlobser) (map[restic.ID]restic.BlobType, error) {
packs := make(map[restic.ID]restic.BlobType) packs := make(map[restic.ID]restic.BlobType)
err := idx.ListBlobs(ctx, func(pb restic.PackedBlob) { err := idx.ListBlobs(ctx, func(pb restic.PackBlob) {
tpe, exists := packs[pb.PackID] packID := pb.PackID()
h := pb.Handle()
tpe, exists := packs[packID]
if exists { if exists {
if pb.Type != tpe { if h.Type != tpe {
tpe = restic.InvalidBlob tpe = restic.InvalidBlob
} }
} else { } else {
tpe = pb.Type tpe = h.Type
} }
packs[pb.PackID] = tpe packs[packID] = tpe
}) })
return packs, err return packs, err
} }
+2 -2
View File
@@ -26,8 +26,8 @@ func TestAllIndexBlobs(t *testing.T) {
rtest.OK(t, repo.LoadIndex(context.TODO(), nil)) rtest.OK(t, repo.LoadIndex(context.TODO(), nil))
fromMaster := restic.NewBlobSet() fromMaster := restic.NewBlobSet()
rtest.OK(t, repo.ListBlobs(context.TODO(), func(pb restic.PackedBlob) { rtest.OK(t, repo.ListBlobs(context.TODO(), func(pb restic.PackBlob) {
fromMaster.Insert(pb.BlobHandle) fromMaster.Insert(pb.Handle())
})) }))
rtest.Equals(t, want, fromMaster) rtest.Equals(t, want, fromMaster)
@@ -0,0 +1,17 @@
package repository
import (
"github.com/restic/restic/internal/restic"
)
// BlobsInPack returns index entries for blobs stored in packID, sorted by offset.
func BlobsInPack(repo *Repository, packID restic.ID) restic.Blobs {
var blobs restic.Blobs
for pb := range repo.idx.Values() {
if pb.PackID.Equal(packID) {
blobs = append(blobs, pb.Blob)
}
}
blobs.Sort()
return blobs
}
+9 -8
View File
@@ -65,7 +65,7 @@ func (p *Packer) Add(t restic.BlobType, id restic.ID, data []byte, uncompressedL
p.bytes += uint(n) p.bytes += uint(n)
p.blobs = append(p.blobs, c) p.blobs = append(p.blobs, c)
return n + CalculateEntrySize(c), nil return n + CalculateEntrySize(c.IsCompressed()), nil
} }
var entrySize = uint(binary.Size(restic.BlobType(0)) + 2*headerLengthSize + len(restic.ID{})) var entrySize = uint(binary.Size(restic.BlobType(0)) + 2*headerLengthSize + len(restic.ID{}))
@@ -420,8 +420,8 @@ func parseHeaderEntry(p []byte) (b restic.Blob, size uint, err error) {
return b, size, nil return b, size, nil
} }
func CalculateEntrySize(blob restic.Blob) int { func CalculateEntrySize(compressed bool) int {
if blob.UncompressedLength != 0 { if compressed {
return int(entrySize) return int(entrySize)
} }
return int(plainEntrySize) return int(plainEntrySize)
@@ -430,7 +430,7 @@ func CalculateEntrySize(blob restic.Blob) int {
func CalculateHeaderSize(blobs restic.Blobs) int { func CalculateHeaderSize(blobs restic.Blobs) int {
size := headerSize size := headerSize
for _, blob := range blobs { for _, blob := range blobs {
size += CalculateEntrySize(blob) size += CalculateEntrySize(blob.IsCompressed())
} }
return size return size
} }
@@ -442,15 +442,16 @@ func CalculateHeaderSize(blobs restic.Blobs) int {
func Size(ctx context.Context, mi restic.ListBlobser, onlyHdr bool) (map[restic.ID]int64, error) { func Size(ctx context.Context, mi restic.ListBlobser, onlyHdr bool) (map[restic.ID]int64, error) {
packSize := make(map[restic.ID]int64) packSize := make(map[restic.ID]int64)
err := mi.ListBlobs(ctx, func(blob restic.PackedBlob) { err := mi.ListBlobs(ctx, func(blob restic.PackBlob) {
size, ok := packSize[blob.PackID] packID := blob.PackID()
size, ok := packSize[packID]
if !ok { if !ok {
size = headerSize size = headerSize
} }
if !onlyHdr { if !onlyHdr {
size += int64(blob.Length) size += int64(blob.CiphertextLength())
} }
packSize[blob.PackID] = size + int64(CalculateEntrySize(blob.Blob)) packSize[packID] = size + int64(CalculateEntrySize(blob.IsCompressed()))
}) })
return packSize, err return packSize, err
+22 -17
View File
@@ -142,11 +142,12 @@ func PlanPrune(ctx context.Context, opts PruneOptions, repo *Repository, getUsed
if len(plan.repackPacks) != 0 { if len(plan.repackPacks) != 0 {
// when repacking, we do not want to keep blobs which are // when repacking, we do not want to keep blobs which are
// already contained in kept packs, so delete them from keepBlobs // already contained in kept packs, so delete them from keepBlobs
err := repo.ListBlobs(ctx, func(blob restic.PackedBlob) { err := repo.ListBlobs(ctx, func(blob restic.PackBlob) {
if plan.removePacks.Has(blob.PackID) || plan.repackPacks.Has(blob.PackID) { packID := blob.PackID()
if plan.removePacks.Has(packID) || plan.repackPacks.Has(packID) {
return return
} }
keepBlobs.Delete(blob.BlobHandle) keepBlobs.Delete(blob.Handle())
}) })
if err != nil { if err != nil {
return nil, err return nil, err
@@ -179,8 +180,8 @@ func packInfoFromIndex(ctx context.Context, idx restic.ListBlobser, usedBlobs *i
// iterate over all blobs in index to find out which blobs are duplicates // iterate over all blobs in index to find out which blobs are duplicates
// The counter in usedBlobs describes how many instances of the blob exist in the repository index // The counter in usedBlobs describes how many instances of the blob exist in the repository index
// Thus 0 == blob is missing, 1 == blob exists once, >= 2 == duplicates exist // Thus 0 == blob is missing, 1 == blob exists once, >= 2 == duplicates exist
err := idx.ListBlobs(ctx, func(blob restic.PackedBlob) { err := idx.ListBlobs(ctx, func(blob restic.PackBlob) {
bh := blob.BlobHandle bh := blob.Handle()
count, ok := usedBlobs.Get(bh) count, ok := usedBlobs.Get(bh)
if ok { if ok {
if count < math.MaxUint8 { if count < math.MaxUint8 {
@@ -229,21 +230,24 @@ func packInfoFromIndex(ctx context.Context, idx restic.ListBlobser, usedBlobs *i
hasDuplicates := false hasDuplicates := false
// iterate over all blobs in index to generate packInfo // iterate over all blobs in index to generate packInfo
err = idx.ListBlobs(ctx, func(blob restic.PackedBlob) { err = idx.ListBlobs(ctx, func(blob restic.PackBlob) {
ip := indexPack[blob.PackID] packID := blob.PackID()
h := blob.Handle()
ip := indexPack[packID]
// Set blob type if not yet set // Set blob type if not yet set
if ip.tpe == restic.NumBlobTypes { if ip.tpe == restic.NumBlobTypes {
ip.tpe = blob.Type ip.tpe = h.Type
} }
// mark mixed packs with "Invalid blob type" // mark mixed packs with "Invalid blob type"
if ip.tpe != blob.Type { if ip.tpe != h.Type {
ip.tpe = restic.InvalidBlob ip.tpe = restic.InvalidBlob
} }
bh := blob.BlobHandle bh := h
size := uint64(blob.Length) size := uint64(blob.CiphertextLength())
dupCount, _ := usedBlobs.Get(bh) dupCount, _ := usedBlobs.Get(bh)
switch { switch {
case dupCount >= 2: case dupCount >= 2:
@@ -273,7 +277,7 @@ func packInfoFromIndex(ctx context.Context, idx restic.ListBlobser, usedBlobs *i
ip.uncompressed = true ip.uncompressed = true
} }
// update indexPack // update indexPack
indexPack[blob.PackID] = ip indexPack[packID] = ip
}) })
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
@@ -287,8 +291,9 @@ func packInfoFromIndex(ctx context.Context, idx restic.ListBlobser, usedBlobs *i
// - if there are no used blobs in a pack, possibly mark duplicates as "unused" // - if there are no used blobs in a pack, possibly mark duplicates as "unused"
if hasDuplicates { if hasDuplicates {
// iterate again over all blobs in index (this is pretty cheap, all in-mem) // iterate again over all blobs in index (this is pretty cheap, all in-mem)
err = idx.ListBlobs(ctx, func(blob restic.PackedBlob) { err = idx.ListBlobs(ctx, func(blob restic.PackBlob) {
bh := blob.BlobHandle packID := blob.PackID()
bh := blob.Handle()
count, ok := usedBlobs.Get(bh) count, ok := usedBlobs.Get(bh)
// skip non-duplicate, aka. normal blobs // skip non-duplicate, aka. normal blobs
// count == 0 is used to mark that this was a duplicate blob with only a single occurrence remaining // count == 0 is used to mark that this was a duplicate blob with only a single occurrence remaining
@@ -296,8 +301,8 @@ func packInfoFromIndex(ctx context.Context, idx restic.ListBlobser, usedBlobs *i
return return
} }
ip := indexPack[blob.PackID] ip := indexPack[packID]
size := uint64(blob.Length) size := uint64(blob.CiphertextLength())
switch { switch {
case ip.usedBlobs > 0, ip.duplicateBlobs == ip.unusedBlobs, count == 0: case ip.usedBlobs > 0, ip.duplicateBlobs == ip.unusedBlobs, count == 0:
// other used blobs in pack, only duplicate blobs or "last" occurrence -> transition to used // other used blobs in pack, only duplicate blobs or "last" occurrence -> transition to used
@@ -325,7 +330,7 @@ func packInfoFromIndex(ctx context.Context, idx restic.ListBlobser, usedBlobs *i
usedBlobs.Set(bh, count) usedBlobs.Set(bh, count)
} }
// update indexPack // update indexPack
indexPack[blob.PackID] = ip indexPack[packID] = ip
}) })
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
+11 -10
View File
@@ -16,8 +16,8 @@ import (
func listBlobs(repo restic.Repository) restic.BlobSet { func listBlobs(repo restic.Repository) restic.BlobSet {
blobs := restic.NewBlobSet() blobs := restic.NewBlobSet()
_ = repo.ListBlobs(context.TODO(), func(pb restic.PackedBlob) { _ = repo.ListBlobs(context.TODO(), func(pb restic.PackBlob) {
blobs.Insert(pb.BlobHandle) blobs.Insert(pb.Handle())
}) })
return blobs return blobs
} }
@@ -66,11 +66,12 @@ func testRepairBrokenPack(t *testing.T, version uint) {
// find blob that starts at offset 0 // find blob that starts at offset 0
var damagedBlob restic.BlobHandle var damagedBlob restic.BlobHandle
_ = repo.ListBlobs(context.TODO(), func(pb restic.PackedBlob) { for _, blob := range repository.BlobsInPack(repo, damagedID) {
if pb.PackID == damagedID && pb.Offset == 0 { if blob.Offset == 0 {
damagedBlob = pb.BlobHandle damagedBlob = blob.BlobHandle
break
} }
}) }
return restic.NewIDSet(damagedID), restic.NewBlobSet(damagedBlob) return restic.NewIDSet(damagedID), restic.NewBlobSet(damagedBlob)
}, },
@@ -87,11 +88,11 @@ func testRepairBrokenPack(t *testing.T, version uint) {
// all blobs in the file are broken // all blobs in the file are broken
damagedBlobs := restic.NewBlobSet() damagedBlobs := restic.NewBlobSet()
_ = repo.ListBlobs(context.TODO(), func(pb restic.PackedBlob) { rtest.OK(t, repo.ListBlobs(context.TODO(), func(pb restic.PackBlob) {
if pb.PackID == damagedID { if pb.PackID().Equal(damagedID) {
damagedBlobs.Insert(pb.BlobHandle) damagedBlobs.Insert(pb.Handle())
} }
}) }))
return restic.NewIDSet(damagedID), damagedBlobs return restic.NewIDSet(damagedID), damagedBlobs
}, },
}, { }, {
+2 -2
View File
@@ -683,12 +683,12 @@ func (r *Repository) LookupBlobSize(tpe restic.BlobType, id restic.ID) (uint, bo
// ListBlobs runs fn on all blobs known to the index. When the context is cancelled, // ListBlobs runs fn on all blobs known to the index. When the context is cancelled,
// the index iteration returns immediately with ctx.Err(). This blocks any modification of the index. // the index iteration returns immediately with ctx.Err(). This blocks any modification of the index.
func (r *Repository) ListBlobs(ctx context.Context, fn func(restic.PackedBlob)) error { func (r *Repository) ListBlobs(ctx context.Context, fn func(restic.PackBlob)) error {
for blob := range r.idx.Values() { for blob := range r.idx.Values() {
if ctx.Err() != nil { if ctx.Err() != nil {
return ctx.Err() return ctx.Err()
} }
fn(blob) fn(restic.AsPackBlob(blob))
} }
return nil return nil
} }
+2 -2
View File
@@ -30,7 +30,7 @@ type Repository interface {
NewAssociatedBlobSet() AssociatedBlobSet NewAssociatedBlobSet() AssociatedBlobSet
// ListBlobs runs fn on all blobs known to the index. When the context is cancelled, // ListBlobs runs fn on all blobs known to the index. When the context is cancelled,
// the index iteration returns immediately with ctx.Err(). This blocks any modification of the index. // the index iteration returns immediately with ctx.Err(). This blocks any modification of the index.
ListBlobs(ctx context.Context, fn func(PackedBlob)) error ListBlobs(ctx context.Context, fn func(PackBlob)) error
// ListPackHandles returns the blob handles stored in the pack file header. // ListPackHandles returns the blob handles stored in the pack file header.
ListPackHandles(ctx context.Context, id ID, packSize int64) ([]BlobHandle, error) ListPackHandles(ctx context.Context, id ID, packSize int64) ([]BlobHandle, error)
@@ -152,7 +152,7 @@ type Unpacked[FT FileTypes] interface {
} }
type ListBlobser interface { type ListBlobser interface {
ListBlobs(ctx context.Context, fn func(PackedBlob)) error ListBlobs(ctx context.Context, fn func(PackBlob)) error
} }
type BlobLoader interface { type BlobLoader interface {