From 2c07f7fff319029d31ab9310a0475b8760db0d9a Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Sat, 7 May 2022 14:33:53 +0200 Subject: [PATCH] stats: hardlinks only reduce restore within a snapshot The `stats` command checks inodes to not count hardlinked files multiple times into the restore size. This check applies across all snapshots and not only within snapshots. As a result the result size was far too low when calculating it for multiple snapshots and it would vary depending on the order in which snapshots were listed. --- changelog/unreleased/issue-3736 | 8 ++++++++ cmd/restic/cmd_stats.go | 14 +++++--------- 2 files changed, 13 insertions(+), 9 deletions(-) create mode 100644 changelog/unreleased/issue-3736 diff --git a/changelog/unreleased/issue-3736 b/changelog/unreleased/issue-3736 new file mode 100644 index 000000000..bb14192c4 --- /dev/null +++ b/changelog/unreleased/issue-3736 @@ -0,0 +1,8 @@ +Bugfix: `stats` fix restore size calculation for multiple snapshots + +Since restic 0.10.0 the restore size calculated by the `stats` command for +multiple snapshots was too low. The hardlink detection was accidentally applied +across multiple snapshots and thus ignored many files. This has been fixed. + +https://github.com/restic/restic/issues/3736 +https://github.com/restic/restic/pull/3740 diff --git a/cmd/restic/cmd_stats.go b/cmd/restic/cmd_stats.go index 59dc75852..b8dcac2a3 100644 --- a/cmd/restic/cmd_stats.go +++ b/cmd/restic/cmd_stats.go @@ -111,7 +111,6 @@ func runStats(gopts GlobalOptions, args []string) error { // create a container for the stats (and other needed state) stats := &statsContainer{ uniqueFiles: make(map[fileID]struct{}), - uniqueInodes: make(map[uint64]struct{}), fileBlobs: make(map[string]restic.IDSet), blobs: restic.NewBlobSet(), snapshotsCount: 0, @@ -175,7 +174,8 @@ func statsWalkSnapshot(ctx context.Context, snapshot *restic.Snapshot, repo rest return restic.FindUsedBlobs(ctx, repo, restic.IDs{*snapshot.Tree}, stats.blobs, nil) } - err := walker.Walk(ctx, repo, *snapshot.Tree, restic.NewIDSet(), statsWalkTree(repo, stats)) + uniqueInodes := make(map[uint64]struct{}) + err := walker.Walk(ctx, repo, *snapshot.Tree, restic.NewIDSet(), statsWalkTree(repo, stats, uniqueInodes)) if err != nil { return fmt.Errorf("walking tree %s: %v", *snapshot.Tree, err) } @@ -183,7 +183,7 @@ func statsWalkSnapshot(ctx context.Context, snapshot *restic.Snapshot, repo rest return nil } -func statsWalkTree(repo restic.Repository, stats *statsContainer) walker.WalkFunc { +func statsWalkTree(repo restic.Repository, stats *statsContainer, uniqueInodes map[uint64]struct{}) walker.WalkFunc { return func(parentTreeID restic.ID, npath string, node *restic.Node, nodeErr error) (bool, error) { if nodeErr != nil { return true, nodeErr @@ -242,8 +242,8 @@ func statsWalkTree(repo restic.Repository, stats *statsContainer) walker.WalkFun // if inodes are present, only count each inode once // (hard links do not increase restore size) - if _, ok := stats.uniqueInodes[node.Inode]; !ok || node.Inode == 0 { - stats.uniqueInodes[node.Inode] = struct{}{} + if _, ok := uniqueInodes[node.Inode]; !ok || node.Inode == 0 { + uniqueInodes[node.Inode] = struct{}{} stats.TotalSize += node.Size } @@ -290,10 +290,6 @@ type statsContainer struct { // contents (hashed sequence of content blob IDs) uniqueFiles map[fileID]struct{} - // uniqueInodes marks visited files according to their - // inode # (hashed sequence of inode numbers) - uniqueInodes map[uint64]struct{} - // fileBlobs maps a file name (path) to the set of // blobs that have been seen as a part of the file fileBlobs map[string]restic.IDSet