feat: Presigned object URLs (s3 driver)

POST /buckets/:id/object-presigns mints SigV4 query-signed GET/PUT
URLs so clients move bytes directly against S3 — large transfers skip
the backend entirely.

- storage.Presigner optional interface; S3Store implements, local
  driver returns a clear error (no external surface to sign).
- TTL 60s–1h (default 15m); download requires read role + existing
  object row (no key probing); upload requires write role.
- S3_PUBLIC_ENDPOINT rewrites the client-facing host for internal
  S3_ENDPOINTs — signatures bind the host, URLs are valid only where
  minted.
- CLI: primora objects:presign <bucket> <key> [--upload] [--ttl].

Verified live against cloudserver: GET fetched the object with no
auth headers; PUT pushed 1 MB direct to S3; unsigned list → 403;
untracked-key download presign → 404.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Tomas DvorakandDevin committed 2026-09-24 20:35:21 +02:00
1 parent a84049b8fc
commit fd36389328
22 files changed
+446 -7

No files matched your search

+15
View File
@@ -3,6 +3,7 @@ package storage
import (
"context"
"io"
"time"
)
// Store is the object-storage contract the platform service depends on.
@@ -14,3 +15,17 @@ type Store interface {
MoveBetweenBuckets(sourceBucketID, destinationBucketID, fromKey, toKey string) (string, error)
DeleteBucket(bucketID string) error
}
// PresignedURL is a short-lived direct-to-storage URL for one operation.
type PresignedURL struct {
URL string `json:"url"`
Method string `json:"method"`
ExpiresAt time.Time `json:"expires_at"`
}
// Presigner is an optional capability: stores backed by a real object API
// (S3) can mint URLs clients call directly. The local driver can't — its
// "URL" would have to be served by this backend anyway.
type Presigner interface {
Presign(ctx context.Context, bucketID, objectKey, method string, ttl time.Duration) (*PresignedURL, error)
}