Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a PresignPostObject to the PresignClient for s3.PutObject operation #2758

Merged
merged 9 commits into from
Aug 28, 2024
8 changes: 8 additions & 0 deletions .changelog/f80f134492ef472493f6e5090b404bc2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"id": "f80f1344-92ef-4724-93f6-e5090b404bc2",
"type": "feature",
"description": "Add presignPost for s3 PutObject",
"modules": [
"service/s3"
]
}
10 changes: 5 additions & 5 deletions service/internal/integrationtest/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ module github.com/aws/aws-sdk-go-v2/service/internal/integrationtest

require (
github.com/aws/aws-sdk-go-v2 v1.30.4
github.com/aws/aws-sdk-go-v2/config v1.27.29
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.13
github.com/aws/aws-sdk-go-v2/config v1.27.31
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.15
github.com/aws/aws-sdk-go-v2/service/acm v1.28.6
github.com/aws/aws-sdk-go-v2/service/apigateway v1.25.6
github.com/aws/aws-sdk-go-v2/service/applicationautoscaling v1.31.2
Expand All @@ -17,7 +17,7 @@ require (
github.com/aws/aws-sdk-go-v2/service/cloudhsmv2 v1.25.3
github.com/aws/aws-sdk-go-v2/service/cloudtrail v1.42.5
github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.40.5
github.com/aws/aws-sdk-go-v2/service/codebuild v1.42.0
github.com/aws/aws-sdk-go-v2/service/codebuild v1.42.1
github.com/aws/aws-sdk-go-v2/service/codecommit v1.25.1
github.com/aws/aws-sdk-go-v2/service/codedeploy v1.27.5
github.com/aws/aws-sdk-go-v2/service/codepipeline v1.31.1
Expand Down Expand Up @@ -82,13 +82,13 @@ require (
github.com/aws/aws-sdk-go-v2/service/waf v1.23.4
github.com/aws/aws-sdk-go-v2/service/wafregional v1.23.4
github.com/aws/aws-sdk-go-v2/service/wafv2 v1.51.5
github.com/aws/aws-sdk-go-v2/service/workspaces v1.45.1
github.com/aws/aws-sdk-go-v2/service/workspaces v1.46.0
github.com/aws/smithy-go v1.20.4
)

require (
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.4 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.29 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.30 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.12 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.16 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.16 // indirect
Expand Down
187 changes: 187 additions & 0 deletions service/internal/integrationtest/s3/presign_post_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
//go:build integration
// +build integration

package s3

import (
"bytes"
"context"
"io"
"mime/multipart"
"net/http"
"os"
"testing"
"time"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/internal/integrationtest"
"github.com/aws/aws-sdk-go-v2/service/s3"
)

func TestInteg_PresigPost(t *testing.T) {

const filePath = "sample.txt"

cases := map[string]struct {
params s3.PutObjectInput
conditions []interface{}
expectedStatusCode int
}{
"standard": {
params: s3.PutObjectInput{},
},
"extra conditions, fail upload": {
params: s3.PutObjectInput{},
conditions: []interface{}{
[]interface{}{
// any number larger than the small sample
"content-length-range",
100000,
200000,
},
},
expectedStatusCode: http.StatusBadRequest,
},
}

for name, c := range cases {
t.Run(name, func(t *testing.T) {

ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second)
defer cancelFn()

cfg, err := integrationtest.LoadConfigWithDefaultRegion("us-west-2")
if err != nil {
t.Fatalf("failed to load config, %v", err)
}

client := s3.NewFromConfig(cfg)

// construct a put object
presignerClient := s3.NewPresignClient(client)

params := c.params
if params.Key == nil {
params.Key = aws.String(integrationtest.UniqueID())
}
params.Bucket = &setupMetadata.Buckets.Source.Name
var presignRequest *s3.PresignedPostRequest
if c.conditions != nil {
presignRequest, err = presignerClient.PresignPostObject(ctx, &params, func(opts *s3.PresignPostOptions) {
opts.Conditions = c.conditions
})

} else {
presignRequest, err = presignerClient.PresignPostObject(ctx, &params)
}
if err != nil {
t.Fatalf("expect no error, got %v", err)
}

resp, err := sendMultipartRequest(presignRequest.URL, presignRequest.Values, filePath)
if err != nil {
t.Fatalf("expect no error while sending HTTP request using presigned url, got %v", err)
}

defer resp.Body.Close()
if c.expectedStatusCode != 0 {
if resp.StatusCode != c.expectedStatusCode {
t.Fatalf("expect status code %v, got %v", c.expectedStatusCode, resp.StatusCode)
}
// don't check the rest of the tests if there's a custom status code
return
} else {
// expected result is 204 on POST requests
if resp.StatusCode != http.StatusNoContent {
t.Fatalf("failed to put S3 object, %d:%s", resp.StatusCode, resp.Status)
}
}

// construct a get object
getObjectInput := &s3.GetObjectInput{
Bucket: params.Bucket,
Key: params.Key,
}

// This could be a regular GetObject call, but since we already have a presigner client available
getRequest, err := presignerClient.PresignGetObject(ctx, getObjectInput)
if err != nil {
t.Errorf("expect no error, got %v", err)
}

resp, err = sendHTTPRequest(getRequest, nil)
if err != nil {
t.Errorf("expect no error while sending HTTP request using presigned url, got %v", err)
}

defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
t.Fatalf("failed to get S3 object, %d:%s", resp.StatusCode, resp.Status)
}

content, err := os.ReadFile(filePath)
if err != nil {
t.Fatalf("expect no error reading local file %v, got %v", filePath, err)
}
respBytes, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatalf("expect no error reading response %v, got %v", resp.Body, err)
}
if !bytes.Equal(content, respBytes) {
t.Fatalf("expect response body %v, got %v", content, resp.Body)
}
})
}
}

func sendMultipartRequest(url string, fields map[string]string, filePath string) (*http.Response, error) {
// Create a buffer to hold the multipart data
var requestBody bytes.Buffer
writer := multipart.NewWriter(&requestBody)

// Add form fields
for key, val := range fields {
err := writer.WriteField(key, val)
if err != nil {
return nil, err
}
}

// Add the file
file, err := os.Open(filePath)
if err != nil {
return nil, err
}
defer file.Close()

// Always has to be named like this
fileField := "file"
part, err := writer.CreateFormFile(fileField, filePath)
if err != nil {
return nil, err
}
_, err = io.Copy(part, file)
if err != nil {
return nil, err
}

// Close the writer to finalize the multipart message
err = writer.Close()
if err != nil {
return nil, err
}

// Create a new HTTP request
req, err := http.NewRequest("POST", url, &requestBody)
if err != nil {
return nil, err
}

// Set the Content-Type header
req.Header.Set("Content-Type", writer.FormDataContentType())

// Send the request
client := &http.Client{}
return client.Do(req)
}
1 change: 1 addition & 0 deletions service/internal/integrationtest/s3/sample.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Lorem ipsum et dolor
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ pt:
goto pt
}
// fail if not succeed after 10 attempts
return fmt.Errorf("failed to determine if a bucket %s exists and you have permission to access it", bucketName)
return fmt.Errorf("failed to determine if a bucket %s exists and you have permission to access it %v", bucketName, err)
}

return nil
Expand Down
Loading
Loading