91 lines
2.5 KiB
Go
91 lines
2.5 KiB
Go
package storage
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
"strings"
|
|
)
|
|
|
|
// ReadLimitError reports that a remote object exceeded its caller-owned read
|
|
// limit. The limit is enforced against both available object metadata and the
|
|
// bytes returned by the opened object body.
|
|
type ReadLimitError struct {
|
|
Key string
|
|
Limit int64
|
|
Observed int64
|
|
}
|
|
|
|
func (e *ReadLimitError) Error() string {
|
|
return fmt.Sprintf("object %q exceeds %d-byte read limit (observed at least %d bytes)", e.Key, e.Limit, e.Observed)
|
|
}
|
|
|
|
// ReadObjectBounded opens one object version and retains at most maxBytes of
|
|
// its content. Object metadata may reject an oversized body early, but a
|
|
// limit-plus-one read always enforces the boundary when transfer begins.
|
|
func ReadObjectBounded(ctx context.Context, store ObjectStore, key string, maxBytes int64) (info ObjectInfo, data []byte, err error) {
|
|
key = strings.TrimSpace(key)
|
|
if store == nil {
|
|
return ObjectInfo{}, nil, fmt.Errorf("read bounded object: store is required")
|
|
}
|
|
if key == "" {
|
|
return ObjectInfo{}, nil, fmt.Errorf("read bounded object: key is required")
|
|
}
|
|
if maxBytes <= 0 || maxBytes == math.MaxInt64 {
|
|
return ObjectInfo{}, nil, fmt.Errorf("read bounded object %q: limit must be between 1 and %d bytes", key, int64(math.MaxInt64-1))
|
|
}
|
|
if err := ctx.Err(); err != nil {
|
|
return ObjectInfo{}, nil, err
|
|
}
|
|
|
|
info, body, err := store.Read(ctx, key)
|
|
if err != nil {
|
|
return ObjectInfo{}, nil, err
|
|
}
|
|
if body == nil {
|
|
return ObjectInfo{}, nil, fmt.Errorf("read bounded object %q: store returned no body", key)
|
|
}
|
|
defer func() {
|
|
if closeErr := body.Close(); closeErr != nil {
|
|
data = nil
|
|
err = errors.Join(err, fmt.Errorf("close object %q: %w", key, closeErr))
|
|
}
|
|
}()
|
|
|
|
if info.Size > maxBytes {
|
|
return info, nil, &ReadLimitError{Key: key, Limit: maxBytes, Observed: info.Size}
|
|
}
|
|
|
|
data, err = io.ReadAll(io.LimitReader(contextReader{ctx: ctx, reader: body}, maxBytes+1))
|
|
if err != nil {
|
|
return info, nil, err
|
|
}
|
|
if err := ctx.Err(); err != nil {
|
|
return info, nil, err
|
|
}
|
|
if int64(len(data)) > maxBytes {
|
|
return info, nil, &ReadLimitError{Key: key, Limit: maxBytes, Observed: int64(len(data))}
|
|
}
|
|
return info, data, nil
|
|
}
|
|
|
|
type contextReader struct {
|
|
ctx context.Context
|
|
reader io.Reader
|
|
}
|
|
|
|
func (r contextReader) Read(p []byte) (int, error) {
|
|
if err := r.ctx.Err(); err != nil {
|
|
return 0, err
|
|
}
|
|
n, err := r.reader.Read(p)
|
|
if err == nil {
|
|
if contextErr := r.ctx.Err(); contextErr != nil {
|
|
return n, contextErr
|
|
}
|
|
}
|
|
return n, err
|
|
}
|