mirror of
https://github.com/Dvorinka/Primora.git
synced 2026-10-28 15:51:23 +00:00
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:
1 parent
a84049b8fc
commit
fd36389328
22 files changed
+446
-7
No files matched your search
@@ -10,6 +10,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -1418,6 +1419,48 @@ func (s *PlatformService) GetObject(ctx context.Context, actor *models.Actor, bu
|
||||
return object, file, nil
|
||||
}
|
||||
|
||||
// PresignObject mints a short-lived direct-to-storage URL for a single
|
||||
// object. Requires the s3 storage driver — the local store has no external
|
||||
// URL surface to sign against.
|
||||
func (s *PlatformService) PresignObject(ctx context.Context, actor *models.Actor, bucketID uuid.UUID, objectKey, op string, ttlSeconds int) (*storage.PresignedURL, error) {
|
||||
bucket, err := s.repo.Queries().GetBucketByID(ctx, bucketID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
method := ""
|
||||
switch op {
|
||||
case "download", "":
|
||||
method = http.MethodGet
|
||||
if err := s.requireBucketRead(ctx, actor, bucket); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !objectExists(ctx, s, bucketID, objectKey) {
|
||||
return nil, pgx.ErrNoRows
|
||||
}
|
||||
case "upload":
|
||||
method = http.MethodPut
|
||||
if err := s.requireBucketWrite(ctx, actor, bucket); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid op %q — download or upload", op)
|
||||
}
|
||||
p, ok := s.store.(storage.Presigner)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("presigned URLs require BACKEND_STORAGE_DRIVER=s3")
|
||||
}
|
||||
ttl := time.Duration(ttlSeconds) * time.Second
|
||||
return p.Presign(ctx, bucketID.String(), objectKey, method, ttl)
|
||||
}
|
||||
|
||||
func objectExists(ctx context.Context, s *PlatformService, bucketID uuid.UUID, key string) bool {
|
||||
_, err := s.repo.Queries().GetBucketObjectByKey(ctx, db.GetBucketObjectByKeyParams{
|
||||
BucketID: bucketID,
|
||||
ObjectKey: key,
|
||||
})
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (s *PlatformService) UpdateObject(ctx context.Context, actor *models.Actor, bucketID uuid.UUID, objectKey string, input UpdateObjectInput, requestID string) (db.CoreBucketObject, error) {
|
||||
sourceBucket, err := s.repo.Queries().GetBucketByID(ctx, bucketID)
|
||||
if err != nil {
|
||||
|
||||
Reference in new issue
Block a user