Confine publish archive reads

This commit is contained in:
2026-08-10 19:16:18 +00:00
parent 60cebf0e4b
commit 9900211fa4
18 changed files with 655 additions and 152 deletions

View File

@@ -142,7 +142,34 @@ func (f *FakeBackend) Upload(ctx context.Context, localPath, key string, opts Up
return ObjectInfo{}, fmt.Errorf("upload object: key is required")
}
data, err := os.ReadFile(localPath)
file, err := os.Open(localPath)
if err != nil {
return ObjectInfo{}, fmt.Errorf("upload object %q from %q: %w", key, localPath, err)
}
defer file.Close()
return f.uploadReader(ctx, file, key, opts, localPath)
}
// UploadReader stores content provided by a caller-owned reader.
func (f *FakeBackend) UploadReader(ctx context.Context, source io.Reader, key string, opts UploadOptions) (ObjectInfo, error) {
return f.uploadReader(ctx, source, key, opts, "reader")
}
func (f *FakeBackend) uploadReader(ctx context.Context, source io.Reader, key string, opts UploadOptions, localPath string) (ObjectInfo, error) {
if err := ctx.Err(); err != nil {
return ObjectInfo{}, err
}
if f.UploadErr != nil {
return ObjectInfo{}, f.UploadErr
}
if source == nil {
return ObjectInfo{}, fmt.Errorf("upload object: source is required")
}
if strings.TrimSpace(key) == "" {
return ObjectInfo{}, fmt.Errorf("upload object: key is required")
}
data, err := io.ReadAll(source)
if err != nil {
return ObjectInfo{}, fmt.Errorf("upload object %q from %q: %w", key, localPath, err)
}

View File

@@ -2,9 +2,16 @@ package storage
import (
"context"
"io"
"time"
)
// ReaderUploader streams caller-owned, already-opened content to object storage.
// Callers retain source-selection and filesystem-confinement policy.
type ReaderUploader interface {
UploadReader(ctx context.Context, source io.Reader, key string, opts UploadOptions) (ObjectInfo, error)
}
// ObjectStore is a remote object storage boundary used by prepare, restore, and publish work.
//
// Key invariant:

View File

@@ -214,11 +214,27 @@ func (b *S3Backend) Upload(ctx context.Context, localPath, key string, opts Uplo
if err != nil {
return ObjectInfo{}, fmt.Errorf("upload object %q from %q: stat local file: %w", normalizedKey, localPath, err)
}
return b.uploadReader(ctx, file, key, opts, stat.Size())
}
// UploadReader sends caller-owned content to key.
func (b *S3Backend) UploadReader(ctx context.Context, source io.Reader, key string, opts UploadOptions) (ObjectInfo, error) {
return b.uploadReader(ctx, source, key, opts, 0)
}
func (b *S3Backend) uploadReader(ctx context.Context, source io.Reader, key string, opts UploadOptions, size int64) (ObjectInfo, error) {
normalizedKey := normalizeObjectKey(key)
if source == nil {
return ObjectInfo{}, fmt.Errorf("upload object: source is required")
}
if normalizedKey == "" {
return ObjectInfo{}, fmt.Errorf("upload object: key is required")
}
input := &s3.PutObjectInput{
Bucket: &b.bucket,
Key: &normalizedKey,
Body: file,
Body: source,
Metadata: copyMetadata(opts.Metadata),
}
if strings.TrimSpace(opts.ContentType) != "" {
@@ -228,14 +244,17 @@ func (b *S3Backend) Upload(ctx context.Context, localPath, key string, opts Uplo
resp, err := b.client.PutObject(ctx, input)
if err != nil {
return ObjectInfo{}, fmt.Errorf("upload object %q from %q: %w", normalizedKey, localPath, err)
return ObjectInfo{}, fmt.Errorf("upload object %q: %w", normalizedKey, err)
}
return ObjectInfo{
info := ObjectInfo{
Key: normalizedKey,
Size: stat.Size(),
ETag: strings.Trim(valueOrEmpty(resp.ETag), "\""),
}, nil
}
if size > 0 {
info.Size = size
}
return info, nil
}
// Exists checks whether one object key exists.