Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions simplestorage/buckets_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ func TestCreateBucket(t *testing.T) {
if cleanup == nil {
cleanup = func() func() { return func() {} }
}
defer cleanup()
defer cleanup()()

client, err := New(context.Background(),
WithEndpoint("https://test.endpoint.dev"),
Expand Down Expand Up @@ -128,7 +128,7 @@ func TestDeleteBucket(t *testing.T) {
if cleanup == nil {
cleanup = func() func() { return func() {} }
}
defer cleanup()
defer cleanup()()

client, err := New(context.Background(),
WithEndpoint("https://test.endpoint.dev"),
Expand Down Expand Up @@ -176,7 +176,7 @@ func TestListBuckets(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cleanup := tt.setupEnv
defer cleanup()
defer cleanup()()

client, err := New(context.Background(),
WithEndpoint("https://test.endpoint.dev"),
Expand Down Expand Up @@ -219,7 +219,7 @@ func TestGetBucketInfo(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cleanup := tt.setupEnv
defer cleanup()
defer cleanup()()

// Create a client
os.Setenv("TIGRIS_STORAGE_BUCKET", "dummy-bucket")
Expand Down
23 changes: 16 additions & 7 deletions simplestorage/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -538,16 +538,25 @@ func (c *Client) PresignURL(ctx context.Context, method string, key string, expi

switch method {
case http.MethodGet:
return presignURLGet(ctx, presignClient, o.BucketName, key, expiry)
return presignURLGet(ctx, presignClient, o.BucketName, key, expiry, o)
case http.MethodPut:
return presignURLPut(ctx, presignClient, o.BucketName, key, expiry, o)
case http.MethodDelete:
return presignURLDelete(ctx, presignClient, o.BucketName, key, expiry)
return presignURLDelete(ctx, presignClient, o.BucketName, key, expiry, o)
}

return "", nil // unreachable
}

func presignOptions(expiry time.Duration, opts ClientOptions) []func(*s3.PresignOptions) {
return []func(*s3.PresignOptions){
s3.WithPresignExpires(expiry),
func(po *s3.PresignOptions) {
po.ClientOptions = append(po.ClientOptions, opts.S3Options...)
Comment thread
greptile-apps[bot] marked this conversation as resolved.
},
}
}

// generateRandomSuffix generates a random hexadecimal string of the specified length.
// Uses (length+1)/2 random bytes so odd lengths still produce a hex string at
// least length characters long before truncation.
Expand Down Expand Up @@ -636,11 +645,11 @@ func raise[T comparable](v T) *T {
}

// presignURLGet generates a presigned URL for GET operations.
func presignURLGet(ctx context.Context, client *s3.PresignClient, bucket, key string, expiry time.Duration) (string, error) {
func presignURLGet(ctx context.Context, client *s3.PresignClient, bucket, key string, expiry time.Duration, opts ClientOptions) (string, error) {
presignResult, err := client.PresignGetObject(ctx, &s3.GetObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
}, s3.WithPresignExpires(expiry))
}, presignOptions(expiry, opts)...)
if err != nil {
return "", fmt.Errorf("presign get: %w", err)
}
Expand All @@ -662,7 +671,7 @@ func presignURLPut(ctx context.Context, client *s3.PresignClient, bucket, key st
input.ContentDisposition = opts.ContentDisposition
}

presignResult, err := client.PresignPutObject(ctx, input, s3.WithPresignExpires(expiry))
presignResult, err := client.PresignPutObject(ctx, input, presignOptions(expiry, opts)...)
if err != nil {
return "", fmt.Errorf("presign put: %w", err)
}
Expand All @@ -671,11 +680,11 @@ func presignURLPut(ctx context.Context, client *s3.PresignClient, bucket, key st
}

// presignURLDelete generates a presigned URL for DELETE operations.
func presignURLDelete(ctx context.Context, client *s3.PresignClient, bucket, key string, expiry time.Duration) (string, error) {
func presignURLDelete(ctx context.Context, client *s3.PresignClient, bucket, key string, expiry time.Duration, opts ClientOptions) (string, error) {
presignResult, err := client.PresignDeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
}, s3.WithPresignExpires(expiry))
}, presignOptions(expiry, opts)...)
if err != nil {
return "", fmt.Errorf("presign delete: %w", err)
}
Expand Down
43 changes: 43 additions & 0 deletions simplestorage/presign_example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import (
"fmt"
"log"
"net/http"
"strings"
"time"

simplestorage "github.com/tigrisdata/storage-go/simplestorage"
"github.com/tigrisdata/storage-go/tigrisheaders"
)

func ExampleClient_PresignURL_get() {
Expand Down Expand Up @@ -52,6 +54,47 @@ func ExampleClient_PresignURL_put() {
fmt.Println("Presigned PUT URL:", url)
}

func ExampleClient_PresignURL_signedTigrisHeaders() {
ctx := context.Background()

client, err := simplestorage.New(ctx,
simplestorage.WithBucket("my-default-bucket"),
)
if err != nil {
log.Fatal(err)
}

// Headers passed through WithS3Options are part of the signature. The
// client using the URL must send them with the same values, or Tigris
// rejects the upload because the signature will not match.
url, err := client.PresignURL(ctx, http.MethodPut, "uploads/model.bin", 15*time.Minute,
simplestorage.WithContentType("application/octet-stream"),
simplestorage.WithS3Options(
tigrisheaders.WithStaticReplicationRegions([]tigrisheaders.Region{tigrisheaders.IAD, tigrisheaders.FRA}),
tigrisheaders.WithHeader("x-amz-meta-owner", "alice"),
),
)
if err != nil {
log.Fatal(err) // handle the error here
}

req, err := http.NewRequestWithContext(ctx, http.MethodPut, url, strings.NewReader("model bytes"))
if err != nil {
log.Fatal(err) // handle the error here
}
req.Header.Set("Content-Type", "application/octet-stream")
req.Header.Set("X-Tigris-Regions", "iad,fra")
req.Header.Set("x-amz-meta-owner", "alice")

resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatal(err) // handle the error here
}
defer resp.Body.Close()

fmt.Println("Upload status:", resp.Status)
}

func ExampleClient_PresignURL_delete() {
ctx := context.Background()

Expand Down
79 changes: 79 additions & 0 deletions simplestorage/presign_options_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package simplestorage

import (
"context"
"net/http"
"net/url"
"strings"
"testing"
"time"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/s3"
storage "github.com/tigrisdata/storage-go"
"github.com/tigrisdata/storage-go/tigrisheaders"
)

func offlinePresignClient(t *testing.T) *Client {
t.Helper()

s3cli := s3.New(s3.Options{
Region: "auto",
BaseEndpoint: aws.String("https://t3.storage.dev"),
Credentials: credentials.NewStaticCredentialsProvider("tid_test", "tsec_test", ""),
})

return &Client{cli: &storage.Client{Client: s3cli}, options: Options{BucketName: "presign-test"}}
}

func signedHeaders(t *testing.T, rawURL string) []string {
t.Helper()

u, err := url.Parse(rawURL)
if err != nil {
t.Fatalf("parse presigned url: %v", err)
}

return strings.Split(u.Query().Get("X-Amz-SignedHeaders"), ";")
}

func TestPresignURLSignsS3OptionHeaders(t *testing.T) {
cli := offlinePresignClient(t)

for _, method := range []string{http.MethodGet, http.MethodPut, http.MethodDelete} {
t.Run(method, func(t *testing.T) {
u, err := cli.PresignURL(context.Background(), method, "k", time.Minute,
WithS3Options(tigrisheaders.WithStaticReplicationRegions([]tigrisheaders.Region{tigrisheaders.IAD, tigrisheaders.FRA})))
if err != nil {
t.Fatalf("PresignURL(%s): %v", method, err)
}

got := signedHeaders(t, u)
for _, want := range []string{"host", "x-tigris-regions"} {
found := false
for _, h := range got {
if h == want {
found = true
}
}
if !found {
t.Errorf("PresignURL(%s): signed headers %v do not include %q", method, got, want)
}
}
})
}
}

func TestPresignURLWithoutS3OptionsSignsHostOnly(t *testing.T) {
cli := offlinePresignClient(t)

u, err := cli.PresignURL(context.Background(), http.MethodPut, "k", time.Minute)
if err != nil {
t.Fatalf("PresignURL: %v", err)
}

if got := signedHeaders(t, u); len(got) != 1 || got[0] != "host" {
t.Errorf("signed headers = %v, want [host]", got)
}
}
Loading