From a389ac0aa7041142a5af391b16aca1116c021cd6 Mon Sep 17 00:00:00 2001 From: erikcarlsson Date: Fri, 17 Jan 2025 12:17:26 +0100 Subject: [PATCH] Add a separate bucket ref type for signed upload URLs. (#1716) --- runtimes/go/storage/objects/refs.go | 16 ++++++++++++++++ runtimes/js/encore.dev/storage/objects/bucket.ts | 4 ++-- runtimes/js/encore.dev/storage/objects/mod.ts | 2 +- runtimes/js/encore.dev/storage/objects/refs.ts | 8 +++++++- tsparser/src/parser/resources/infra/objects.rs | 5 ++++- v2/parser/infra/objects/usage.go | 8 ++++++-- v2/parser/infra/objects/usage_test.go | 8 ++++---- 7 files changed, 40 insertions(+), 11 deletions(-) diff --git a/runtimes/go/storage/objects/refs.go b/runtimes/go/storage/objects/refs.go index a6df90e785..a94e248864 100644 --- a/runtimes/go/storage/objects/refs.go +++ b/runtimes/go/storage/objects/refs.go @@ -38,6 +38,22 @@ type Uploader interface { // Upload begins uploading an object to the bucket. Upload(ctx context.Context, object string, options ...UploadOption) *Writer + perms() +} + +// SignedUploader is the interface for creating external URLs to upload objects +// to a bucket. It can be used in conjunction with [BucketRef] to declare +// a reference that can generate upload URLs to the bucket. +// +// For example: +// +// var MyBucket = objects.NewBucket(...) +// var ref = objects.BucketRef[objects.SignedUploader](MyBucket) +// +// The ref object can then be used to generate upload URLs and can be +// passed around freely within the service, without being subject +// to Encore's static analysis restrictions that apply to MyBucket. +type SignedUploader interface { // SignedUploadURL returns a signed URL that can be used to upload directly to // storage, without any other authentication. SignedUploadURL(ctx context.Context, object string, options ...UploadURLOption) (string, error) diff --git a/runtimes/js/encore.dev/storage/objects/bucket.ts b/runtimes/js/encore.dev/storage/objects/bucket.ts index 12e0ba715e..dfe8430292 100644 --- a/runtimes/js/encore.dev/storage/objects/bucket.ts +++ b/runtimes/js/encore.dev/storage/objects/bucket.ts @@ -2,7 +2,7 @@ import { getCurrentRequest } from "../../internal/reqtrack/mod"; import * as runtime from "../../internal/runtime/mod"; import { StringLiteral } from "../../internal/utils/constraints"; import { unwrapErr } from "./error"; -import { BucketPerms, Uploader, Downloader, Attrser, Lister, Remover, PublicUrler } from "./refs"; +import { BucketPerms, Uploader, SignedUploader, Downloader, Attrser, Lister, Remover, PublicUrler } from "./refs"; export interface BucketConfig { /** @@ -21,7 +21,7 @@ export interface BucketConfig { /** * Defines a new Object Storage bucket infrastructure resource. */ -export class Bucket extends BucketPerms implements Uploader, Downloader, Attrser, Lister, Remover, PublicUrler { +export class Bucket extends BucketPerms implements Uploader, SignedUploader, Downloader, Attrser, Lister, Remover, PublicUrler { impl: runtime.Bucket; /** diff --git a/runtimes/js/encore.dev/storage/objects/mod.ts b/runtimes/js/encore.dev/storage/objects/mod.ts index d25da2b8cc..724fe710c0 100644 --- a/runtimes/js/encore.dev/storage/objects/mod.ts +++ b/runtimes/js/encore.dev/storage/objects/mod.ts @@ -1,4 +1,4 @@ export { Bucket } from "./bucket"; export type { BucketConfig, ObjectAttrs, UploadOptions } from "./bucket"; export { ObjectsError, ObjectNotFound, PreconditionFailed } from "./error"; -export type { BucketPerms, Uploader, Downloader, Attrser, Lister, ReadWriter, PublicUrler } from "./refs"; +export type { BucketPerms, Uploader, SignedUploader, Downloader, Attrser, Lister, ReadWriter, PublicUrler } from "./refs"; diff --git a/runtimes/js/encore.dev/storage/objects/refs.ts b/runtimes/js/encore.dev/storage/objects/refs.ts index 1ae009e9a9..e5462bae07 100644 --- a/runtimes/js/encore.dev/storage/objects/refs.ts +++ b/runtimes/js/encore.dev/storage/objects/refs.ts @@ -1,4 +1,5 @@ -import type { AttrsOptions, DeleteOptions, DownloadOptions, ExistsOptions, ListEntry, ListOptions, ObjectAttrs, UploadOptions } from "./bucket"; +import type { AttrsOptions, DeleteOptions, DownloadOptions, ExistsOptions, ListEntry, + ListOptions, ObjectAttrs, SignedUploadUrl, UploadOptions, UploadUrlOptions } from "./bucket"; export abstract class BucketPerms { private bucketPerms(): void { }; @@ -8,6 +9,10 @@ export abstract class Uploader extends BucketPerms { abstract upload(name: string, data: Buffer, options?: UploadOptions): Promise; } +export abstract class SignedUploader extends BucketPerms { + abstract signedUploadUrl(name: string, options?: UploadUrlOptions): Promise; +} + export abstract class Downloader extends BucketPerms { abstract download(name: string, options?: DownloadOptions): Promise; } @@ -31,6 +36,7 @@ export abstract class PublicUrler extends BucketPerms { export type ReadWriter = & Uploader + & SignedUploader & Downloader & Attrser & Lister diff --git a/tsparser/src/parser/resources/infra/objects.rs b/tsparser/src/parser/resources/infra/objects.rs index 03c2f880a8..de4f059ac5 100644 --- a/tsparser/src/parser/resources/infra/objects.rs +++ b/tsparser/src/parser/resources/infra/objects.rs @@ -183,7 +183,10 @@ fn parse_bucket_ref( let ops = match named.obj.name.as_deref() { Some("Lister") => vec![Operation::ListObjects], Some("Attrser") => vec![Operation::GetObjectMetadata], - Some("Uploader") => vec![Operation::WriteObject, Operation::SignedUploadUrl], + Some("Uploader") => vec![Operation::WriteObject], + Some("SignedUploader") => { + vec![Operation::WriteObject, Operation::SignedUploadUrl] + } Some("Downloader") => vec![Operation::ReadObjectContents], Some("Remover") => vec![Operation::DeleteObject], Some("PublicUrler") => vec![Operation::GetPublicUrl], diff --git a/v2/parser/infra/objects/usage.go b/v2/parser/infra/objects/usage.go index 5543be58bf..a78aa0d9bd 100644 --- a/v2/parser/infra/objects/usage.go +++ b/v2/parser/infra/objects/usage.go @@ -129,6 +129,8 @@ func parseBucketRef(errs *perr.List, expr *usage.FuncArg) usage.Usage { for _, typ := range types { switch { case isNamed(typ, "Uploader"): + perms = append(perms, WriteObject) + case isNamed(typ, "SignedUploader"): perms = append(perms, WriteObject, SignedUploadURL) case isNamed(typ, "Downloader"): perms = append(perms, ReadObjectContents) @@ -141,7 +143,9 @@ func parseBucketRef(errs *perr.List, expr *usage.FuncArg) usage.Usage { case isNamed(typ, "PublicURLer"): perms = append(perms, GetPublicURL) case isNamed(typ, "ReadWriter"): - perms = append(perms, WriteObject, ReadObjectContents, ListObjects, DeleteObject, GetObjectMetadata, SignedUploadURL, UpdateObjectMetadata) + perms = append(perms, + WriteObject, ReadObjectContents, ListObjects, DeleteObject, + GetObjectMetadata, SignedUploadURL, UpdateObjectMetadata) default: return nil, false } @@ -149,7 +153,7 @@ func parseBucketRef(errs *perr.List, expr *usage.FuncArg) usage.Usage { // Sort and de-dup the perms. slices.Sort(perms) - slices.Compact(perms) + perms = slices.Compact(perms) return &RefUsage{ Base: usage.Base{ diff --git a/v2/parser/infra/objects/usage_test.go b/v2/parser/infra/objects/usage_test.go index d62e001ddf..f2b826a13c 100644 --- a/v2/parser/infra/objects/usage_test.go +++ b/v2/parser/infra/objects/usage_test.go @@ -46,7 +46,7 @@ var bkt = objects.NewBucket("bucket", objects.BucketConfig{}) var ref = objects.BucketRef[objects.Uploader](bkt) `, Want: []usage.Usage{&objects.RefUsage{ - Perms: []objects.Perm{objects.SignedUploadURL, objects.WriteObject}, + Perms: []objects.Perm{objects.WriteObject}, }}, }, { @@ -78,7 +78,7 @@ type MyRef = objects.Uploader var ref = objects.BucketRef[MyRef](bkt) `, Want: []usage.Usage{&objects.RefUsage{ - Perms: []objects.Perm{objects.SignedUploadURL, objects.WriteObject}, + Perms: []objects.Perm{objects.WriteObject}, }}, }, { @@ -91,7 +91,7 @@ type MyRef interface { objects.Uploader } var ref = objects.BucketRef[MyRef](bkt) `, Want: []usage.Usage{&objects.RefUsage{ - Perms: []objects.Perm{objects.SignedUploadURL, objects.WriteObject}, + Perms: []objects.Perm{objects.WriteObject}, }}, }, { @@ -99,7 +99,7 @@ var ref = objects.BucketRef[MyRef](bkt) Code: ` var bkt = objects.NewBucket("bucket", objects.BucketConfig{}) -type MyRef interface { objects.Uploader; objects.Downloader } +type MyRef interface { objects.Uploader; objects.Downloader; objects.SignedUploader } var ref = objects.BucketRef[MyRef](bkt) `,