diff --git a/blob/memory/storage.go b/blob/memory/storage.go index e2674bc..e7ba40f 100644 --- a/blob/memory/storage.go +++ b/blob/memory/storage.go @@ -19,7 +19,8 @@ const ( uploadURL = "https://upload.blobs.test/" downloadURLPrefix = "https://cdn.blobs.test/" - uploadTTL = 15 * time.Minute + uploadTTL = 15 * time.Minute + downloadTTL = 15 * time.Minute ) // Storage is an in-memory blob.ObjectStorage for tests. It stands in for the @@ -88,8 +89,11 @@ func (s *Storage) DeleteUpload(_ context.Context, key string) error { return nil } -func (s *Storage) SignDownloadURL(_ context.Context, key string) (string, error) { - return downloadURLPrefix + key, nil +func (s *Storage) SignDownloadURL(_ context.Context, key string) (*blobpb.DownloadUrl, error) { + return &blobpb.DownloadUrl{ + Url: downloadURLPrefix + key, + ExpiresAt: timestamppb.New(time.Now().Add(downloadTTL)), + }, nil } // SimulateUpload stores bytes as if the client had POSTed them to the presigned diff --git a/blob/s3/storage.go b/blob/s3/storage.go index a2e0d52..177fd47 100644 --- a/blob/s3/storage.go +++ b/blob/s3/storage.go @@ -282,13 +282,17 @@ func (s *Storage) DeleteUpload(ctx context.Context, key string) error { return nil } -func (s *Storage) SignDownloadURL(_ context.Context, key string) (string, error) { +func (s *Storage) SignDownloadURL(_ context.Context, key string) (*blobpb.DownloadUrl, error) { + expiresAt := time.Now().Add(s.cfg.DownloadTTL) rawURL := strings.TrimRight(s.cfg.CDNBaseURL, "/") + "/" + key - signed, err := s.signer.Sign(rawURL, time.Now().Add(s.cfg.DownloadTTL)) + signed, err := s.signer.Sign(rawURL, expiresAt) if err != nil { - return "", fmt.Errorf("failed to sign download url: %w", err) + return nil, fmt.Errorf("failed to sign download url: %w", err) } - return signed, nil + return &blobpb.DownloadUrl{ + Url: signed, + ExpiresAt: timestamppb.New(expiresAt), + }, nil } // sigV4SigningKey derives the SigV4 signing key for the S3 service. diff --git a/blob/server.go b/blob/server.go index f8e5837..71156b7 100644 --- a/blob/server.go +++ b/blob/server.go @@ -207,9 +207,18 @@ func (s *Server) CompleteExternalUpload(ctx context.Context, req *blobpb.Complet return nil, status.Error(codes.Unavailable, "upload not yet finalized") } + // todo: Support rejection metadata + var rejectionMetadata *blobpb.RejectionMetadata + if finalStatus == blobpb.BlobStatus_BLOB_STATUS_REJECTED { + rejectionMetadata = &blobpb.RejectionMetadata{ + Reason: blobpb.RejectionReason_REJECTION_REASON_INTERNAL, + } + } + return &blobpb.CompleteExternalUploadResponse{ - Result: blobpb.CompleteExternalUploadResponse_OK, - Status: finalStatus, + Result: blobpb.CompleteExternalUploadResponse_OK, + Status: finalStatus, + RejectionMetadata: rejectionMetadata, }, nil } @@ -243,9 +252,10 @@ func (s *Server) GetBlobs(ctx context.Context, req *blobpb.GetBlobsRequest) (*bl Status: blobStatus, } - // download_url and the rest of the metadata are only meaningful, and only - // minted, for a servable (READY) blob. - if blobStatus == blobpb.BlobStatus_BLOB_STATUS_READY { + switch blobStatus { + case blobpb.BlobStatus_BLOB_STATUS_READY: + // download_url and the rest of the metadata are only meaningful, and only + // minted, for a servable (READY) blob. metadata, err := s.buildMetadata(ctx, record) if err != nil { s.log.Warn("Failed to mint blob metadata", @@ -255,6 +265,11 @@ func (s *Server) GetBlobs(ctx context.Context, req *blobpb.GetBlobsRequest) (*bl return nil, status.Error(codes.Internal, "failed to get blobs") } protoBlob.Metadata = metadata + case blobpb.BlobStatus_BLOB_STATUS_REJECTED: + // todo: Support rejection metadata + protoBlob.Rejection = &blobpb.RejectionMetadata{ + Reason: blobpb.RejectionReason_REJECTION_REASON_INTERNAL, + } } resolved = append(resolved, protoBlob) diff --git a/blob/storage.go b/blob/storage.go index f71d3c5..4df69e7 100644 --- a/blob/storage.go +++ b/blob/storage.go @@ -51,9 +51,10 @@ type ObjectStorage interface { DeleteUpload(ctx context.Context, key string) error // SignDownloadURL mints a fresh, short-lived CDN URL for fetching a promoted - // object's bytes from the ORIGIN store. It is authorized at mint time and - // expires on its own, so callers mint a new one rather than persisting it. - SignDownloadURL(ctx context.Context, key string) (string, error) + // object's bytes from the ORIGIN store, paired with the instant it expires. It + // is authorized at mint time and expires on its own, so callers mint a new one + // rather than persisting it. + SignDownloadURL(ctx context.Context, key string) (*blobpb.DownloadUrl, error) } // StorageKey derives the object key for an image blob's bytes from its id and diff --git a/blob/tests/server.go b/blob/tests/server.go index d456653..d5bedc2 100644 --- a/blob/tests/server.go +++ b/blob/tests/server.go @@ -7,6 +7,7 @@ import ( "image/color" "image/png" "testing" + "time" "github.com/stretchr/testify/require" "go.uber.org/zap/zaptest" @@ -274,7 +275,10 @@ func testGetBlobs(t *testing.T, accounts account.Store, blobs blob.Store, storag require.NotNil(t, got.Metadata) require.Equal(t, "image/png", got.Metadata.MimeType) require.EqualValues(t, len(imageBytes), got.Metadata.SizeBytes) - require.NotEmpty(t, got.Metadata.DownloadUrl) + require.NotNil(t, got.Metadata.DownloadUrl) + require.NotEmpty(t, got.Metadata.DownloadUrl.Url) + require.NotNil(t, got.Metadata.DownloadUrl.ExpiresAt) + require.True(t, got.Metadata.DownloadUrl.ExpiresAt.AsTime().After(time.Now())) image := got.Metadata.GetImage() require.NotNil(t, image) require.EqualValues(t, 10, image.Width) diff --git a/go.mod b/go.mod index 418b2e4..12004d1 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.42.3 github.com/aws/smithy-go v1.27.1 github.com/buckket/go-blurhash v1.1.0 - github.com/code-payments/flipcash2-protobuf-api v1.14.1-0.20260629164249-229024d1e8f3 + github.com/code-payments/flipcash2-protobuf-api v1.14.1-0.20260629203049-f1014792ec00 github.com/code-payments/ocp-protobuf-api v1.13.2-0.20260610171241-de46af911053 github.com/code-payments/ocp-server v1.19.0 github.com/devsisters/go-applereceipt v0.0.0-20240805020915-fa22a0160fc2 @@ -101,7 +101,6 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/shopspring/decimal v1.4.0 // indirect - github.com/steebchen/prisma-client-go v0.47.0 // indirect github.com/tyler-smith/go-bip39 v1.1.0 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect diff --git a/go.sum b/go.sum index b367635..b3d6e15 100644 --- a/go.sum +++ b/go.sum @@ -78,8 +78,8 @@ github.com/cockroachdb/cockroach-go/v2 v2.2.0 h1:/5znzg5n373N/3ESjHF5SMLxiW4RKB0 github.com/cockroachdb/cockroach-go/v2 v2.2.0/go.mod h1:u3MiKYGupPPjkn3ozknpMUpxPaNLTFWAya419/zv6eI= github.com/code-payments/code-vm-indexer v1.2.0 h1:rSHpBMiT9BKgmKcXg/VIoi/h0t7jNxGx07Qz59m+6Q0= github.com/code-payments/code-vm-indexer v1.2.0/go.mod h1:vn91YN2qNqb+gGJeZe2+l+TNxVmEEiRHXXnIn2Y40h8= -github.com/code-payments/flipcash2-protobuf-api v1.14.1-0.20260629164249-229024d1e8f3 h1:UqbDRlTMBuRMr7R+k/LrwnEBYjkZhWQStwIqrFryEd4= -github.com/code-payments/flipcash2-protobuf-api v1.14.1-0.20260629164249-229024d1e8f3/go.mod h1:s/1pOsb4FTRD+LcvRKGjfmm6ygRS/m1ep34EIW0fuDs= +github.com/code-payments/flipcash2-protobuf-api v1.14.1-0.20260629203049-f1014792ec00 h1:YT5cVHI5610PW6vvPUjgHqV3FzmWdIXKcBqH6ngX9/o= +github.com/code-payments/flipcash2-protobuf-api v1.14.1-0.20260629203049-f1014792ec00/go.mod h1:s/1pOsb4FTRD+LcvRKGjfmm6ygRS/m1ep34EIW0fuDs= github.com/code-payments/ocp-protobuf-api v1.13.2-0.20260610171241-de46af911053 h1:0B96K7/z7TpFbAJeyzP43A0eM58Ie/KRuu5U6PUrses= github.com/code-payments/ocp-protobuf-api v1.13.2-0.20260610171241-de46af911053/go.mod h1:tw6BooY5a8l6CtSZnKOruyKII0W04n89pcM4BizrgG8= github.com/code-payments/ocp-server v1.19.0 h1:l4cznUxOAmaclHKpJANJ2zE3LTXg+BDBbRhl+r66lFI= @@ -316,8 +316,6 @@ github.com/smartystreets/assertions v1.13.1 h1:Ef7KhSmjZcK6AVf9YbJdvPYG9avaF0Zxu github.com/smartystreets/assertions v1.13.1/go.mod h1:cXr/IwVfSo/RbCSPhoAPv73p3hlSdrBH/b3SdnW/LMY= github.com/smartystreets/goconvey v1.8.0 h1:Oi49ha/2MURE0WexF052Z0m+BNSGirfjg5RL+JXWq3w= github.com/smartystreets/goconvey v1.8.0/go.mod h1:EdX8jtrTIj26jmjCOVNMVSIYAtgexqXKHOXW2Dx9JLg= -github.com/steebchen/prisma-client-go v0.47.0 h1:mKelgkcGPcIardjTP5diGq6hvnueQc/DYEyQ+6uZ0/E= -github.com/steebchen/prisma-client-go v0.47.0/go.mod h1:i1B0PEaE+BUcBUiwvd9drWpyMG/zNYMRrD5MancMf2I= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=