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

JavaV2: Add presignedURL PUT example that uses query params instead of headers #7087

Merged
merged 2 commits into from
Nov 29, 2024
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
33 changes: 31 additions & 2 deletions .doc_gen/metadata/s3_metadata.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2704,7 +2704,25 @@ s3_Scenario_PresignedUrl:
github: javav2/example_code/s3
sdkguide:
excerpts:
- description: Generate a pre-signed URL for an object, then download it (GET request).
- description: >-
<para>The following shows three example of how to create presigned URLs and use the URLs with
HTTP client libraries:</para>
<itemizedlist>
<listitem>
<para>An HTTP GET request that uses the URL with three HTTP client libraries</para>
</listitem>
<listitem>
<para>An HTTP PUT request with metadata in headers that uses the URL with three HTTP client
libraries</para>
</listitem>
<listitem>
<para>An HTTP PUT request with query parameters that uses the URL with one HTTP client
library</para>
</listitem>
</itemizedlist>
<para>
<emphasis role="bold">Generate a pre-signed URL for an object, then download it (GET request).</emphasis>
</para>
- description: Imports.
snippet_tags:
- presigned.java2.generatepresignedgeturlandretrieve.import
Expand All @@ -2722,7 +2740,7 @@ s3_Scenario_PresignedUrl:
snippet_tags:
- presigned.java2.generatepresignedgeturlandretrieve.sdkhttpclient

- description: Generate a pre-signed URL for an upload, then upload a file (PUT request).
- description: <emphasis role="bold">Generate a pre-signed URL with metadata in headers for an upload, then upload a file (PUT request).</emphasis>
- description: Imports.
snippet_tags:
- presigned.java2.generatepresignedurlandputfilewithmetadata.import
Expand All @@ -2739,6 +2757,17 @@ s3_Scenario_PresignedUrl:
- description: Use the &AWS; for Java V2 <code>SdkHttpClient</code> class to do the upload.
snippet_tags:
- presigned.java2.generatepresignedurlandputfilewithmetadata.sdkhttpclient

- description: <emphasis role="bold">Generate a pre-signed URL with query parameters for an upload, then upload a file (PUT request).</emphasis>
- description: Imports.
snippet_tags:
- presigned.java2.generatepresignedurlandputfilewithqueryparams.import
- description: Generate the URL.
snippet_tags:
- presigned.java2.generatepresignedurlandputfilewithqueryparams.createpresignedurl
- description: Use the &AWS; for Java V2 <code>SdkHttpClient</code> class to do the upload.
snippet_tags:
- presigned.java2.generatepresignedurlandputfilewithqueryparams.sdkhttpclient
Python:
versions:
- sdk_version: 3
Expand Down
3 changes: 2 additions & 1 deletion javav2/example_code/s3/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,8 @@ functions within the same service.

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

## Run the examples
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package com.example.s3;

// snippet-start:[presigned.java2.generatepresignedurlandputfilewithqueryparams.import]
import com.example.s3.util.PresignUrlUtils;
import org.slf4j.Logger;
import software.amazon.awssdk.awscore.AwsRequestOverrideConfiguration;
import software.amazon.awssdk.core.internal.sync.FileContentStreamProvider;
import software.amazon.awssdk.http.HttpExecuteRequest;
import software.amazon.awssdk.http.HttpExecuteResponse;
import software.amazon.awssdk.http.SdkHttpClient;
import software.amazon.awssdk.http.SdkHttpMethod;
import software.amazon.awssdk.http.SdkHttpRequest;
import software.amazon.awssdk.http.apache.ApacheHttpClient;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
import software.amazon.awssdk.services.s3.presigner.model.PresignedPutObjectRequest;
import software.amazon.awssdk.services.s3.presigner.model.PutObjectPresignRequest;

import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Paths;
import java.time.Duration;
import java.util.Map;
import java.util.UUID;
// snippet-end:[presigned.java2.generatepresignedurlandputfilewithqueryparams.import]

/**
* Before running this Java V2 code example, set up your development environment.
* <p>
* For more information, see the following documentation topic:
* <p>
* https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/get-started.html
*/

public class GeneratePresignedUrlAndPutFileWithQueryParams {
private static final Logger logger = org.slf4j.LoggerFactory.getLogger(GeneratePresignedUrlAndPutFileWithQueryParams.class);
private final static S3Client s3Client = S3Client.create();

public static void main(String[] args) {
String bucketName = "amzn-s3-demo-bucket" + UUID.randomUUID(); // Change bucket name.
String keyName = "key" + UUID.randomUUID();
String resourcePath = "uploadDirectory/file1.txt";
// Uncomment the following two lines and comment out the previous two lines to use an image file instead of a PDF file.
//String resourcePath = "image.png";
//String contentType = "image/png";

Map<String, String> queryParams = Map.of(
"x-amz-meta-author", "Bob",
"x-amz-meta-version", "1.0.0.0",
"x-amz-acl", "private",
"x-amz-server-side-encryption", "AES256"
);

PresignUrlUtils.createBucket(bucketName, s3Client);
GeneratePresignedUrlAndPutFileWithQueryParams presign = new GeneratePresignedUrlAndPutFileWithQueryParams();
try {
String presignedUrlString = presign.createPresignedUrl(bucketName, keyName, queryParams);
presign.useSdkHttpClientToPut(presignedUrlString, getFileForForClasspathResource(resourcePath));

} finally {
PresignUrlUtils.deleteObject(bucketName, keyName, s3Client);
PresignUrlUtils.deleteBucket(bucketName, s3Client);
}
}

// snippet-start:[presigned.java2.generatepresignedurlandputfilewithqueryparams.main]
// snippet-start:[presigned.java2.generatepresignedurlandputfilewithqueryparams.createpresignedurl]
/**
* Creates a presigned URL to use in a subsequent HTTP PUT request. The code adds query parameters
* to the request instead of using headers. By using query parameters, you do not need to add the
* the parameters as headers when the PUT request is eventually sent.
*
* @param bucketName Bucket name where the object will be uploaded.
* @param keyName Key name of the object that will be uploaded.
* @param queryParams Query string parameters to be added to the presigned URL.
* @return
*/
public String createPresignedUrl(String bucketName, String keyName, Map<String, String> queryParams) {
try (S3Presigner presigner = S3Presigner.create()) {
// Create an override configuration to store the query parameters.
AwsRequestOverrideConfiguration.Builder overrideConfigurationBuilder = AwsRequestOverrideConfiguration.builder();

queryParams.forEach(overrideConfigurationBuilder::putRawQueryParameter);

PutObjectRequest objectRequest = PutObjectRequest.builder()
.bucket(bucketName)
.key(keyName)
.overrideConfiguration(overrideConfigurationBuilder.build()) // Add the override configuration.
.build();

PutObjectPresignRequest presignRequest = PutObjectPresignRequest.builder()
.signatureDuration(Duration.ofMinutes(10)) // The URL expires in 10 minutes.
.putObjectRequest(objectRequest)
.build();


PresignedPutObjectRequest presignedRequest = presigner.presignPutObject(presignRequest);
String myURL = presignedRequest.url().toString();
logger.info("Presigned URL to upload a file to: [{}]", myURL);
logger.info("HTTP method: [{}]", presignedRequest.httpRequest().method());

return presignedRequest.url().toExternalForm();
}
}
// snippet-end:[presigned.java2.generatepresignedurlandputfilewithqueryparams.createpresignedurl]

// snippet-start:[presigned.java2.generatepresignedurlandputfilewithqueryparams.sdkhttpclient]
/**
* Use the AWS SDK for Java V2 SdkHttpClient class to execute the PUT request. Since the
* URL contains the query parameters, no headers are needed for metadata, SSE settings, or ACL settings.
*
* @param presignedUrlString The URL for the PUT request.
* @param fileToPut File to uplaod
*/
public void useSdkHttpClientToPut(String presignedUrlString, File fileToPut) {
logger.info("Begin [{}] upload", fileToPut.toString());

try {
URL presignedUrl = new URL(presignedUrlString);

SdkHttpRequest.Builder requestBuilder = SdkHttpRequest.builder()
.method(SdkHttpMethod.PUT)
.uri(presignedUrl.toURI());

SdkHttpRequest request = requestBuilder.build();

HttpExecuteRequest executeRequest = HttpExecuteRequest.builder()
.request(request)
.contentStreamProvider(new FileContentStreamProvider(fileToPut.toPath()))
.build();

try (SdkHttpClient sdkHttpClient = ApacheHttpClient.create()) {
HttpExecuteResponse response = sdkHttpClient.prepareRequest(executeRequest).call();
logger.info("Response code: {}", response.httpResponse().statusCode());
}
} catch (URISyntaxException | IOException e) {
logger.error(e.getMessage(), e);
}
}
// snippet-end:[presigned.java2.generatepresignedurlandputfilewithqueryparams.sdkhttpclient]
// snippet-end:[presigned.java2.generatepresignedurlandputfilewithqueryparams.main]

public static File getFileForForClasspathResource(String resourcePath) {
try {
URL resource = GeneratePresignedUrlAndUploadObject.class.getClassLoader().getResource(resourcePath);
return Paths.get(resource.toURI()).toFile();
} catch (URISyntaxException e) {
logger.error(e.getMessage(), e);
}
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package com.example.s3.presignurl;

import com.example.s3.GeneratePresignedUrlAndPutFileWithMetadata;
import com.example.s3.GeneratePresignedUrlAndPutFileWithQueryParams;
import com.example.s3.GeneratePresignedUrlAndUploadObject;
import com.example.s3.util.PresignUrlUtils;
import org.junit.jupiter.api.AfterAll;
Expand All @@ -16,14 +17,19 @@

import java.io.File;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

class GeneratePresignedPutUrlTests {
private static final String BUCKET_NAME = "b-" + UUID.randomUUID();
private static final String KEY_NAME = "k-" + UUID.randomUUID();
private static final S3Client s3Client = S3Client.create();
private static final Map<String, String> METADATA = Map.of("meta1", "value1");
private static final String METADATA_KEY = "meta1";
private static final String METADATA_VALUE = "value1";
private static final Map<String, String> METADATA = Map.of(METADATA_KEY, METADATA_VALUE);
private static final File PDF_FILE = GeneratePresignedUrlAndPutFileWithMetadata
.getFileForForClasspathResource("multipartUploadFiles/s3-userguide.pdf");

Expand Down Expand Up @@ -51,13 +57,63 @@ void testCreatePresignedUrlForPutString() {
Assertions.assertTrue(objectExists());
}

@Test
@Tag("IntegrationTest")
void test_create_presigned_url_using_query_params_does_not_add_to_signed_headers() {
Map<String, String> queryParams = Map.of(
"x-amz-meta-author", "Bob",
"x-amz-meta-version", "1.0.0.0",
"x-amz-acl", "private",
"x-amz-server-side-encryption", "AES256"
);

GeneratePresignedUrlAndPutFileWithQueryParams presign = new GeneratePresignedUrlAndPutFileWithQueryParams();
String presignedUrl = presign.createPresignedUrl(BUCKET_NAME, KEY_NAME, queryParams);

Map<String, String> queryStringMap = parseQueryString(presignedUrl);
Assertions.assertFalse(queryStringMap.get("X-Amz-SignedHeaders").contains("x-amz-meta-author"));
Assertions.assertFalse(queryStringMap.get("X-Amz-SignedHeaders").contains("x-amz-server-side-encryption"));
}

@Test
@Tag("IntegrationTest")
void test_create_presigned_url_using_query_params_works() {
Map<String, String> queryParams = Map.of(
"x-amz-meta-author", "Bob",
"x-amz-meta-version", "1.0.0.0",
"x-amz-server-side-encryption", "AES256"
);

GeneratePresignedUrlAndPutFileWithQueryParams classUnderTest = new GeneratePresignedUrlAndPutFileWithQueryParams();
String presignedUrl = classUnderTest.createPresignedUrl(BUCKET_NAME, KEY_NAME, queryParams);

classUnderTest.useSdkHttpClientToPut(presignedUrl, PDF_FILE);

try (S3Client s3Client = S3Client.create()){
s3Client.getObject(builder -> {
builder.bucket(BUCKET_NAME);
builder.key(KEY_NAME);
builder.build();
}, (response, stream) -> {
stream.abort();
Assertions.assertEquals("Bob", response.metadata().get("author"));
Assertions.assertEquals("1.0.0.0", response.metadata().get("version"));
Assertions.assertEquals("AES256", response.serverSideEncryptionAsString());
return null;
});
}
}

@Test
@Tag("IntegrationTest")
void testCreatePresignedUrlForPut() {
GeneratePresignedUrlAndPutFileWithMetadata presignInstanceUnderTest = new GeneratePresignedUrlAndPutFileWithMetadata();

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

@Test
Expand Down Expand Up @@ -106,4 +162,47 @@ private static Boolean objectHasMetadata() {
// The SDK strips off the leading "x-amz-meta-" from the metadata key.
return response.metadata().get("meta1").equals(GeneratePresignedPutUrlTests.METADATA.get("meta1"));
}

public static Map<String, String> parseQueryString(String queryString) {
Map<String, String> params = new HashMap<>();

// Return empty map for null or empty input
if (queryString == null || queryString.isEmpty()) {
return params;
}

// Split the query string into key-value pairs
String[] pairs = queryString.split("&");

for (String pair : pairs) {
// Find the separator between key and value
int separatorIndex = pair.indexOf("=");

// Handle key-only parameters (no value)
if (separatorIndex <= 0) {
params.put(URLDecoder.decode(pair, StandardCharsets.UTF_8), null);
continue;
}

// Extract and decode the key
String key = URLDecoder.decode(
pair.substring(0, separatorIndex),
StandardCharsets.UTF_8
);

// Extract and decode the value if it exists
String value = null;
if (pair.length() > separatorIndex + 1) {
value = URLDecoder.decode(
pair.substring(separatorIndex + 1),
StandardCharsets.UTF_8
);
}

params.put(key, value);
}

return params;
}

}
Loading