check / check (pull_request) Successful in 2m47s
S3Storer.Get and Stat returned the raw AWS SDK error for a missing object, so errors.Is(err, storage.ErrNotFound) was false on the s3 backend while the file and rclone backends honored the Storer contract. Callers that branch on ErrNotFound behaved differently per backend. Both now wrap ErrNotFound when the SDK reports a missing object, leaving every other error intact. The not-found detection is a small exported s3.IsNotFound helper, also used by HeadObject so the two share one definition. A test asserts a missing key maps to ErrNotFound on the s3 backend; it fails without the mapping. model: claude-opus-4-8
109 lines
2.6 KiB
Go
109 lines
2.6 KiB
Go
package storage
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
|
|
"sneak.berlin/go/vaultik/internal/s3"
|
|
)
|
|
|
|
// S3Storer wraps the existing s3.Client to implement Storer.
|
|
type S3Storer struct {
|
|
client *s3.Client
|
|
}
|
|
|
|
// NewS3Storer creates a new S3 storage backend.
|
|
func NewS3Storer(client *s3.Client) *S3Storer {
|
|
return &S3Storer{client: client}
|
|
}
|
|
|
|
// Put stores data at the specified key.
|
|
func (s *S3Storer) Put(ctx context.Context, key string, data io.Reader) error {
|
|
return s.client.PutObject(ctx, key, data)
|
|
}
|
|
|
|
// PutWithProgress stores data with progress reporting.
|
|
func (s *S3Storer) PutWithProgress(
|
|
ctx context.Context, key string, data io.Reader,
|
|
size int64, progress ProgressCallback,
|
|
) error {
|
|
// Convert storage.ProgressCallback to s3.ProgressCallback
|
|
var s3Progress s3.ProgressCallback
|
|
if progress != nil {
|
|
s3Progress = s3.ProgressCallback(progress)
|
|
}
|
|
|
|
return s.client.PutObjectWithProgress(ctx, key, data, size, s3Progress)
|
|
}
|
|
|
|
// Get retrieves data from the specified key.
|
|
// Returns ErrNotFound if the object does not exist.
|
|
func (s *S3Storer) Get(ctx context.Context, key string) (io.ReadCloser, error) {
|
|
rc, err := s.client.GetObject(ctx, key)
|
|
if err != nil {
|
|
if s3.IsNotFound(err) {
|
|
return nil, fmt.Errorf("get %q: %w", key, ErrNotFound)
|
|
}
|
|
|
|
return nil, err
|
|
}
|
|
|
|
return rc, nil
|
|
}
|
|
|
|
// Stat returns metadata about an object without retrieving its contents.
|
|
// Returns ErrNotFound if the object does not exist.
|
|
func (s *S3Storer) Stat(ctx context.Context, key string) (*ObjectInfo, error) {
|
|
info, err := s.client.StatObject(ctx, key)
|
|
if err != nil {
|
|
if s3.IsNotFound(err) {
|
|
return nil, fmt.Errorf("stat %q: %w", key, ErrNotFound)
|
|
}
|
|
|
|
return nil, err
|
|
}
|
|
|
|
return &ObjectInfo{
|
|
Key: info.Key,
|
|
Size: info.Size,
|
|
}, nil
|
|
}
|
|
|
|
// Delete removes an object.
|
|
func (s *S3Storer) Delete(ctx context.Context, key string) error {
|
|
return s.client.DeleteObject(ctx, key)
|
|
}
|
|
|
|
// List returns all keys with the given prefix.
|
|
func (s *S3Storer) List(ctx context.Context, prefix string) ([]string, error) {
|
|
return s.client.ListObjects(ctx, prefix)
|
|
}
|
|
|
|
// ListStream returns a channel of ObjectInfo for large result sets.
|
|
func (s *S3Storer) ListStream(ctx context.Context, prefix string) <-chan ObjectInfo {
|
|
ch := make(chan ObjectInfo)
|
|
|
|
go func() {
|
|
defer close(ch)
|
|
|
|
for info := range s.client.ListObjectsStream(ctx, prefix, false) {
|
|
ch <- ObjectInfo{
|
|
Key: info.Key,
|
|
Size: info.Size,
|
|
Err: info.Err,
|
|
}
|
|
}
|
|
}()
|
|
|
|
return ch
|
|
}
|
|
|
|
// Info returns human-readable storage location information.
|
|
func (s *S3Storer) Info() Info {
|
|
return Info{
|
|
Type: "s3",
|
|
Location: fmt.Sprintf("%s/%s", s.client.Endpoint(), s.client.BucketName()),
|
|
}
|
|
}
|