Skip to content

Commit 5bd7c5e

Browse files
authored
JavaV2: Add presignedURL PUT example that uses query params instead of headers (#7087)
Add example and test for using query params instead of headers on presigned urls V1516533081
1 parent 539ed9b commit 5bd7c5e

File tree

4 files changed

+292
-5
lines changed

4 files changed

+292
-5
lines changed

.doc_gen/metadata/s3_metadata.yaml

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2704,7 +2704,25 @@ s3_Scenario_PresignedUrl:
27042704
github: javav2/example_code/s3
27052705
sdkguide:
27062706
excerpts:
2707-
- description: Generate a pre-signed URL for an object, then download it (GET request).
2707+
- description: >-
2708+
<para>The following shows three example of how to create presigned URLs and use the URLs with
2709+
HTTP client libraries:</para>
2710+
<itemizedlist>
2711+
<listitem>
2712+
<para>An HTTP GET request that uses the URL with three HTTP client libraries</para>
2713+
</listitem>
2714+
<listitem>
2715+
<para>An HTTP PUT request with metadata in headers that uses the URL with three HTTP client
2716+
libraries</para>
2717+
</listitem>
2718+
<listitem>
2719+
<para>An HTTP PUT request with query parameters that uses the URL with one HTTP client
2720+
library</para>
2721+
</listitem>
2722+
</itemizedlist>
2723+
<para>
2724+
<emphasis role="bold">Generate a pre-signed URL for an object, then download it (GET request).</emphasis>
2725+
</para>
27082726
- description: Imports.
27092727
snippet_tags:
27102728
- presigned.java2.generatepresignedgeturlandretrieve.import
@@ -2722,7 +2740,7 @@ s3_Scenario_PresignedUrl:
27222740
snippet_tags:
27232741
- presigned.java2.generatepresignedgeturlandretrieve.sdkhttpclient
27242742

2725-
- description: Generate a pre-signed URL for an upload, then upload a file (PUT request).
2743+
- description: <emphasis role="bold">Generate a pre-signed URL with metadata in headers for an upload, then upload a file (PUT request).</emphasis>
27262744
- description: Imports.
27272745
snippet_tags:
27282746
- presigned.java2.generatepresignedurlandputfilewithmetadata.import
@@ -2739,6 +2757,17 @@ s3_Scenario_PresignedUrl:
27392757
- description: Use the &AWS; for Java V2 <code>SdkHttpClient</code> class to do the upload.
27402758
snippet_tags:
27412759
- presigned.java2.generatepresignedurlandputfilewithmetadata.sdkhttpclient
2760+
2761+
- description: <emphasis role="bold">Generate a pre-signed URL with query parameters for an upload, then upload a file (PUT request).</emphasis>
2762+
- description: Imports.
2763+
snippet_tags:
2764+
- presigned.java2.generatepresignedurlandputfilewithqueryparams.import
2765+
- description: Generate the URL.
2766+
snippet_tags:
2767+
- presigned.java2.generatepresignedurlandputfilewithqueryparams.createpresignedurl
2768+
- description: Use the &AWS; for Java V2 <code>SdkHttpClient</code> class to do the upload.
2769+
snippet_tags:
2770+
- presigned.java2.generatepresignedurlandputfilewithqueryparams.sdkhttpclient
27422771
Python:
27432772
versions:
27442773
- sdk_version: 3

javav2/example_code/s3/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,8 @@ functions within the same service.
9898

9999
<!--custom.examples.start-->
100100
- [Create a presigned URL for download](src/main/java/com/example/s3/GeneratePresignedGetUrlAndRetrieve.java)
101-
- [Create a presigned URL for upload](src/main/java/com/example/s3/GeneratePresignedUrlAndPutFileWithMetadata.java)
101+
- [Create a presigned URL with metadata in headers for upload](src/main/java/com/example/s3/GeneratePresignedUrlAndPutFileWithMetadata.java)
102+
- [Create a presigned URL with query parameters for upload](src/main/java/com/example/s3/GeneratePresignedUrlAndPutFileWithQueryParams.java)
102103
<!--custom.examples.end-->
103104

104105
## Run the examples
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package com.example.s3;
5+
6+
// snippet-start:[presigned.java2.generatepresignedurlandputfilewithqueryparams.import]
7+
import com.example.s3.util.PresignUrlUtils;
8+
import org.slf4j.Logger;
9+
import software.amazon.awssdk.awscore.AwsRequestOverrideConfiguration;
10+
import software.amazon.awssdk.core.internal.sync.FileContentStreamProvider;
11+
import software.amazon.awssdk.http.HttpExecuteRequest;
12+
import software.amazon.awssdk.http.HttpExecuteResponse;
13+
import software.amazon.awssdk.http.SdkHttpClient;
14+
import software.amazon.awssdk.http.SdkHttpMethod;
15+
import software.amazon.awssdk.http.SdkHttpRequest;
16+
import software.amazon.awssdk.http.apache.ApacheHttpClient;
17+
import software.amazon.awssdk.services.s3.S3Client;
18+
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
19+
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
20+
import software.amazon.awssdk.services.s3.presigner.model.PresignedPutObjectRequest;
21+
import software.amazon.awssdk.services.s3.presigner.model.PutObjectPresignRequest;
22+
23+
import java.io.File;
24+
import java.io.IOException;
25+
import java.net.URISyntaxException;
26+
import java.net.URL;
27+
import java.nio.file.Paths;
28+
import java.time.Duration;
29+
import java.util.Map;
30+
import java.util.UUID;
31+
// snippet-end:[presigned.java2.generatepresignedurlandputfilewithqueryparams.import]
32+
33+
/**
34+
* Before running this Java V2 code example, set up your development environment.
35+
* <p>
36+
* For more information, see the following documentation topic:
37+
* <p>
38+
* https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/get-started.html
39+
*/
40+
41+
public class GeneratePresignedUrlAndPutFileWithQueryParams {
42+
private static final Logger logger = org.slf4j.LoggerFactory.getLogger(GeneratePresignedUrlAndPutFileWithQueryParams.class);
43+
private final static S3Client s3Client = S3Client.create();
44+
45+
public static void main(String[] args) {
46+
String bucketName = "amzn-s3-demo-bucket" + UUID.randomUUID(); // Change bucket name.
47+
String keyName = "key" + UUID.randomUUID();
48+
String resourcePath = "uploadDirectory/file1.txt";
49+
// Uncomment the following two lines and comment out the previous two lines to use an image file instead of a PDF file.
50+
//String resourcePath = "image.png";
51+
//String contentType = "image/png";
52+
53+
Map<String, String> queryParams = Map.of(
54+
"x-amz-meta-author", "Bob",
55+
"x-amz-meta-version", "1.0.0.0",
56+
"x-amz-acl", "private",
57+
"x-amz-server-side-encryption", "AES256"
58+
);
59+
60+
PresignUrlUtils.createBucket(bucketName, s3Client);
61+
GeneratePresignedUrlAndPutFileWithQueryParams presign = new GeneratePresignedUrlAndPutFileWithQueryParams();
62+
try {
63+
String presignedUrlString = presign.createPresignedUrl(bucketName, keyName, queryParams);
64+
presign.useSdkHttpClientToPut(presignedUrlString, getFileForForClasspathResource(resourcePath));
65+
66+
} finally {
67+
PresignUrlUtils.deleteObject(bucketName, keyName, s3Client);
68+
PresignUrlUtils.deleteBucket(bucketName, s3Client);
69+
}
70+
}
71+
72+
// snippet-start:[presigned.java2.generatepresignedurlandputfilewithqueryparams.main]
73+
// snippet-start:[presigned.java2.generatepresignedurlandputfilewithqueryparams.createpresignedurl]
74+
/**
75+
* Creates a presigned URL to use in a subsequent HTTP PUT request. The code adds query parameters
76+
* to the request instead of using headers. By using query parameters, you do not need to add the
77+
* the parameters as headers when the PUT request is eventually sent.
78+
*
79+
* @param bucketName Bucket name where the object will be uploaded.
80+
* @param keyName Key name of the object that will be uploaded.
81+
* @param queryParams Query string parameters to be added to the presigned URL.
82+
* @return
83+
*/
84+
public String createPresignedUrl(String bucketName, String keyName, Map<String, String> queryParams) {
85+
try (S3Presigner presigner = S3Presigner.create()) {
86+
// Create an override configuration to store the query parameters.
87+
AwsRequestOverrideConfiguration.Builder overrideConfigurationBuilder = AwsRequestOverrideConfiguration.builder();
88+
89+
queryParams.forEach(overrideConfigurationBuilder::putRawQueryParameter);
90+
91+
PutObjectRequest objectRequest = PutObjectRequest.builder()
92+
.bucket(bucketName)
93+
.key(keyName)
94+
.overrideConfiguration(overrideConfigurationBuilder.build()) // Add the override configuration.
95+
.build();
96+
97+
PutObjectPresignRequest presignRequest = PutObjectPresignRequest.builder()
98+
.signatureDuration(Duration.ofMinutes(10)) // The URL expires in 10 minutes.
99+
.putObjectRequest(objectRequest)
100+
.build();
101+
102+
103+
PresignedPutObjectRequest presignedRequest = presigner.presignPutObject(presignRequest);
104+
String myURL = presignedRequest.url().toString();
105+
logger.info("Presigned URL to upload a file to: [{}]", myURL);
106+
logger.info("HTTP method: [{}]", presignedRequest.httpRequest().method());
107+
108+
return presignedRequest.url().toExternalForm();
109+
}
110+
}
111+
// snippet-end:[presigned.java2.generatepresignedurlandputfilewithqueryparams.createpresignedurl]
112+
113+
// snippet-start:[presigned.java2.generatepresignedurlandputfilewithqueryparams.sdkhttpclient]
114+
/**
115+
* Use the AWS SDK for Java V2 SdkHttpClient class to execute the PUT request. Since the
116+
* URL contains the query parameters, no headers are needed for metadata, SSE settings, or ACL settings.
117+
*
118+
* @param presignedUrlString The URL for the PUT request.
119+
* @param fileToPut File to uplaod
120+
*/
121+
public void useSdkHttpClientToPut(String presignedUrlString, File fileToPut) {
122+
logger.info("Begin [{}] upload", fileToPut.toString());
123+
124+
try {
125+
URL presignedUrl = new URL(presignedUrlString);
126+
127+
SdkHttpRequest.Builder requestBuilder = SdkHttpRequest.builder()
128+
.method(SdkHttpMethod.PUT)
129+
.uri(presignedUrl.toURI());
130+
131+
SdkHttpRequest request = requestBuilder.build();
132+
133+
HttpExecuteRequest executeRequest = HttpExecuteRequest.builder()
134+
.request(request)
135+
.contentStreamProvider(new FileContentStreamProvider(fileToPut.toPath()))
136+
.build();
137+
138+
try (SdkHttpClient sdkHttpClient = ApacheHttpClient.create()) {
139+
HttpExecuteResponse response = sdkHttpClient.prepareRequest(executeRequest).call();
140+
logger.info("Response code: {}", response.httpResponse().statusCode());
141+
}
142+
} catch (URISyntaxException | IOException e) {
143+
logger.error(e.getMessage(), e);
144+
}
145+
}
146+
// snippet-end:[presigned.java2.generatepresignedurlandputfilewithqueryparams.sdkhttpclient]
147+
// snippet-end:[presigned.java2.generatepresignedurlandputfilewithqueryparams.main]
148+
149+
public static File getFileForForClasspathResource(String resourcePath) {
150+
try {
151+
URL resource = GeneratePresignedUrlAndUploadObject.class.getClassLoader().getResource(resourcePath);
152+
return Paths.get(resource.toURI()).toFile();
153+
} catch (URISyntaxException e) {
154+
logger.error(e.getMessage(), e);
155+
}
156+
return null;
157+
}
158+
}

javav2/example_code/s3/src/test/java/com/example/s3/presignurl/GeneratePresignedPutUrlTests.java

Lines changed: 101 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package com.example.s3.presignurl;
55

66
import com.example.s3.GeneratePresignedUrlAndPutFileWithMetadata;
7+
import com.example.s3.GeneratePresignedUrlAndPutFileWithQueryParams;
78
import com.example.s3.GeneratePresignedUrlAndUploadObject;
89
import com.example.s3.util.PresignUrlUtils;
910
import org.junit.jupiter.api.AfterAll;
@@ -16,14 +17,19 @@
1617

1718
import java.io.File;
1819
import java.net.URL;
20+
import java.net.URLDecoder;
21+
import java.nio.charset.StandardCharsets;
22+
import java.util.HashMap;
1923
import java.util.Map;
2024
import java.util.UUID;
2125

2226
class GeneratePresignedPutUrlTests {
2327
private static final String BUCKET_NAME = "b-" + UUID.randomUUID();
2428
private static final String KEY_NAME = "k-" + UUID.randomUUID();
2529
private static final S3Client s3Client = S3Client.create();
26-
private static final Map<String, String> METADATA = Map.of("meta1", "value1");
30+
private static final String METADATA_KEY = "meta1";
31+
private static final String METADATA_VALUE = "value1";
32+
private static final Map<String, String> METADATA = Map.of(METADATA_KEY, METADATA_VALUE);
2733
private static final File PDF_FILE = GeneratePresignedUrlAndPutFileWithMetadata
2834
.getFileForForClasspathResource("multipartUploadFiles/s3-userguide.pdf");
2935

@@ -51,13 +57,63 @@ void testCreatePresignedUrlForPutString() {
5157
Assertions.assertTrue(objectExists());
5258
}
5359

60+
@Test
61+
@Tag("IntegrationTest")
62+
void test_create_presigned_url_using_query_params_does_not_add_to_signed_headers() {
63+
Map<String, String> queryParams = Map.of(
64+
"x-amz-meta-author", "Bob",
65+
"x-amz-meta-version", "1.0.0.0",
66+
"x-amz-acl", "private",
67+
"x-amz-server-side-encryption", "AES256"
68+
);
69+
70+
GeneratePresignedUrlAndPutFileWithQueryParams presign = new GeneratePresignedUrlAndPutFileWithQueryParams();
71+
String presignedUrl = presign.createPresignedUrl(BUCKET_NAME, KEY_NAME, queryParams);
72+
73+
Map<String, String> queryStringMap = parseQueryString(presignedUrl);
74+
Assertions.assertFalse(queryStringMap.get("X-Amz-SignedHeaders").contains("x-amz-meta-author"));
75+
Assertions.assertFalse(queryStringMap.get("X-Amz-SignedHeaders").contains("x-amz-server-side-encryption"));
76+
}
77+
78+
@Test
79+
@Tag("IntegrationTest")
80+
void test_create_presigned_url_using_query_params_works() {
81+
Map<String, String> queryParams = Map.of(
82+
"x-amz-meta-author", "Bob",
83+
"x-amz-meta-version", "1.0.0.0",
84+
"x-amz-server-side-encryption", "AES256"
85+
);
86+
87+
GeneratePresignedUrlAndPutFileWithQueryParams classUnderTest = new GeneratePresignedUrlAndPutFileWithQueryParams();
88+
String presignedUrl = classUnderTest.createPresignedUrl(BUCKET_NAME, KEY_NAME, queryParams);
89+
90+
classUnderTest.useSdkHttpClientToPut(presignedUrl, PDF_FILE);
91+
92+
try (S3Client s3Client = S3Client.create()){
93+
s3Client.getObject(builder -> {
94+
builder.bucket(BUCKET_NAME);
95+
builder.key(KEY_NAME);
96+
builder.build();
97+
}, (response, stream) -> {
98+
stream.abort();
99+
Assertions.assertEquals("Bob", response.metadata().get("author"));
100+
Assertions.assertEquals("1.0.0.0", response.metadata().get("version"));
101+
Assertions.assertEquals("AES256", response.serverSideEncryptionAsString());
102+
return null;
103+
});
104+
}
105+
}
106+
54107
@Test
55108
@Tag("IntegrationTest")
56109
void testCreatePresignedUrlForPut() {
57110
GeneratePresignedUrlAndPutFileWithMetadata presignInstanceUnderTest = new GeneratePresignedUrlAndPutFileWithMetadata();
58111

59112
final String presignedUrlString = presignInstanceUnderTest.createPresignedUrl(BUCKET_NAME, KEY_NAME, METADATA);
60-
Assertions.assertTrue(presignedUrlString.contains("meta1"));
113+
final Map<String, String> queryParamsMap = parseQueryString(presignedUrlString);
114+
// Assert that the metadata key, but not the value, is in the signed headers.
115+
Assertions.assertTrue(queryParamsMap.get("X-Amz-SignedHeaders").contains("x-amz-meta-" + METADATA_KEY));
116+
Assertions.assertFalse(queryParamsMap.get("X-Amz-SignedHeaders").contains("x-amz-meta-" + METADATA_VALUE));
61117
}
62118

63119
@Test
@@ -106,4 +162,47 @@ private static Boolean objectHasMetadata() {
106162
// The SDK strips off the leading "x-amz-meta-" from the metadata key.
107163
return response.metadata().get("meta1").equals(GeneratePresignedPutUrlTests.METADATA.get("meta1"));
108164
}
165+
166+
public static Map<String, String> parseQueryString(String queryString) {
167+
Map<String, String> params = new HashMap<>();
168+
169+
// Return empty map for null or empty input
170+
if (queryString == null || queryString.isEmpty()) {
171+
return params;
172+
}
173+
174+
// Split the query string into key-value pairs
175+
String[] pairs = queryString.split("&");
176+
177+
for (String pair : pairs) {
178+
// Find the separator between key and value
179+
int separatorIndex = pair.indexOf("=");
180+
181+
// Handle key-only parameters (no value)
182+
if (separatorIndex <= 0) {
183+
params.put(URLDecoder.decode(pair, StandardCharsets.UTF_8), null);
184+
continue;
185+
}
186+
187+
// Extract and decode the key
188+
String key = URLDecoder.decode(
189+
pair.substring(0, separatorIndex),
190+
StandardCharsets.UTF_8
191+
);
192+
193+
// Extract and decode the value if it exists
194+
String value = null;
195+
if (pair.length() > separatorIndex + 1) {
196+
value = URLDecoder.decode(
197+
pair.substring(separatorIndex + 1),
198+
StandardCharsets.UTF_8
199+
);
200+
}
201+
202+
params.put(key, value);
203+
}
204+
205+
return params;
206+
}
207+
109208
}

0 commit comments

Comments
 (0)