restic check and restic repair packs: treat missing packfiles the same as damaged and truncated packfiles (#21845)

Co-authored-by: Michael Eischer <michael.eischer@fau.de>
This commit is contained in:
Winfried Plappert
2026-06-28 11:09:42 +01:00
committed by GitHub
parent e4056e70a8
commit 75de8b54e6
11 changed files with 122 additions and 21 deletions
+1 -1
View File
@@ -313,7 +313,7 @@ func runCheck(ctx context.Context, opts CheckOptions, gopts global.Options, args
orphanedPacks++
printer.V("%v\n", err)
} else {
if packErr.Truncated {
if packErr.Truncated || packErr.Missing {
salvagePacks.Insert(packErr.ID)
}
errorsFound = true
+9 -8
View File
@@ -11,35 +11,36 @@ import (
func testRunCheck(t testing.TB, gopts global.Options) {
t.Helper()
output, err := testRunCheckOutput(t, gopts, true)
stdout, stderr, err := testRunCheckOutput(t, gopts, true)
if err != nil {
t.Error(output)
t.Error(stdout)
t.Error(stderr)
t.Fatalf("unexpected error: %+v", err)
}
}
func testRunCheckMustFail(t testing.TB, gopts global.Options) {
t.Helper()
_, err := testRunCheckOutput(t, gopts, false)
_, _, err := testRunCheckOutput(t, gopts, false)
rtest.Assert(t, err != nil, "expected non nil error after check of damaged repository")
}
func testRunCheckOutput(t testing.TB, gopts global.Options, checkUnused bool) (string, error) {
buf, err := withCaptureStdout(t, gopts, func(ctx context.Context, gopts global.Options) error {
func testRunCheckOutput(t testing.TB, gopts global.Options, checkUnused bool) (string, string, error) {
stdout, stderr, err := withCaptureStdoutStderr(t, gopts, func(ctx context.Context, gopts global.Options) error {
opts := CheckOptions{
ReadData: true,
CheckUnused: checkUnused,
}
_, err := runCheck(context.TODO(), opts, gopts, nil, gopts.Term)
_, err := runCheck(ctx, opts, gopts, nil, gopts.Term)
return err
})
return buf.String(), err
return stdout.String(), stderr.String(), err
}
func testRunCheckOutputWithOpts(t testing.TB, gopts global.Options, opts CheckOptions, args []string) (string, error) {
buf, err := withCaptureStdout(t, gopts, func(ctx context.Context, gopts global.Options) error {
gopts.Verbosity = 2
_, err := runCheck(context.TODO(), opts, gopts, args, gopts.Term)
_, err := runCheck(ctx, opts, gopts, args, gopts.Term)
return err
})
return buf.String(), err
@@ -31,7 +31,7 @@ func testRebuildIndex(t *testing.T, backendTestHook global.BackendWrapper) {
datafile := filepath.Join("..", "..", "internal", "checker", "testdata", "duplicate-packs-in-index-test-repo.tar.gz")
rtest.SetupTarTestFixture(t, env.base, datafile)
out, err := testRunCheckOutput(t, env.gopts, false)
out, _, err := testRunCheckOutput(t, env.gopts, false)
if !strings.Contains(out, "contained in several indexes") {
t.Fatalf("did not find checker hint for packs in several indexes")
}
@@ -48,7 +48,7 @@ func testRebuildIndex(t *testing.T, backendTestHook global.BackendWrapper) {
testRunRebuildIndex(t, env.gopts)
env.gopts.BackendTestHook = nil
out, err = testRunCheckOutput(t, env.gopts, false)
out, _, err = testRunCheckOutput(t, env.gopts, false)
if len(out) != 0 {
t.Fatalf("expected no output from the checker, got: %v", out)
}
+7 -3
View File
@@ -69,9 +69,13 @@ func runRepairPacks(ctx context.Context, gopts global.Options, term ui.Terminal,
printer.P("saving backup copies of pack files to current folder")
for id := range ids {
buf, err := repo.LoadRaw(ctx, restic.PackFile, id)
// corrupted data is fine
if buf == nil {
return err
if ctx.Err() != nil {
return ctx.Err()
}
// only skip creating a local copy if no data at all could be loaded
if err != nil && buf == nil {
printer.E("will remove packfile %v due to failed download: %v", id, err)
continue
}
f, err := os.OpenFile("pack-"+id.String(), os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o666)
@@ -0,0 +1,75 @@
package main
import (
"context"
"os"
"path/filepath"
"strings"
"testing"
"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/progress"
)
// testRunRepairPacks runs `restic repair packs` with capturing stdout and stderr
func testRunRepairPacks(t testing.TB, gopts global.Options, args []string) (string, string, error) {
bufStdout, bufStderr, err := withCaptureStdoutStderr(t, gopts, func(ctx context.Context, gopts global.Options) error {
return runRepairPacks(ctx, gopts, gopts.Term, args)
})
return bufStdout.String(), bufStderr.String(), err
}
func TestRunRepairPackfiles(t *testing.T) {
env, cleanup := withTestEnvironment(t)
defer cleanup()
testSetupBackupData(t, env)
// backup of subtree 0/0/9/42
testRunBackup(t, env.testdata, []string{filepath.Join(env.testdata, "0", "0", "9", "42")}, BackupOptions{}, env.gopts)
testListSnapshots(t, env.gopts, 1)
packfileID := restic.ID{}
err := withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
printer := progress.NewTerminalPrinter(false, gopts.Verbosity, gopts.Term)
_, repo, unlock, err := openWithReadLock(ctx, gopts, false, printer)
rtest.OK(t, err)
defer unlock()
rtest.OK(t, repo.LoadIndex(ctx, printer))
// load packfiles from master index
err = repo.ListBlobs(ctx, func(blob restic.PackBlob) {
if blob.Handle().Type == restic.DataBlob {
packfileID = blob.PackID()
return
}
})
rtest.OK(t, err)
return nil
})
rtest.OK(t, err)
rtest.Assert(t, !packfileID.IsNull(), "expected valid packfile ID")
packIDString := packfileID.String()
filename := filepath.Join(env.gopts.Repo, "data", packIDString[0:2], packIDString)
rtest.OK(t, os.Remove(filename))
_, outError, err := testRunCheckOutput(t, env.gopts, false)
rtest.Assert(t, err != nil, "expected check errors, got none")
rtest.Assert(t, strings.Contains(string(outError), packIDString), "expected mention of %q", packIDString)
// change to temporary directory to not pollute the repository with backup files
cleanupChdir := rtest.Chdir(t, env.base)
defer cleanupChdir()
// restic repair packs 'packIDString'
_, _, err = testRunRepairPacks(t, env.gopts, []string{packIDString})
rtest.OK(t, err)
// run restic repair snapshots --forget
testRunRepairSnapshot(t, env.gopts, true)
_, _, err = testRunCheckOutput(t, env.gopts, false)
rtest.OK(t, err)
}
@@ -69,7 +69,7 @@ func TestRepairSnapshotsWithLostData(t *testing.T) {
// repository must be ok after removing the broken snapshots
testRunForget(t, env.gopts, ForgetOptions{}, snapshotIDs[0].String(), snapshotIDs[1].String())
testListSnapshots(t, env.gopts, 2)
_, err := testRunCheckOutput(t, env.gopts, false)
_, _, err := testRunCheckOutput(t, env.gopts, false)
rtest.OK(t, err)
}
@@ -98,7 +98,7 @@ func TestRepairSnapshotsWithLostTree(t *testing.T) {
testRunRebuildIndex(t, env.gopts)
testRunRepairSnapshot(t, env.gopts, true)
testListSnapshots(t, env.gopts, 1)
_, err := testRunCheckOutput(t, env.gopts, false)
_, _, err := testRunCheckOutput(t, env.gopts, false)
rtest.OK(t, err)
}
@@ -121,7 +121,7 @@ func TestRepairSnapshotsWithLostRootTree(t *testing.T) {
testRunRebuildIndex(t, env.gopts)
testRunRepairSnapshot(t, env.gopts, true)
testListSnapshots(t, env.gopts, 0)
_, err := testRunCheckOutput(t, env.gopts, false)
_, _, err := testRunCheckOutput(t, env.gopts, false)
rtest.OK(t, err)
}
+7
View File
@@ -423,6 +423,13 @@ func withCaptureStdout(t testing.TB, gopts global.Options, callback func(ctx con
return buf, err
}
func withCaptureStdoutStderr(t testing.TB, gopts global.Options, callback func(ctx context.Context, gopts global.Options) error) (*bytes.Buffer, *bytes.Buffer, error) {
bufStdout := bytes.NewBuffer(nil)
bufStderr := bytes.NewBuffer(nil)
err := withTermStatusRaw(os.Stdin, bufStdout, bufStderr, gopts, callback)
return bufStdout, bufStderr, err
}
func withTermStatus(t testing.TB, gopts global.Options, callback func(ctx context.Context, gopts global.Options) error) error {
// stdout and stderr are written to by printer functions etc. That is the written data
// usually consists of one or multiple lines and therefore can be handled well