Report an unreadable blob once, without its bytes (closes #424)
check / check (push) Successful in 9m10s

blobgen.NewReader returns ErrNotAgeStream when age cannot open the
data, in place of age's error, which quotes the data's first bytes. A
failed read of the header is still returned as the read error, and a
wrong key as age.NoIdentityMatchError.

Under --skip-errors, restore reports a blob that could not be
downloaded once, with the number of files that need it. Those files
are counted in the final warning, not listed; each is logged at debug
level.

Model: opus-5-5
This commit is contained in:
sneak committed 2026-11-11 03:09:49 +00:00
1 parent 0d0c76fa24
commit 80289ea14d
4 files changed
+217 -29

No files matched your search

+39 -3
View File
@@ -11,6 +11,11 @@ import (
"github.com/klauspost/compress/zstd"
)
// ErrNotAgeStream is returned by NewReader when the data cannot be opened
// as an age stream, for example because its header is malformed. It
// replaces age's own error, which can quote the data's first bytes.
var ErrNotAgeStream = errors.New("not a valid age stream")
// Reader wraps decompression and decryption with SHA256 verification
type Reader struct {
reader io.Reader
@@ -24,11 +29,25 @@ type Reader struct {
// NewReader creates a new Reader that decrypts, decompresses, and verifies
// data. Every supplied identity is offered to age.Decrypt, so a blob
// encrypted to any one of them can be read.
//
// A failure to read r is returned as that error, and a blob encrypted to
// none of the identities as age.NoIdentityMatchError. Any other failure
// to open the age stream is ErrNotAgeStream.
func NewReader(r io.Reader, identities ...age.Identity) (*Reader, error) {
// Create decryption reader
decReader, err := age.Decrypt(r, identities...)
src := &readErrorRecorder{reader: r}
decReader, err := age.Decrypt(src, identities...)
if err != nil {
return nil, fmt.Errorf("creating decryption reader: %w", err)
var noMatch *age.NoIdentityMatchError
switch {
case src.err != nil:
return nil, fmt.Errorf("reading age header: %w", src.err)
case errors.As(err, &noMatch):
return nil, fmt.Errorf("creating decryption reader: %w", err)
default:
return nil, ErrNotAgeStream
}
}
// Create decompression reader
@@ -52,6 +71,23 @@ func NewReader(r io.Reader, identities ...age.Identity) (*Reader, error) {
}, nil
}
// readErrorRecorder passes reads through to reader and keeps the first
// error other than io.EOF, so NewReader can tell a failed read from bad
// data.
type readErrorRecorder struct {
reader io.Reader
err error
}
func (r *readErrorRecorder) Read(p []byte) (int, error) {
n, err := r.reader.Read(p)
if err != nil && !errors.Is(err, io.EOF) && r.err == nil {
r.err = err
}
return n, err
}
// Read implements io.Reader
func (r *Reader) Read(p []byte) (int, error) {
n, err := r.teeReader.Read(p)