Skip to content

Commit 9c621d1

Browse files
authored
Add a PresignPostObject to the PresignClient for s3.PutObject operation (#2758)
* Add a `PresignPostObject` to the `PresignClient` for `s3.PutObject` operation
1 parent 7e17fb5 commit 9c621d1

File tree

6 files changed

+1119
-1
lines changed

6 files changed

+1119
-1
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"id": "f80f1344-92ef-4724-93f6-e5090b404bc2",
3+
"type": "feature",
4+
"description": "Add presignPost for s3 PutObject",
5+
"modules": [
6+
"service/s3"
7+
]
8+
}
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
//go:build integration
2+
// +build integration
3+
4+
package s3
5+
6+
import (
7+
"bytes"
8+
"context"
9+
"io"
10+
"mime/multipart"
11+
"net/http"
12+
"os"
13+
"testing"
14+
"time"
15+
16+
"github.com/aws/aws-sdk-go-v2/aws"
17+
"github.com/aws/aws-sdk-go-v2/service/internal/integrationtest"
18+
"github.com/aws/aws-sdk-go-v2/service/s3"
19+
)
20+
21+
func TestInteg_PresigPost(t *testing.T) {
22+
23+
const filePath = "sample.txt"
24+
25+
cases := map[string]struct {
26+
params s3.PutObjectInput
27+
conditions []interface{}
28+
expectedStatusCode int
29+
}{
30+
"standard": {
31+
params: s3.PutObjectInput{},
32+
},
33+
"extra conditions, fail upload": {
34+
params: s3.PutObjectInput{},
35+
conditions: []interface{}{
36+
[]interface{}{
37+
// any number larger than the small sample
38+
"content-length-range",
39+
100000,
40+
200000,
41+
},
42+
},
43+
expectedStatusCode: http.StatusBadRequest,
44+
},
45+
}
46+
47+
for name, c := range cases {
48+
t.Run(name, func(t *testing.T) {
49+
50+
ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second)
51+
defer cancelFn()
52+
53+
cfg, err := integrationtest.LoadConfigWithDefaultRegion("us-west-2")
54+
if err != nil {
55+
t.Fatalf("failed to load config, %v", err)
56+
}
57+
58+
client := s3.NewFromConfig(cfg)
59+
60+
// construct a put object
61+
presignerClient := s3.NewPresignClient(client)
62+
63+
params := c.params
64+
if params.Key == nil {
65+
params.Key = aws.String(integrationtest.UniqueID())
66+
}
67+
params.Bucket = &setupMetadata.Buckets.Source.Name
68+
var presignRequest *s3.PresignedPostRequest
69+
if c.conditions != nil {
70+
presignRequest, err = presignerClient.PresignPostObject(ctx, &params, func(opts *s3.PresignPostOptions) {
71+
opts.Conditions = c.conditions
72+
})
73+
74+
} else {
75+
presignRequest, err = presignerClient.PresignPostObject(ctx, &params)
76+
}
77+
if err != nil {
78+
t.Fatalf("expect no error, got %v", err)
79+
}
80+
81+
resp, err := sendMultipartRequest(presignRequest.URL, presignRequest.Values, filePath)
82+
if err != nil {
83+
t.Fatalf("expect no error while sending HTTP request using presigned url, got %v", err)
84+
}
85+
86+
defer resp.Body.Close()
87+
if c.expectedStatusCode != 0 {
88+
if resp.StatusCode != c.expectedStatusCode {
89+
t.Fatalf("expect status code %v, got %v", c.expectedStatusCode, resp.StatusCode)
90+
}
91+
// don't check the rest of the tests if there's a custom status code
92+
return
93+
} else {
94+
// expected result is 204 on POST requests
95+
if resp.StatusCode != http.StatusNoContent {
96+
t.Fatalf("failed to put S3 object, %d:%s", resp.StatusCode, resp.Status)
97+
}
98+
}
99+
100+
// construct a get object
101+
getObjectInput := &s3.GetObjectInput{
102+
Bucket: params.Bucket,
103+
Key: params.Key,
104+
}
105+
106+
// This could be a regular GetObject call, but since we already have a presigner client available
107+
getRequest, err := presignerClient.PresignGetObject(ctx, getObjectInput)
108+
if err != nil {
109+
t.Errorf("expect no error, got %v", err)
110+
}
111+
112+
resp, err = sendHTTPRequest(getRequest, nil)
113+
if err != nil {
114+
t.Errorf("expect no error while sending HTTP request using presigned url, got %v", err)
115+
}
116+
117+
defer resp.Body.Close()
118+
119+
if resp.StatusCode != http.StatusOK {
120+
t.Fatalf("failed to get S3 object, %d:%s", resp.StatusCode, resp.Status)
121+
}
122+
123+
content, err := os.ReadFile(filePath)
124+
if err != nil {
125+
t.Fatalf("expect no error reading local file %v, got %v", filePath, err)
126+
}
127+
respBytes, err := io.ReadAll(resp.Body)
128+
if err != nil {
129+
t.Fatalf("expect no error reading response %v, got %v", resp.Body, err)
130+
}
131+
if !bytes.Equal(content, respBytes) {
132+
t.Fatalf("expect response body %v, got %v", content, resp.Body)
133+
}
134+
})
135+
}
136+
}
137+
138+
func sendMultipartRequest(url string, fields map[string]string, filePath string) (*http.Response, error) {
139+
// Create a buffer to hold the multipart data
140+
var requestBody bytes.Buffer
141+
writer := multipart.NewWriter(&requestBody)
142+
143+
// Add form fields
144+
for key, val := range fields {
145+
err := writer.WriteField(key, val)
146+
if err != nil {
147+
return nil, err
148+
}
149+
}
150+
151+
// Add the file
152+
file, err := os.Open(filePath)
153+
if err != nil {
154+
return nil, err
155+
}
156+
defer file.Close()
157+
158+
// Always has to be named like this
159+
fileField := "file"
160+
part, err := writer.CreateFormFile(fileField, filePath)
161+
if err != nil {
162+
return nil, err
163+
}
164+
_, err = io.Copy(part, file)
165+
if err != nil {
166+
return nil, err
167+
}
168+
169+
// Close the writer to finalize the multipart message
170+
err = writer.Close()
171+
if err != nil {
172+
return nil, err
173+
}
174+
175+
// Create a new HTTP request
176+
req, err := http.NewRequest("POST", url, &requestBody)
177+
if err != nil {
178+
return nil, err
179+
}
180+
181+
// Set the Content-Type header
182+
req.Header.Set("Content-Type", writer.FormDataContentType())
183+
184+
// Send the request
185+
client := &http.Client{}
186+
return client.Do(req)
187+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Lorem ipsum et dolor

service/internal/integrationtest/s3shared/integ_test_setup.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ pt:
7171
goto pt
7272
}
7373
// fail if not succeed after 10 attempts
74-
return fmt.Errorf("failed to determine if a bucket %s exists and you have permission to access it", bucketName)
74+
return fmt.Errorf("failed to determine if a bucket %s exists and you have permission to access it %v", bucketName, err)
7575
}
7676

7777
return nil

0 commit comments

Comments
 (0)