mirror of
https://github.com/restic/restic.git
synced 2026-07-02 21:44:17 +00:00
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:
committed by
GitHub
parent
e4056e70a8
commit
75de8b54e6
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user