Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Retry loading of corrupted data from backend / cache #4800

Merged
merged 25 commits into from
May 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
47232bf
backend: move LimitReadCloser to util package
MichaelEischer Apr 25, 2024
1d6d365
repository: move backend.LoadAll to repository.LoadRaw
MichaelEischer May 9, 2024
779c8d3
debug/repair packs/upgrade repo v2: use repository.LoadRaw
MichaelEischer May 9, 2024
021fb49
repository: Implement repository.LoadUnpacked using LoadRaw
MichaelEischer May 8, 2024
6563f1d
repository: remove redundant debug log
MichaelEischer May 8, 2024
503c814
repository: unify blob decoding code
MichaelEischer May 8, 2024
e939035
cache: code cleanups
MichaelEischer May 9, 2024
2ace242
repository: make reloading broken files explicit
MichaelEischer May 9, 2024
e33ce7f
repository: retry failed LoadBlob once
MichaelEischer May 9, 2024
7017adb
repository: retry failed ListPack once
MichaelEischer May 9, 2024
e401af0
check: fix error message formatting
MichaelEischer May 9, 2024
433a6aa
repository: remove redundant blob loading fallback from RepairPacks
MichaelEischer May 9, 2024
8cce06d
repair packs: drop experimental warning
MichaelEischer May 9, 2024
97a307d
cache: Always use cached file if it exists
MichaelEischer May 9, 2024
e734746
cache: forget cached file at most once
MichaelEischer May 9, 2024
385cee0
repository: fix caching of tree packs in LoadBlobsFromPack
MichaelEischer May 9, 2024
3ff063e
check: verify pack a second time if broken
MichaelEischer May 9, 2024
5214af8
cache: test forget behavior
MichaelEischer May 9, 2024
ac805d6
cache: cleanup debug logs
MichaelEischer May 9, 2024
4f45668
repository: rework and extend LoadRaw tests
MichaelEischer May 9, 2024
bf16096
repository: test LoadBlob retries
MichaelEischer May 9, 2024
987c3b2
repository: test retries of ListPack
MichaelEischer May 9, 2024
ff0744b
check: test checkPack retries
MichaelEischer May 9, 2024
8f8d872
fix compatibility with go 1.19
MichaelEischer May 9, 2024
74d9065
check: use ReadFull to load pack header in checkPack
MichaelEischer May 10, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions cmd/restic/cmd_cat.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (

"github.com/spf13/cobra"

"github.com/restic/restic/internal/backend"
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/repository"
"github.com/restic/restic/internal/restic"
Expand Down Expand Up @@ -146,9 +145,9 @@ func runCat(ctx context.Context, gopts GlobalOptions, args []string) error {
return nil

case "pack":
h := backend.Handle{Type: restic.PackFile, Name: id.String()}
buf, err := backend.LoadAll(ctx, nil, repo.Backend(), h)
if err != nil {
buf, err := repo.LoadRaw(ctx, restic.PackFile, id)
// allow returning broken pack files
if buf == nil {
return err
}

Expand Down
28 changes: 11 additions & 17 deletions cmd/restic/cmd_debug.go
Original file line number Diff line number Diff line change
Expand Up @@ -316,10 +316,11 @@ func loadBlobs(ctx context.Context, opts DebugExamineOptions, repo restic.Reposi
if err != nil {
panic(err)
}
be := repo.Backend()
h := backend.Handle{
Name: packID.String(),
Type: restic.PackFile,

pack, err := repo.LoadRaw(ctx, restic.PackFile, packID)
// allow processing broken pack files
if pack == nil {
return err
}

wg, ctx := errgroup.WithContext(ctx)
Expand All @@ -331,19 +332,11 @@ func loadBlobs(ctx context.Context, opts DebugExamineOptions, repo restic.Reposi
wg.Go(func() error {
for _, blob := range list {
Printf(" loading blob %v at %v (length %v)\n", blob.ID, blob.Offset, blob.Length)
buf := make([]byte, blob.Length)
err := be.Load(ctx, h, int(blob.Length), int64(blob.Offset), func(rd io.Reader) error {
n, err := io.ReadFull(rd, buf)
if err != nil {
return fmt.Errorf("read error after %d bytes: %v", n, err)
}
return nil
})
if err != nil {
Warnf("error read: %v\n", err)
if int(blob.Offset+blob.Length) > len(pack) {
Warnf("skipping truncated blob\n")
continue
}

buf := pack[blob.Offset : blob.Offset+blob.Length]
key := repo.Key()

nonce, plaintext := buf[:key.NonceSize()], buf[key.NonceSize():]
Expand Down Expand Up @@ -492,8 +485,9 @@ func examinePack(ctx context.Context, opts DebugExamineOptions, repo restic.Repo
}
Printf(" file size is %v\n", fi.Size)

buf, err := backend.LoadAll(ctx, nil, repo.Backend(), h)
if err != nil {
buf, err := repo.LoadRaw(ctx, restic.PackFile, id)
// also process damaged pack files
if buf == nil {
return err
}
gotID := restic.Hash(buf)
Expand Down
21 changes: 8 additions & 13 deletions cmd/restic/cmd_repair_packs.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package main

import (
"bytes"
"context"
"io"
"os"

"github.com/restic/restic/internal/backend"
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/repository"
"github.com/restic/restic/internal/restic"
Expand All @@ -17,8 +17,6 @@ var cmdRepairPacks = &cobra.Command{
Use: "packs [packIDs...]",
Short: "Salvage damaged pack files",
Long: `
WARNING: The CLI for this command is experimental and will likely change in the future!

The "repair packs" command extracts intact blobs from the specified pack files, rebuilds
the index to remove the damaged pack files and removes the pack files from the repository.

Expand Down Expand Up @@ -68,20 +66,17 @@ func runRepairPacks(ctx context.Context, gopts GlobalOptions, term *termstatus.T

printer.P("saving backup copies of pack files to current folder")
for id := range ids {
f, err := os.OpenFile("pack-"+id.String(), os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o666)
if err != nil {
buf, err := repo.LoadRaw(ctx, restic.PackFile, id)
// corrupted data is fine
if buf == nil {
return err
}

err = repo.Backend().Load(ctx, backend.Handle{Type: restic.PackFile, Name: id.String()}, 0, 0, func(rd io.Reader) error {
_, err := f.Seek(0, 0)
if err != nil {
return err
}
_, err = io.Copy(f, rd)
return err
})
f, err := os.OpenFile("pack-"+id.String(), os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o666)
if err != nil {
return err
}
if _, err := io.Copy(f, bytes.NewReader(buf)); err != nil {
_ = f.Close()
return err
}
Expand Down
2 changes: 1 addition & 1 deletion internal/backend/local/local.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ func (b *Local) openReader(_ context.Context, h backend.Handle, length int, offs
}

if length > 0 {
return backend.LimitReadCloser(f, int64(length)), nil
return util.LimitReadCloser(f, int64(length)), nil
}

return f, nil
Expand Down
4 changes: 2 additions & 2 deletions internal/backend/sftp/sftp.go
Original file line number Diff line number Diff line change
Expand Up @@ -437,7 +437,7 @@ func (r *SFTP) Load(ctx context.Context, h backend.Handle, length int, offset in

// check the underlying reader to be agnostic to however fn() handles the returned error
_, rderr := rd.Read([]byte{0})
if rderr == io.EOF && rd.(*backend.LimitedReadCloser).N != 0 {
if rderr == io.EOF && rd.(*util.LimitedReadCloser).N != 0 {
// file is too short
return fmt.Errorf("%w: %v", errTooShort, err)
}
Expand All @@ -463,7 +463,7 @@ func (r *SFTP) openReader(_ context.Context, h backend.Handle, length int, offse
if length > 0 {
// unlimited reads usually use io.Copy which needs WriteTo support at the underlying reader
// limited reads are usually combined with io.ReadFull which reads all required bytes into a buffer in one go
return backend.LimitReadCloser(f, int64(length)), nil
return util.LimitReadCloser(f, int64(length)), nil
}

return f, nil
Expand Down
21 changes: 17 additions & 4 deletions internal/backend/test/tests.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,19 @@ func beTest(ctx context.Context, be backend.Backend, h backend.Handle) (bool, er
return err == nil, err
}

func LoadAll(ctx context.Context, be backend.Backend, h backend.Handle) ([]byte, error) {
var buf []byte
err := be.Load(ctx, h, 0, 0, func(rd io.Reader) error {
var err error
buf, err = io.ReadAll(rd)
return err
})
if err != nil {
return nil, err
}
return buf, nil
}

// TestStripPasswordCall tests that the StripPassword method of a factory can be called without crashing.
// It does not verify whether passwords are removed correctly
func (s *Suite[C]) TestStripPasswordCall(_ *testing.T) {
Expand Down Expand Up @@ -94,7 +107,7 @@ func (s *Suite[C]) TestConfig(t *testing.T) {
var testString = "Config"

// create config and read it back
_, err := backend.LoadAll(context.TODO(), nil, b, backend.Handle{Type: backend.ConfigFile})
_, err := LoadAll(context.TODO(), b, backend.Handle{Type: backend.ConfigFile})
if err == nil {
t.Fatalf("did not get expected error for non-existing config")
}
Expand All @@ -110,7 +123,7 @@ func (s *Suite[C]) TestConfig(t *testing.T) {
// same config
for _, name := range []string{"", "foo", "bar", "0000000000000000000000000000000000000000000000000000000000000000"} {
h := backend.Handle{Type: backend.ConfigFile, Name: name}
buf, err := backend.LoadAll(context.TODO(), nil, b, h)
buf, err := LoadAll(context.TODO(), b, h)
if err != nil {
t.Fatalf("unable to read config with name %q: %+v", name, err)
}
Expand Down Expand Up @@ -519,7 +532,7 @@ func (s *Suite[C]) TestSave(t *testing.T) {
err := b.Save(context.TODO(), h, backend.NewByteReader(data, b.Hasher()))
test.OK(t, err)

buf, err := backend.LoadAll(context.TODO(), nil, b, h)
buf, err := LoadAll(context.TODO(), b, h)
test.OK(t, err)
if len(buf) != len(data) {
t.Fatalf("number of bytes does not match, want %v, got %v", len(data), len(buf))
Expand Down Expand Up @@ -821,7 +834,7 @@ func (s *Suite[C]) TestBackend(t *testing.T) {

// test Load()
h := backend.Handle{Type: tpe, Name: ts.id}
buf, err := backend.LoadAll(context.TODO(), nil, b, h)
buf, err := LoadAll(context.TODO(), b, h)
test.OK(t, err)
test.Equals(t, ts.data, string(buf))

Expand Down
15 changes: 15 additions & 0 deletions internal/backend/util/limited_reader.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package util

import "io"

// LimitedReadCloser wraps io.LimitedReader and exposes the Close() method.
type LimitedReadCloser struct {
io.Closer
io.LimitedReader
}

// LimitReadCloser returns a new reader wraps r in an io.LimitedReader, but also
// exposes the Close() method.
func LimitReadCloser(r io.ReadCloser, n int64) *LimitedReadCloser {
return &LimitedReadCloser{Closer: r, LimitedReader: io.LimitedReader{R: r, N: n}}
}
76 changes: 0 additions & 76 deletions internal/backend/utils.go

This file was deleted.

Loading
Loading