diff --git a/.doc_gen/metadata/s3-directory-buckets_metadata.yaml b/.doc_gen/metadata/s3-directory-buckets_metadata.yaml index 11acfe6708b..a0809b38e6b 100644 --- a/.doc_gen/metadata/s3-directory-buckets_metadata.yaml +++ b/.doc_gen/metadata/s3-directory-buckets_metadata.yaml @@ -418,6 +418,18 @@ s3-directory-buckets_Scenario_ExpressBasics: - Prompt the user to see if they want to clean up the resources. category: Basics languages: + Java: + versions: + - sdk_version: 2 + github: javav2/example_code/s3 + sdkguide: + excerpts: + - description: Run an interactive scenario demonstrating &S3; features. + snippet_tags: + - s3.java2.directories.scenario.main + - description: A wrapper class for &S3; SDK methods. + snippet_tags: + - s3.java2.directories.actions.main PHP: versions: - sdk_version: 3 diff --git a/javav2/example_code/s3/pom.xml b/javav2/example_code/s3/pom.xml index 957e73c9091..7c41c1230bb 100644 --- a/javav2/example_code/s3/pom.xml +++ b/javav2/example_code/s3/pom.xml @@ -157,6 +157,10 @@ software.amazon.awssdk iam + + software.amazon.awssdk + ec2 + org.apache.logging.log4j log4j-core diff --git a/javav2/example_code/s3/src/main/java/com/example/s3/directorybucket/DeleteDirectoryBucketObjects.java b/javav2/example_code/s3/src/main/java/com/example/s3/directorybucket/DeleteDirectoryBucketObjects.java index ab5bf43b54b..917c6ea264f 100644 --- a/javav2/example_code/s3/src/main/java/com/example/s3/directorybucket/DeleteDirectoryBucketObjects.java +++ b/javav2/example_code/s3/src/main/java/com/example/s3/directorybucket/DeleteDirectoryBucketObjects.java @@ -69,23 +69,20 @@ public static void deleteDirectoryBucketObjects(S3Client s3Client, String bucket logger.info("Deleting objects from bucket: {}", bucketName); try { - // Create a list of ObjectIdentifier + // Create a list of ObjectIdentifier. List identifiers = objectKeys.stream() .map(key -> ObjectIdentifier.builder().key(key).build()) .toList(); - // Create a Delete object Delete delete = Delete.builder() .objects(identifiers) .build(); - // Create a DeleteObjectsRequest DeleteObjectsRequest deleteObjectsRequest = DeleteObjectsRequest.builder() .bucket(bucketName) .delete(delete) .build(); - // Delete the objects DeleteObjectsResponse deleteObjectsResponse = s3Client.deleteObjects(deleteObjectsRequest); deleteObjectsResponse.deleted().forEach(deleted -> logger.info("Deleted object: {}", deleted.key())); diff --git a/javav2/example_code/s3/src/main/java/com/example/s3/express/CloudFormationHelper.java b/javav2/example_code/s3/src/main/java/com/example/s3/express/CloudFormationHelper.java new file mode 100644 index 00000000000..77d31b9e710 --- /dev/null +++ b/javav2/example_code/s3/src/main/java/com/example/s3/express/CloudFormationHelper.java @@ -0,0 +1,162 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.example.s3.express; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; +import software.amazon.awssdk.core.retry.RetryMode; +import software.amazon.awssdk.http.async.SdkAsyncHttpClient; +import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient; +import software.amazon.awssdk.services.cloudformation.CloudFormationAsyncClient; +import software.amazon.awssdk.services.cloudformation.model.Capability; +import software.amazon.awssdk.services.cloudformation.model.CloudFormationException; +import software.amazon.awssdk.services.cloudformation.model.DescribeStacksRequest; +import software.amazon.awssdk.services.cloudformation.model.DescribeStacksResponse; +import software.amazon.awssdk.services.cloudformation.model.Output; +import software.amazon.awssdk.services.cloudformation.model.Stack; +import software.amazon.awssdk.services.cloudformation.waiters.CloudFormationAsyncWaiter; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Duration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +public class CloudFormationHelper { + private static final String CFN_TEMPLATE = "s3_express_template.yaml"; + private static final Logger logger = LoggerFactory.getLogger(CloudFormationHelper.class); + + private static CloudFormationAsyncClient cloudFormationClient; + + private static CloudFormationAsyncClient getCloudFormationClient() { + if (cloudFormationClient == null) { + SdkAsyncHttpClient httpClient = NettyNioAsyncHttpClient.builder() + .maxConcurrency(100) + .connectionTimeout(Duration.ofSeconds(60)) + .readTimeout(Duration.ofSeconds(60)) + .writeTimeout(Duration.ofSeconds(60)) + .build(); + + ClientOverrideConfiguration overrideConfig = ClientOverrideConfiguration.builder() + .apiCallTimeout(Duration.ofMinutes(2)) + .apiCallAttemptTimeout(Duration.ofSeconds(90)) + .retryStrategy(RetryMode.STANDARD) + .build(); + + cloudFormationClient = CloudFormationAsyncClient.builder() + .httpClient(httpClient) + .overrideConfiguration(overrideConfig) + .build(); + } + return cloudFormationClient; + } + + public static void deployCloudFormationStack(String stackName) { + String templateBody; + boolean doesExist = describeStack(stackName); + if (!doesExist) { + try { + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + Path filePath = Paths.get(classLoader.getResource(CFN_TEMPLATE).toURI()); + templateBody = Files.readString(filePath); + } catch (IOException | URISyntaxException e) { + throw new RuntimeException(e); + } + + getCloudFormationClient().createStack(b -> b.stackName(stackName) + .templateBody(templateBody) + .capabilities(Capability.CAPABILITY_IAM)) + .whenComplete((csr, t) -> { + if (csr != null) { + System.out.println("Stack creation requested, ARN is " + csr.stackId()); + try (CloudFormationAsyncWaiter waiter = getCloudFormationClient().waiter()) { + waiter.waitUntilStackCreateComplete(request -> request.stackName(stackName)) + .whenComplete((dsr, th) -> { + if (th != null) { + System.out.println("Error waiting for stack creation: " + th.getMessage()); + } else { + dsr.matched().response().orElseThrow(() -> new RuntimeException("Failed to deploy")); + System.out.println("Stack created successfully"); + } + }).join(); + } + } else { + System.out.format("Error creating stack: " + t.getMessage(), t); + throw new RuntimeException(t.getCause().getMessage(), t); + } + }).join(); + } else { + logger.info("{} stack already exists", CFN_TEMPLATE); + } + } + + // Check to see if the Stack exists before deploying it + public static Boolean describeStack(String stackName) { + try { + CompletableFuture future = getCloudFormationClient().describeStacks(); + DescribeStacksResponse stacksResponse = (DescribeStacksResponse) future.join(); + List stacks = stacksResponse.stacks(); + for (Stack myStack : stacks) { + if (myStack.stackName().compareTo(stackName) == 0) { + return true; + } + } + } catch (CloudFormationException e) { + System.err.println(e.getMessage()); + } + return false; + } + + public static void destroyCloudFormationStack(String stackName) { + getCloudFormationClient().deleteStack(b -> b.stackName(stackName)) + .whenComplete((dsr, t) -> { + if (dsr != null) { + System.out.println("Delete stack requested ...."); + try (CloudFormationAsyncWaiter waiter = getCloudFormationClient().waiter()) { + waiter.waitUntilStackDeleteComplete(request -> request.stackName(stackName)) + .whenComplete((waiterResponse, throwable) -> + System.out.println("Stack deleted successfully.")) + .join(); + } + } else { + System.out.format("Error deleting stack: " + t.getMessage(), t); + throw new RuntimeException(t.getCause().getMessage(), t); + } + }).join(); + } + + public static CompletableFuture> getStackOutputsAsync(String stackName) { + CloudFormationAsyncClient cloudFormationAsyncClient = getCloudFormationClient(); + + DescribeStacksRequest describeStacksRequest = DescribeStacksRequest.builder() + .stackName(stackName) + .build(); + + return cloudFormationAsyncClient.describeStacks(describeStacksRequest) + .handle((describeStacksResponse, throwable) -> { + if (throwable != null) { + throw new RuntimeException("Failed to get stack outputs for: " + stackName, throwable); + } + + // Process the result + if (describeStacksResponse.stacks().isEmpty()) { + throw new RuntimeException("Stack not found: " + stackName); + } + + Stack stack = describeStacksResponse.stacks().get(0); + Map outputs = new HashMap<>(); + for (Output output : stack.outputs()) { + outputs.put(output.outputKey(), output.outputValue()); + } + + return outputs; + }); + } +} diff --git a/javav2/example_code/s3/src/main/java/com/example/s3/express/S3DirectoriesActions.java b/javav2/example_code/s3/src/main/java/com/example/s3/express/S3DirectoriesActions.java new file mode 100644 index 00000000000..71633eec7bd --- /dev/null +++ b/javav2/example_code/s3/src/main/java/com/example/s3/express/S3DirectoriesActions.java @@ -0,0 +1,572 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.example.s3.express; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import software.amazon.awssdk.core.ResponseBytes; +import software.amazon.awssdk.core.async.AsyncRequestBody; +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; +import software.amazon.awssdk.core.retry.RetryMode; +import software.amazon.awssdk.core.waiters.WaiterResponse; +import software.amazon.awssdk.http.async.SdkAsyncHttpClient; +import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.ec2.Ec2AsyncClient; +import software.amazon.awssdk.services.ec2.model.AvailabilityZone; +import software.amazon.awssdk.services.ec2.model.CreateVpcEndpointRequest; +import software.amazon.awssdk.services.ec2.model.CreateVpcRequest; +import software.amazon.awssdk.services.ec2.model.DescribeAvailabilityZonesRequest; +import software.amazon.awssdk.services.ec2.model.DescribeRouteTablesRequest; +import software.amazon.awssdk.services.ec2.model.DescribeVpcsRequest; +import software.amazon.awssdk.services.ec2.model.Ec2Exception; +import software.amazon.awssdk.services.ec2.model.Filter; +import software.amazon.awssdk.services.ec2.waiters.Ec2AsyncWaiter; +import software.amazon.awssdk.services.iam.IamAsyncClient; +import software.amazon.awssdk.services.iam.model.CreateAccessKeyRequest; +import software.amazon.awssdk.services.iam.model.CreateAccessKeyResponse; +import software.amazon.awssdk.services.iam.model.IamException; +import software.amazon.awssdk.services.s3.S3AsyncClient; +import software.amazon.awssdk.services.s3.model.BucketAlreadyExistsException; +import software.amazon.awssdk.services.s3.model.BucketInfo; +import software.amazon.awssdk.services.s3.model.BucketType; +import software.amazon.awssdk.services.s3.model.CopyObjectRequest; +import software.amazon.awssdk.services.s3.model.CreateBucketConfiguration; +import software.amazon.awssdk.services.s3.model.CreateBucketRequest; +import software.amazon.awssdk.services.s3.model.CreateBucketResponse; +import software.amazon.awssdk.services.s3.model.CreateSessionRequest; +import software.amazon.awssdk.services.s3.model.CreateSessionResponse; +import software.amazon.awssdk.services.s3.model.DataRedundancy; +import software.amazon.awssdk.services.s3.model.Delete; +import software.amazon.awssdk.services.s3.model.DeleteBucketRequest; +import software.amazon.awssdk.services.s3.model.DeleteObjectsRequest; +import software.amazon.awssdk.services.s3.model.GetObjectRequest; +import software.amazon.awssdk.services.s3.model.GetObjectResponse; +import software.amazon.awssdk.services.s3.model.HeadBucketRequest; +import software.amazon.awssdk.services.s3.model.HeadBucketResponse; +import software.amazon.awssdk.services.s3.model.ListObjectsV2Request; +import software.amazon.awssdk.services.s3.model.LocationInfo; +import software.amazon.awssdk.services.s3.model.LocationType; +import software.amazon.awssdk.services.s3.model.NoSuchBucketException; +import software.amazon.awssdk.services.s3.model.NoSuchKeyException; +import software.amazon.awssdk.services.s3.model.ObjectIdentifier; +import software.amazon.awssdk.services.s3.model.PutObjectResponse; +import software.amazon.awssdk.services.s3.model.S3Object; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; +import software.amazon.awssdk.services.s3.model.S3Exception; +import software.amazon.awssdk.services.s3.waiters.S3AsyncWaiter; +import software.amazon.awssdk.core.async.AsyncResponseTransformer; +import java.time.Duration; +import java.util.AbstractMap; +import java.util.List; +import java.util.Scanner; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +// snippet-start:[s3.java2.directories.actions.main] +public class S3DirectoriesActions { + + private static IamAsyncClient iamAsyncClient; + + private static Ec2AsyncClient ec2AsyncClient; + private static final Logger logger = LoggerFactory.getLogger(S3DirectoriesActions.class); + + private static IamAsyncClient getIAMAsyncClient() { + if (iamAsyncClient == null) { + SdkAsyncHttpClient httpClient = NettyNioAsyncHttpClient.builder() + .maxConcurrency(100) + .connectionTimeout(Duration.ofSeconds(60)) + .readTimeout(Duration.ofSeconds(60)) + .writeTimeout(Duration.ofSeconds(60)) + .build(); + + ClientOverrideConfiguration overrideConfig = ClientOverrideConfiguration.builder() + .apiCallTimeout(Duration.ofMinutes(2)) + .apiCallAttemptTimeout(Duration.ofSeconds(90)) + .retryStrategy(RetryMode.STANDARD) + .build(); + + iamAsyncClient = IamAsyncClient.builder() + .httpClient(httpClient) + .overrideConfiguration(overrideConfig) + .build(); + } + return iamAsyncClient; + } + + private static Ec2AsyncClient getEc2AsyncClient() { + if (ec2AsyncClient == null) { + SdkAsyncHttpClient httpClient = NettyNioAsyncHttpClient.builder() + .maxConcurrency(100) + .connectionTimeout(Duration.ofSeconds(60)) + .readTimeout(Duration.ofSeconds(60)) + .writeTimeout(Duration.ofSeconds(60)) + .build(); + + ClientOverrideConfiguration overrideConfig = ClientOverrideConfiguration.builder() + .apiCallTimeout(Duration.ofMinutes(2)) + .apiCallAttemptTimeout(Duration.ofSeconds(90)) + .retryStrategy(RetryMode.STANDARD) + .build(); + + ec2AsyncClient = Ec2AsyncClient.builder() + .httpClient(httpClient) + .region(Region.US_WEST_2) + .overrideConfiguration(overrideConfig) + .build(); + } + return ec2AsyncClient; + } + + /** + * Deletes the specified S3 bucket and all the objects within it asynchronously. + * + * @param s3AsyncClient the S3 asynchronous client to use for the operations + * @param bucketName the name of the S3 bucket to be deleted + * @return a {@link CompletableFuture} that completes with a {@link WaiterResponse} containing the + * {@link HeadBucketResponse} when the bucket has been successfully deleted + * @throws CompletionException if there was an error deleting the bucket or its objects + */ + public CompletableFuture> deleteBucketAndObjectsAsync(S3AsyncClient s3AsyncClient, String bucketName) { + ListObjectsV2Request listRequest = ListObjectsV2Request.builder() + .bucket(bucketName) + .build(); + + return s3AsyncClient.listObjectsV2(listRequest) + .thenCompose(listResponse -> { + if (!listResponse.contents().isEmpty()) { + List objectIdentifiers = listResponse.contents().stream() + .map(s3Object -> ObjectIdentifier.builder().key(s3Object.key()).build()) + .collect(Collectors.toList()); + + DeleteObjectsRequest deleteRequest = DeleteObjectsRequest.builder() + .bucket(bucketName) + .delete(Delete.builder().objects(objectIdentifiers).build()) + .build(); + + return s3AsyncClient.deleteObjects(deleteRequest) + .thenAccept(deleteResponse -> { + if (!deleteResponse.errors().isEmpty()) { + deleteResponse.errors().forEach(error -> + logger.error("Couldn't delete object " + error.key() + ". Reason: " + error.message())); + } + }); + } + return CompletableFuture.completedFuture(null); + }) + .thenCompose(ignored -> { + DeleteBucketRequest deleteBucketRequest = DeleteBucketRequest.builder() + .bucket(bucketName) + .build(); + return s3AsyncClient.deleteBucket(deleteBucketRequest); + }) + .thenCompose(ignored -> { + S3AsyncWaiter waiter = s3AsyncClient.waiter(); + HeadBucketRequest headBucketRequest = HeadBucketRequest.builder().bucket(bucketName).build(); + return waiter.waitUntilBucketNotExists(headBucketRequest); + }) + .whenComplete((ignored, exception) -> { + if (exception != null) { + Throwable cause = exception.getCause(); + if (cause instanceof S3Exception) { + throw new CompletionException("Error deleting bucket: " + bucketName, cause); + } + throw new CompletionException("Failed to delete bucket and objects: " + bucketName, exception); + } + logger.info("Bucket deleted successfully: " + bucketName); + }); + } + + /** + * Lists the objects in an S3 bucket asynchronously. + * + * @param s3Client the S3 async client to use for the operation + * @param bucketName the name of the S3 bucket containing the objects to list + * @return a {@link CompletableFuture} that contains the list of object keys in the specified bucket + */ + public CompletableFuture> listObjectsAsync(S3AsyncClient s3Client, String bucketName) { + ListObjectsV2Request request = ListObjectsV2Request.builder() + .bucket(bucketName) + .build(); + + return s3Client.listObjectsV2(request) + .thenApply(response -> response.contents().stream() + .map(S3Object::key) + .toList()) + .whenComplete((result, exception) -> { + if (exception != null) { + throw new CompletionException("Couldn't list objects in bucket: " + bucketName, exception); + } + }); + } + + /** + * Retrieves an object from an Amazon S3 bucket asynchronously. + * + * @param s3Client the S3 async client to use for the operation + * @param bucketName the name of the S3 bucket containing the object + * @param keyName the unique identifier (key) of the object to retrieve + * @return a {@link CompletableFuture} that, when completed, contains the object's content as a {@link ResponseBytes} of {@link GetObjectResponse} + */ + public CompletableFuture> getObjectAsync(S3AsyncClient s3Client, String bucketName, String keyName) { + GetObjectRequest objectRequest = GetObjectRequest.builder() + .key(keyName) + .bucket(bucketName) + .build(); + + // Get the object asynchronously and transform it into a byte array + return s3Client.getObject(objectRequest, AsyncResponseTransformer.toBytes()) + .exceptionally(exception -> { + Throwable cause = exception.getCause(); + if (cause instanceof NoSuchKeyException) { + throw new CompletionException("Failed to get the object. Reason: " + ((S3Exception) cause).awsErrorDetails().errorMessage(), cause); + } + throw new CompletionException("Failed to get the object", exception); + }); + } + + /** + * Asynchronously copies an object from one S3 bucket to another. + * + * @param s3Client the S3 async client to use for the copy operation + * @param sourceBucket the name of the source bucket + * @param sourceKey the key of the object to be copied in the source bucket + * @param destinationBucket the name of the destination bucket + * @param destinationKey the key of the copied object in the destination bucket + * @return a {@link CompletableFuture} that completes when the copy operation is finished + */ + public CompletableFuture copyObjectAsync(S3AsyncClient s3Client, String sourceBucket, String sourceKey, String destinationBucket, String destinationKey) { + CopyObjectRequest copyRequest = CopyObjectRequest.builder() + .sourceBucket(sourceBucket) + .sourceKey(sourceKey) + .destinationBucket(destinationBucket) + .destinationKey(destinationKey) + .build(); + + return s3Client.copyObject(copyRequest) + .thenRun(() -> logger.info("Copied object '" + sourceKey + "' from bucket '" + sourceBucket + "' to bucket '" + destinationBucket + "'")) + .whenComplete((ignored, exception) -> { + if (exception != null) { + Throwable cause = exception.getCause(); + if (cause instanceof S3Exception) { + throw new CompletionException("Couldn't copy object '" + sourceKey + "' from bucket '" + sourceBucket + "' to bucket '" + destinationBucket + "'. Reason: " + ((S3Exception) cause).awsErrorDetails().errorMessage(), cause); + } + throw new CompletionException("Failed to copy object", exception); + } + }); + } + + /** + * Asynchronously creates a session for the specified S3 bucket. + * + * @param s3Client the S3 asynchronous client to use for creating the session + * @param bucketName the name of the S3 bucket for which to create the session + * @return a {@link CompletableFuture} that completes when the session is created, or throws a {@link CompletionException} if an error occurs + */ + public CompletableFuture createSessionAsync(S3AsyncClient s3Client, String bucketName) { + CreateSessionRequest request = CreateSessionRequest.builder() + .bucket(bucketName) + .build(); + + return s3Client.createSession(request) + .whenComplete((response, exception) -> { + if (exception != null) { + Throwable cause = exception.getCause(); + if (cause instanceof S3Exception) { + throw new CompletionException("Couldn't create the session. Reason: " + ((S3Exception) cause).awsErrorDetails().errorMessage(), cause); + } + throw new CompletionException("Unexpected error occurred while creating session", exception); + } + logger.info("Created session for bucket: " + bucketName); + }); + + } + + /** + * Creates a new S3 directory bucket in a specified Zone (For example, a + * specified Availability Zone in this code example). + * + * @param s3Client The asynchronous S3 client used to create the bucket + * @param bucketName The name of the bucket to be created + * @param zone The Availability Zone where the bucket will be created + * @throws CompletionException if there's an error creating the bucket + */ + public CompletableFuture createDirectoryBucketAsync(S3AsyncClient s3Client, String bucketName, String zone) { + logger.info("Creating bucket: " + bucketName); + + CreateBucketConfiguration bucketConfiguration = CreateBucketConfiguration.builder() + .location(LocationInfo.builder() + .type(LocationType.AVAILABILITY_ZONE) + .name(zone) + .build()) + .bucket(BucketInfo.builder() + .type(BucketType.DIRECTORY) + .dataRedundancy(DataRedundancy.SINGLE_AVAILABILITY_ZONE) + .build()) + .build(); + + CreateBucketRequest bucketRequest = CreateBucketRequest.builder() + .bucket(bucketName) + .createBucketConfiguration(bucketConfiguration) + .build(); + + return s3Client.createBucket(bucketRequest) + .whenComplete((response, exception) -> { + if (exception != null) { + Throwable cause = exception.getCause(); + if (cause instanceof BucketAlreadyExistsException) { + throw new CompletionException("The bucket already exists: " + ((S3Exception) cause).awsErrorDetails().errorMessage(), cause); + } + throw new CompletionException("Unexpected error occurred while creating bucket", exception); + } + logger.info("Bucket created successfully with location: " + response.location()); + }); + } + + /** + * Creates an S3 bucket asynchronously. + * + * @param s3Client the S3 async client to use for the bucket creation + * @param bucketName the name of the S3 bucket to create + * @return a {@link CompletableFuture} that completes with the {@link WaiterResponse} containing the {@link HeadBucketResponse} + * when the bucket is successfully created + * @throws CompletionException if there's an error creating the bucket + */ + public CompletableFuture> createBucketAsync(S3AsyncClient s3Client, String bucketName) { + CreateBucketRequest bucketRequest = CreateBucketRequest.builder() + .bucket(bucketName) + .build(); + + return s3Client.createBucket(bucketRequest) + .thenCompose(response -> { + S3AsyncWaiter s3Waiter = s3Client.waiter(); + HeadBucketRequest bucketRequestWait = HeadBucketRequest.builder() + .bucket(bucketName) + .build(); + return s3Waiter.waitUntilBucketExists(bucketRequestWait); + }) + .whenComplete((response, exception) -> { + if (exception != null) { + Throwable cause = exception.getCause(); + if (cause instanceof BucketAlreadyExistsException) { + throw new CompletionException("The S3 bucket exists: " + cause.getMessage(), cause); + } else { + throw new CompletionException("Failed to create access key: " + exception.getMessage(), exception); + } + } + logger.info(bucketName + " is ready"); + }); + } + + /** + * Uploads an object to an Amazon S3 bucket asynchronously. + * + * @param s3Client the S3 async client to use for the upload + * @param bucketName the destination S3 bucket name + * @param bucketObject the name of the object to be uploaded + * @param text the content to be uploaded as the object + */ + public CompletableFuture putObjectAsync(S3AsyncClient s3Client, String bucketName, String bucketObject, String text) { + PutObjectRequest objectRequest = PutObjectRequest.builder() + .bucket(bucketName) + .key(bucketObject) + .build(); + + return s3Client.putObject(objectRequest, AsyncRequestBody.fromString(text)) + .whenComplete((response, exception) -> { + if (exception != null) { + Throwable cause = exception.getCause(); + if (cause instanceof NoSuchBucketException) { + throw new CompletionException("The S3 bucket does not exist: " + cause.getMessage(), cause); + } else { + throw new CompletionException("Failed to create access key: " + exception.getMessage(), exception); + } + } + }); + } + + /** + * Creates an AWS IAM access key asynchronously for the specified user name. + * + * @param userName the name of the IAM user for whom to create the access key + * @return a {@link CompletableFuture} that completes with the {@link CreateAccessKeyResponse} containing the created access key + */ + public CompletableFuture createAccessKeyAsync(String userName) { + CreateAccessKeyRequest request = CreateAccessKeyRequest.builder() + .userName(userName) + .build(); + + return getIAMAsyncClient().createAccessKey(request) + .whenComplete((response, exception) -> { + if (response != null) { + logger.info("Access Key Created."); + } else { + if (exception == null) { + Throwable cause = exception.getCause(); + if (cause instanceof IamException) { + throw new CompletionException("IAM error while creating access key: " + cause.getMessage(), cause); + } else { + throw new CompletionException("Failed to create access key: " + exception.getMessage(), exception); + } + } + } + }); + } + + /** + * Asynchronously selects an Availability Zone ID from the available EC2 zones. + * + * @return A {@link CompletableFuture} that resolves to the selected Availability Zone ID. + * @throws CompletionException if an error occurs during the request or processing. + */ + public CompletableFuture selectAvailabilityZoneIdAsync() { + DescribeAvailabilityZonesRequest zonesRequest = DescribeAvailabilityZonesRequest.builder() + .build(); + + return getEc2AsyncClient().describeAvailabilityZones(zonesRequest) + .thenCompose(response -> { + List zonesList = response.availabilityZones(); + if (zonesList.isEmpty()) { + logger.info("No availability zones found."); + return CompletableFuture.completedFuture(null); // Return null if no zones are found + } + + List zoneIds = zonesList.stream() + .map(AvailabilityZone::zoneId) // Get the zoneId (e.g., "usw2-az1") + .toList(); + + return CompletableFuture.supplyAsync(() -> promptUserForZoneSelection(zonesList, zoneIds)) + .thenApply(selectedZone -> { + // Return only the selected Zone ID (e.g., "usw2-az1"). + return selectedZone.zoneId(); + }); + }) + .whenComplete((result, exception) -> { + if (exception == null) { + if (result != null) { + logger.info("Selected Availability Zone ID: " + result); + } else { + logger.info("No availability zone selected."); + } + } else { + Throwable cause = exception.getCause(); + if (cause instanceof Ec2Exception) { + throw new CompletionException("EC2 error while selecting availability zone: " + cause.getMessage(), cause); + } + throw new CompletionException("Failed to select availability zone: " + exception.getMessage(), exception); + } + }); + } + + /** + * Prompts the user to select an Availability Zone from the given list. + * + * @param zonesList the list of Availability Zones + * @param zoneIds the list of zone IDs + * @return the selected Availability Zone + */ + private static AvailabilityZone promptUserForZoneSelection(List zonesList, List zoneIds) { + Scanner scanner = new Scanner(System.in); + int index = -1; + + while (index < 0 || index >= zoneIds.size()) { + logger.info("Select an availability zone:"); + IntStream.range(0, zoneIds.size()).forEach(i -> + logger.info(i + ": " + zoneIds.get(i)) + ); + + logger.info("Enter the number corresponding to your choice: "); + if (scanner.hasNextInt()) { + index = scanner.nextInt(); + } else { + scanner.next(); + } + } + + AvailabilityZone selectedZone = zonesList.get(index); + logger.info("You selected: " + selectedZone.zoneId()); + return selectedZone; + } + + /** + * Asynchronously sets up a new VPC, including creating the VPC, finding the associated route table, and + * creating a VPC endpoint for the S3 service. + * + * @return a {@link CompletableFuture} that, when completed, contains a AbstractMap with the + * VPC ID and VPC endpoint ID. + */ + public CompletableFuture> setupVPCAsync() { + String cidr = "10.0.0.0/16"; + CreateVpcRequest vpcRequest = CreateVpcRequest.builder() + .cidrBlock(cidr) + .build(); + + return getEc2AsyncClient().createVpc(vpcRequest) + .thenCompose(vpcResponse -> { + String vpcId = vpcResponse.vpc().vpcId(); + logger.info("VPC Created: {}", vpcId); + + Ec2AsyncWaiter waiter = getEc2AsyncClient().waiter(); + DescribeVpcsRequest request = DescribeVpcsRequest.builder() + .vpcIds(vpcId) + .build(); + + return waiter.waitUntilVpcAvailable(request) + .thenApply(waiterResponse -> vpcId); + }) + .thenCompose(vpcId -> { + Filter filter = Filter.builder() + .name("vpc-id") + .values(vpcId) + .build(); + + DescribeRouteTablesRequest describeRouteTablesRequest = DescribeRouteTablesRequest.builder() + .filters(filter) + .build(); + + return getEc2AsyncClient().describeRouteTables(describeRouteTablesRequest) + .thenApply(routeTablesResponse -> { + if (routeTablesResponse.routeTables().isEmpty()) { + throw new CompletionException("No route tables found for VPC: " + vpcId, null); + } + String routeTableId = routeTablesResponse.routeTables().get(0).routeTableId(); + logger.info("Route table found: {}", routeTableId); + return new AbstractMap.SimpleEntry<>(vpcId, routeTableId); + }); + }) + .thenCompose(vpcAndRouteTable -> { + String vpcId = vpcAndRouteTable.getKey(); + String routeTableId = vpcAndRouteTable.getValue(); + Region region = getEc2AsyncClient().serviceClientConfiguration().region(); + String serviceName = String.format("com.amazonaws.%s.s3express", region.id()); + + CreateVpcEndpointRequest endpointRequest = CreateVpcEndpointRequest.builder() + .vpcId(vpcId) + .routeTableIds(routeTableId) + .serviceName(serviceName) + .build(); + + return getEc2AsyncClient().createVpcEndpoint(endpointRequest) + .thenApply(vpcEndpointResponse -> { + String vpcEndpointId = vpcEndpointResponse.vpcEndpoint().vpcEndpointId(); + logger.info("VPC Endpoint created: {}", vpcEndpointId); + return new AbstractMap.SimpleEntry<>(vpcId, vpcEndpointId); + }); + }) + .exceptionally(exception -> { + Throwable cause = exception.getCause() != null ? exception.getCause() : exception; + if (cause instanceof Ec2Exception) { + logger.error("EC2 error during VPC setup: {}", cause.getMessage(), cause); + throw new CompletionException("EC2 error during VPC setup: " + cause.getMessage(), cause); + } + + logger.error("VPC setup failed: {}", cause.getMessage(), cause); + throw new CompletionException("VPC setup failed: " + cause.getMessage(), cause); + }); + } + +} +// snippet-end:[s3.java2.directories.actions.main] \ No newline at end of file diff --git a/javav2/example_code/s3/src/main/java/com/example/s3/express/S3DirectoriesScenario.java b/javav2/example_code/s3/src/main/java/com/example/s3/express/S3DirectoriesScenario.java new file mode 100644 index 00000000000..621a30db1a0 --- /dev/null +++ b/javav2/example_code/s3/src/main/java/com/example/s3/express/S3DirectoriesScenario.java @@ -0,0 +1,594 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.example.s3.express; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.ec2.model.Ec2Exception; +import software.amazon.awssdk.services.iam.model.CreateAccessKeyResponse; +import software.amazon.awssdk.services.iam.model.IamException; +import software.amazon.awssdk.services.s3.S3AsyncClient; +import software.amazon.awssdk.services.s3.model.BucketAlreadyExistsException; +import software.amazon.awssdk.services.s3.model.NoSuchBucketException; +import software.amazon.awssdk.services.s3.model.NoSuchKeyException; +import software.amazon.awssdk.services.s3.model.S3Exception; + +import java.util.List; +import java.util.Map; +import java.util.Scanner; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +// snippet-start:[s3.java2.directories.scenario.main] +public class S3DirectoriesScenario { + + public static final String DASHES = new String(new char[80]).replace("\0", "-"); + + private static final Logger logger = LoggerFactory.getLogger(S3DirectoriesScenario.class); + static Scanner scanner = new Scanner(System.in); + + private static S3AsyncClient mS3RegularClient; + private static S3AsyncClient mS3ExpressClient; + + private static String mdirectoryBucketName; + private static String mregularBucketName; + + private static String stackName = "cfn-stack-s3-express-basics--" + UUID.randomUUID(); + + private static String regularUser = ""; + private static String vpcId = ""; + private static String expressUser = ""; + + private static String vpcEndpointId = ""; + + private static final S3DirectoriesActions s3DirectoriesActions = new S3DirectoriesActions(); + + public static void main(String[] args) { + try { + s3ExpressScenario(); + } catch (RuntimeException e) { + logger.info(e.getMessage()); + } + } + + // Runs the scenario. + private static void s3ExpressScenario() { + logger.info(DASHES); + logger.info("Welcome to the Amazon S3 Express Basics demo using AWS SDK for Java V2."); + logger.info(""" + Let's get started! First, please note that S3 Express One Zone works best when working within the AWS infrastructure, + specifically when working in the same Availability Zone (AZ). To see the best results in this example and when you implement + directory buckets into your infrastructure, it is best to put your compute resources in the same AZ as your directory + bucket. + """); + waitForInputToContinue(scanner); + logger.info(DASHES); + + // Create an optional VPC and create 2 IAM users. + UserNames userNames = createVpcUsers(); + String expressUserName = userNames.getExpressUserName(); + String regularUserName = userNames.getRegularUserName(); + + // Set up two S3 clients, one regular and one express, + // and two buckets, one regular and one directory. + setupClientsAndBuckets(expressUserName, regularUserName); + + // Create an S3 session for the express S3 client and add objects to the buckets. + logger.info("Now let's add some objects to our buckets and demonstrate how to work with S3 Sessions."); + waitForInputToContinue(scanner); + String bucketObject = createSessionAddObjects(); + + // Demonstrate performance differences between regular and directory buckets. + demonstratePerformance(bucketObject); + + // Populate the buckets to show the lexicographical difference between + // regular and express buckets. + showLexicographicalDifferences(bucketObject); + + logger.info(DASHES); + logger.info("That's it for our tour of the basic operations for S3 Express One Zone."); + logger.info("Would you like to cleanUp the AWS resources? (y/n): "); + String response = scanner.next().trim().toLowerCase(); + if (response.equals("y")) { + cleanUp(stackName); + } + } + + /* + Delete resources created by this scenario. + */ + public static void cleanUp(String stackName) { + try { + if (mdirectoryBucketName != null) { + s3DirectoriesActions.deleteBucketAndObjectsAsync(mS3ExpressClient, mdirectoryBucketName).join(); + } + logger.info("Deleted directory bucket " + mdirectoryBucketName); + mdirectoryBucketName = null; + if (mregularBucketName != null) { + s3DirectoriesActions.deleteBucketAndObjectsAsync(mS3RegularClient, mregularBucketName).join(); + } + } catch (CompletionException ce) { + Throwable cause = ce.getCause(); + if (cause instanceof S3Exception) { + logger.error("S3Exception occurred: {}", cause.getMessage(), ce); + } else { + logger.error("An unexpected error occurred: {}", cause.getMessage(), ce); + } + } + + logger.info("Deleted regular bucket " + mregularBucketName); + mregularBucketName = null; + CloudFormationHelper.destroyCloudFormationStack(stackName); + } + + private static void showLexicographicalDifferences(String bucketObject) { + logger.info(DASHES); + logger.info(""" + 7. Populate the buckets to show the lexicographical (alphabetical) difference + when object names are listed. Now let's explore how directory buckets store + objects in a different manner to regular buckets. The key is in the name + "Directory". Where regular buckets store their key/value pairs in a + flat manner, directory buckets use actual directories/folders. + This allows for more rapid indexing, traversing, and therefore + retrieval times! + + The more segmented your bucket is, with lots of + directories, sub-directories, and objects, the more efficient it becomes. + This structural difference also causes `ListObject` operations to behave + differently, which can cause unexpected results. Let's add a few more + objects in sub-directories to see how the output of + ListObjects changes. + """); + + waitForInputToContinue(scanner); + + // Populate a few more files in each bucket so that we can use + // ListObjects and show the difference. + String otherObject = "other/" + bucketObject; + String altObject = "alt/" + bucketObject; + String otherAltObject = "other/alt/" + bucketObject; + + try { + s3DirectoriesActions.putObjectAsync(mS3RegularClient, mregularBucketName, otherObject, "").join(); + s3DirectoriesActions.putObjectAsync(mS3ExpressClient, mdirectoryBucketName, otherObject, "").join(); + s3DirectoriesActions.putObjectAsync(mS3RegularClient, mregularBucketName, altObject, "").join(); + s3DirectoriesActions.putObjectAsync(mS3ExpressClient, mdirectoryBucketName, altObject, "").join(); + s3DirectoriesActions.putObjectAsync(mS3RegularClient, mregularBucketName, otherAltObject, "").join(); + s3DirectoriesActions.putObjectAsync(mS3ExpressClient, mdirectoryBucketName, otherAltObject, "").join(); + + } catch (CompletionException ce) { + Throwable cause = ce.getCause(); + if (cause instanceof NoSuchBucketException) { + logger.error("S3Exception occurred: {}", cause.getMessage(), ce); + } else { + logger.error("An unexpected error occurred: {}", cause.getMessage(), ce); + } + return; + } + + try { + // List objects in both S3 buckets. + List dirBucketObjects = s3DirectoriesActions.listObjectsAsync(mS3ExpressClient, mdirectoryBucketName).join(); + List regBucketObjects = s3DirectoriesActions.listObjectsAsync(mS3RegularClient, mregularBucketName).join(); + + logger.info("Directory bucket content"); + for (String obj : dirBucketObjects) { + logger.info(obj); + } + + logger.info("Regular bucket content"); + for (String obj : regBucketObjects) { + logger.info(obj); + } + } catch (CompletionException e) { + logger.error("Async operation failed: {} ", e.getCause().getMessage()); + return; + } + + logger.info(""" + Notice how the regular bucket lists objects in lexicographical order, while the directory bucket does not. This is + because the regular bucket considers the whole "key" to be the object identifier, while the directory bucket actually + creates directories and uses the object "key" as a path to the object. + """); + waitForInputToContinue(scanner); + } + + /** + * Demonstrates the performance difference between downloading an object from a directory bucket and a regular bucket. + * + *

This method: + *

    + *
  • Prompts the user to choose the number of downloads (default is 1,000).
  • + *
  • Downloads the specified object from the directory bucket and measures the total time.
  • + *
  • Downloads the same object from the regular bucket and measures the total time.
  • + *
  • Compares the time differences and prints the results.
  • + *
+ * + *

Note: The performance difference will be more pronounced if this example is run on an EC2 instance + * in the same Availability Zone as the buckets. + * + * @param bucketObject the name of the object to download + */ + private static void demonstratePerformance(String bucketObject) { + logger.info(DASHES); + logger.info("6. Demonstrate the performance difference."); + logger.info(""" + Now, let's do a performance test. We'll download the same object from each + bucket repeatedly and compare the total time needed. + + Note: the performance difference will be much more pronounced if this + example is run in an EC2 instance in the same Availability Zone as + the bucket. + """); + waitForInputToContinue(scanner); + + int downloads = 1000; // Default value. + logger.info("The default number of downloads of the same object for this example is set at " + downloads + "."); + + // Ask if the user wants to download a different number. + logger.info("Would you like to download the file a different number of times? (y/n): "); + String response = scanner.next().trim().toLowerCase(); + if (response.equals("y")) { + int maxDownloads = 1_000_000; + + // Ask for a valid number of downloads. + while (true) { + logger.info("Enter a number between 1 and " + maxDownloads + " for the number of downloads: "); + if (scanner.hasNextInt()) { + downloads = scanner.nextInt(); + if (downloads >= 1 && downloads <= maxDownloads) { + break; + } else { + logger.info("Please enter a number between 1 and " + maxDownloads + "."); + } + } else { + logger.info("Invalid input. Please enter a valid integer."); + scanner.next(); + } + } + + logger.info("You have chosen to download {} items.", downloads); + } else { + logger.info("No changes made. Using default downloads: {}", downloads); + } + // Simulating the download process for the directory bucket. + logger.info("Downloading from the directory bucket."); + long directoryTimeStart = System.nanoTime(); + for (int index = 0; index < downloads; index++) { + if (index % 50 == 0) { + logger.info("Download " + index + " of " + downloads); + } + + try { + // Get the object from the directory bucket. + s3DirectoriesActions.getObjectAsync(mS3ExpressClient, mdirectoryBucketName, bucketObject).join(); + } catch (CompletionException ce) { + Throwable cause = ce.getCause(); + if (cause instanceof NoSuchKeyException) { + logger.error("S3Exception occurred: {}", cause.getMessage(), ce); + } else { + logger.error("An unexpected error occurred: {}", cause.getMessage(), ce); + } + return; + } + } + long directoryTimeDifference = System.nanoTime() - directoryTimeStart; + + // Download from the regular bucket. + logger.info("Downloading from the regular bucket."); + long normalTimeStart = System.nanoTime(); + for (int index = 0; index < downloads; index++) { + if (index % 50 == 0) { + logger.info("Download " + index + " of " + downloads); + } + + try { + s3DirectoriesActions.getObjectAsync(mS3RegularClient, mregularBucketName, bucketObject).join(); + } catch (CompletionException ce) { + Throwable cause = ce.getCause(); + if (cause instanceof NoSuchKeyException) { + logger.error("S3Exception occurred: {}", cause.getMessage(), ce); + } else { + logger.error("An unexpected error occurred: {}", cause.getMessage(), ce); + } + return; + } + } + + long normalTimeDifference = System.nanoTime() - normalTimeStart; + logger.info("The directory bucket took " + directoryTimeDifference + " nanoseconds, while the regular bucket took " + normalTimeDifference + " nanoseconds."); + long difference = normalTimeDifference - directoryTimeDifference; + logger.info("That's a difference of " + difference + " nanoseconds, or"); + logger.info(difference / 1_000_000_000.0 + " seconds."); + + if (difference < 0) { + logger.info("The directory buckets were slower. This can happen if you are not running on the cloud within a VPC."); + } + waitForInputToContinue(scanner); + } + + private static String createSessionAddObjects() { + logger.info(DASHES); + logger.info(""" + 5. Create an object and copy it. + We'll create an object consisting of some text and upload it to the + regular bucket. + """); + waitForInputToContinue(scanner); + + String bucketObject = "basic-text-object.txt"; + try { + s3DirectoriesActions.putObjectAsync(mS3RegularClient, mregularBucketName, bucketObject, "Look Ma, I'm a bucket!").join(); + s3DirectoriesActions.createSessionAsync(mS3ExpressClient, mdirectoryBucketName).join(); + + // Copy the object to the destination S3 bucket. + s3DirectoriesActions.copyObjectAsync(mS3ExpressClient, mregularBucketName, bucketObject, mdirectoryBucketName, bucketObject).join(); + } catch (CompletionException ce) { + Throwable cause = ce.getCause(); + if (cause instanceof S3Exception) { + logger.error("S3Exception occurred: {}", cause.getMessage(), ce); + } else { + logger.error("An unexpected error occurred: {}", cause.getMessage(), ce); + } + } + logger.info(""" + It worked! This is because the S3Client that performed the copy operation + is the expressClient using the credentials for the user with permission to + work with directory buckets. + + It's important to remember the user permissions when interacting with + directory buckets. Instead of validating permissions on every call as + regular buckets do, directory buckets utilize the user credentials and session + token to validate. This allows for much faster connection speeds on every call. + For single calls, this is low, but for many concurrent calls + this adds up to a lot of time saved. + """); + waitForInputToContinue(scanner); + return bucketObject; + } + + /** + * Creates VPC users for the S3 Express One Zone scenario. + *

+ * This method performs the following steps: + *

    + *
  1. Optionally creates a new VPC and VPC Endpoint if the application is running in an EC2 instance in the same Availability Zone as the directory buckets.
  2. + *
  3. Creates two IAM users: one with S3 Express One Zone permissions and one without.
  4. + *
+ * + * @return a {@link UserNames} object containing the names of the created IAM users + */ + public static UserNames createVpcUsers() { + /* + Optionally create a VPC. + Create two IAM users, one with S3 Express One Zone permissions and one without. + */ + logger.info(DASHES); + logger.info(""" + 1. First, we'll set up a new VPC and VPC Endpoint if this program is running in an EC2 instance in the same AZ as your\s + directory buckets will be. Are you running this in an EC2 instance located in the same AZ as your intended directory buckets? + """); + + logger.info("Do you want to setup a VPC Endpoint? (y/n)"); + String endpointAns = scanner.nextLine().trim(); + if (endpointAns.equalsIgnoreCase("y")) { + logger.info(""" + Great! Let's set up a VPC, retrieve the Route Table from it, and create a VPC Endpoint to connect the S3 Client to. + """); + try { + s3DirectoriesActions.setupVPCAsync().join(); + } catch (CompletionException ce) { + Throwable cause = ce.getCause(); + if (cause instanceof Ec2Exception) { + logger.error("IamException occurred: {}", cause.getMessage(), ce); + } else { + logger.error("An unexpected error occurred: {}", cause.getMessage(), ce); + } + } + waitForInputToContinue(scanner); + } else { + logger.info("Skipping the VPC setup. Don't forget to use this in production!"); + } + logger.info(DASHES); + logger.info(""" + 2. Create a RegularUser and ExpressUser by using the AWS CDK. + One IAM User, named RegularUser, will have permissions to work only + with regular buckets and one IAM user, named ExpressUser, will have + permissions to work only with directory buckets. + """); + waitForInputToContinue(scanner); + + // Create two users required for this scenario. + Map stackOutputs = createUsersUsingCDK(stackName); + regularUser = stackOutputs.get("RegularUser"); + expressUser = stackOutputs.get("ExpressUser"); + + UserNames names = new UserNames(); + names.setRegularUserName(regularUser); + names.setExpressUserName(expressUser); + return names; + } + + /** + * Creates users using AWS CloudFormation. + * + * @return a {@link Map} of String keys and String values representing the stack outputs, + * which may include user-related information such as user names and IDs. + */ + public static Map createUsersUsingCDK(String stackName) { + logger.info("We'll use an AWS CloudFormation template to create the IAM users and policies."); + CloudFormationHelper.deployCloudFormationStack(stackName); + return CloudFormationHelper.getStackOutputsAsync(stackName).join(); + } + + /** + * Sets up the necessary clients and buckets for the S3 Express service. + * + * @param expressUserName the username for the user with S3 Express permissions + * @param regularUserName the username for the user with regular S3 permissions + */ + public static void setupClientsAndBuckets(String expressUserName, String regularUserName) { + Scanner locscanner = new Scanner(System.in); + String accessKeyIdforRegUser; + String secretAccessforRegUser; + try { + CreateAccessKeyResponse keyResponse = s3DirectoriesActions.createAccessKeyAsync(regularUserName).join(); + accessKeyIdforRegUser = keyResponse.accessKey().accessKeyId(); + secretAccessforRegUser = keyResponse.accessKey().secretAccessKey(); + } catch (CompletionException ce) { + Throwable cause = ce.getCause(); + if (cause instanceof IamException) { + logger.error("IamException occurred: {}", cause.getMessage(), ce); + } else { + logger.error("An unexpected error occurred: {}", cause.getMessage(), ce); + } + return; + } + + String accessKeyIdforExpressUser; + String secretAccessforExpressUser; + try { + CreateAccessKeyResponse keyResponseExpress = s3DirectoriesActions.createAccessKeyAsync(expressUserName).join(); + accessKeyIdforExpressUser = keyResponseExpress.accessKey().accessKeyId(); + secretAccessforExpressUser = keyResponseExpress.accessKey().secretAccessKey(); + } catch (CompletionException ce) { + Throwable cause = ce.getCause(); + if (cause instanceof IamException) { + logger.error("IamException occurred: {}", cause.getMessage(), ce); + } else { + logger.error("An unexpected error occurred: {}", cause.getMessage(), ce); + } + return; + } + + logger.info(DASHES); + logger.info(""" + 3. Create two S3Clients; one uses the ExpressUser's credentials and one uses the RegularUser's credentials. + The 2 S3Clients will use different credentials. + """); + waitForInputToContinue(locscanner); + try { + mS3RegularClient = createS3ClientWithAccessKeyAsync(accessKeyIdforRegUser, secretAccessforRegUser).join(); + mS3ExpressClient = createS3ClientWithAccessKeyAsync(accessKeyIdforExpressUser, secretAccessforExpressUser).join(); + } catch (CompletionException ce) { + Throwable cause = ce.getCause(); + if (cause instanceof IllegalArgumentException) { + logger.error("An invalid argument exception occurred: {}", cause.getMessage(), ce); + } else { + logger.error("An unexpected error occurred: {}", cause.getMessage(), ce); + } + return; + } + + logger.info(""" + We can now use the ExpressUser client to make calls to S3 Express operations. + """); + waitForInputToContinue(locscanner); + logger.info(DASHES); + logger.info(""" + 4. Create two buckets. + Now we will create a directory bucket which is the linchpin of the S3 Express One Zone service. Directory buckets + behave differently from regular S3 buckets which we will explore here. We'll also create a regular bucket, put + an object into the regular bucket, and copy it to the directory bucket. + """); + + logger.info(""" + Now, let's choose an availability zone (AZ) for the directory bucket. + We'll choose one that is supported. + """); + String zoneId; + String regularBucketName; + try { + zoneId = s3DirectoriesActions.selectAvailabilityZoneIdAsync().join(); + regularBucketName = "reg-bucket-" + System.currentTimeMillis(); + } catch (CompletionException ce) { + Throwable cause = ce.getCause(); + if (cause instanceof Ec2Exception) { + logger.error("EC2Exception occurred: {}", cause.getMessage(), ce); + } else { + logger.error("An unexpected error occurred: {}", cause.getMessage(), ce); + } + return; + } + logger.info(""" + Now, let's create the actual directory bucket, as well as a regular bucket." + """); + + String directoryBucketName = "test-bucket-" + System.currentTimeMillis() + "--" + zoneId + "--x-s3"; + try { + s3DirectoriesActions.createDirectoryBucketAsync(mS3ExpressClient, directoryBucketName, zoneId).join(); + logger.info("Created directory bucket {}", directoryBucketName); + } catch (CompletionException ce) { + Throwable cause = ce.getCause(); + if (cause instanceof BucketAlreadyExistsException) { + logger.error("The bucket already exists. Moving on: {}", cause.getMessage(), ce); + } else { + logger.error("An unexpected error occurred: {}", cause.getMessage(), ce); + return; + } + } + + // Assign to the data member. + mdirectoryBucketName = directoryBucketName; + try { + s3DirectoriesActions.createBucketAsync(mS3RegularClient, regularBucketName).join(); + logger.info("Created regular bucket {} ", regularBucketName); + mregularBucketName = regularBucketName; + } catch (CompletionException ce) { + Throwable cause = ce.getCause(); + if (cause instanceof BucketAlreadyExistsException) { + logger.error("The bucket already exists. Moving on: {}", cause.getMessage(), ce); + } else { + logger.error("An unexpected error occurred: {}", cause.getMessage(), ce); + return; + } + } + logger.info("Great! Both buckets were created."); + waitForInputToContinue(locscanner); + } + + /** + * Creates an asynchronous S3 client with the specified access key and secret access key. + * + * @param accessKeyId the AWS access key ID + * @param secretAccessKey the AWS secret access key + * @return a {@link CompletableFuture} that asynchronously creates the S3 client + * @throws IllegalArgumentException if the access key ID or secret access key is null + */ + public static CompletableFuture createS3ClientWithAccessKeyAsync(String accessKeyId, String secretAccessKey) { + return CompletableFuture.supplyAsync(() -> { + // Validate input parameters + if (accessKeyId == null || accessKeyId.isBlank() || secretAccessKey == null || secretAccessKey.isBlank()) { + throw new IllegalArgumentException("Access Key ID and Secret Access Key must not be null or empty"); + } + + AwsBasicCredentials awsCredentials = AwsBasicCredentials.create(accessKeyId, secretAccessKey); + return S3AsyncClient.builder() + .credentialsProvider(StaticCredentialsProvider.create(awsCredentials)) + .region(Region.US_WEST_2) + .build(); + }); + } + + private static void waitForInputToContinue(Scanner scanner) { + while (true) { + logger.info(""); + logger.info("Enter 'c' followed by to continue:"); + String input = scanner.nextLine(); + + if (input.trim().equalsIgnoreCase("c")) { + logger.info("Continuing with the program..."); + logger.info(""); + break; + } else { + logger.info("Invalid input. Please try again."); + } + } + } +} +// snippet-end:[s3.java2.directories.scenario.main] \ No newline at end of file diff --git a/javav2/example_code/s3/src/main/java/com/example/s3/express/UserNames.java b/javav2/example_code/s3/src/main/java/com/example/s3/express/UserNames.java new file mode 100644 index 00000000000..98ca4860e3a --- /dev/null +++ b/javav2/example_code/s3/src/main/java/com/example/s3/express/UserNames.java @@ -0,0 +1,20 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.example.s3.express; + +public class UserNames { + private String expressUserName; + private String regularUserName; + + public String getExpressUserName() { return expressUserName; } + + public void setExpressUserName(String expressUserName) { + this.expressUserName = expressUserName; + } + + public void setRegularUserName(String regularUserName) { + this.regularUserName = regularUserName; + } + public String getRegularUserName() { return regularUserName; } +} diff --git a/javav2/example_code/s3/src/main/resources/log4j2.xml b/javav2/example_code/s3/src/main/resources/log4j2.xml index 2329c9d3615..32a31484ec9 100644 --- a/javav2/example_code/s3/src/main/resources/log4j2.xml +++ b/javav2/example_code/s3/src/main/resources/log4j2.xml @@ -1,7 +1,7 @@ - + diff --git a/javav2/example_code/s3/src/main/resources/s3_express_template.yaml b/javav2/example_code/s3/src/main/resources/s3_express_template.yaml new file mode 100644 index 00000000000..be510ae8212 --- /dev/null +++ b/javav2/example_code/s3/src/main/resources/s3_express_template.yaml @@ -0,0 +1,51 @@ +Resources: + RegularUser: + Type: AWS::IAM::User + ExpressUser: + Type: AWS::IAM::User + ExpressPolicy: + Type: AWS::IAM::Policy + Properties: + PolicyName: AllowExpressOperations + PolicyDocument: + Statement: + - Effect: Allow + Action: + - "s3express:CreateBucket" + - "s3express:CreateSession" + - "s3express:CopyObject" + - "s3express:GetObject" + - "s3express:PutObject" + - "s3express:ListObjects" + - "s3express:DeleteObjects" + - "s3express:DeleteObject" + - "s3express:DeleteBucket" + - "s3:GetObject" + - "s3:CopyObject" + Resource: "*" + Users: + - !Ref ExpressUser + RegularPolicy: + Type: AWS::IAM::Policy + Properties: + PolicyName: AllowRegularOperations + PolicyDocument: + Statement: + - Effect: Allow + Action: + - "s3:CreateBucket" + - "s3:PutObject" + - "s3:GetObject" + - "S3:ListObjects" + - "S3:DeleteObjects" + - "S3:DeleteObject" + - "s3:ListBucket" + - "s3:DeleteBucket" + Resource: "*" + Users: + - !Ref RegularUser +Outputs: + RegularUser: + Value: !Ref RegularUser + ExpressUser: + Value: !Ref ExpressUser diff --git a/javav2/example_code/s3/src/test/java/S3ExpressTests.java b/javav2/example_code/s3/src/test/java/S3ExpressTests.java new file mode 100644 index 00000000000..18f11c316a8 --- /dev/null +++ b/javav2/example_code/s3/src/test/java/S3ExpressTests.java @@ -0,0 +1,193 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import com.example.s3.express.S3DirectoriesActions; +import com.example.s3.express.S3DirectoriesScenario; +import com.example.s3.express.UserNames; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestMethodOrder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import software.amazon.awssdk.core.waiters.WaiterResponse; +import software.amazon.awssdk.services.iam.model.CreateAccessKeyResponse; +import software.amazon.awssdk.services.s3.S3AsyncClient; +import software.amazon.awssdk.services.s3.model.CreateBucketResponse; +import software.amazon.awssdk.services.s3.model.HeadBucketResponse; + +import java.io.IOException; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +import static com.example.s3.express.S3DirectoriesScenario.createS3ClientWithAccessKeyAsync; +import static com.example.s3.express.S3DirectoriesScenario.createUsersUsingCDK; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class S3ExpressTests { + + private static S3AsyncClient mS3RegularClient; + private static S3AsyncClient mS3ExpressClient; + + private static String regularUser = ""; + + private static String expressUser = ""; + + private static String regularBucketName = ""; + private static String directoryBucketName = ""; + + private static String bucketObject = "basic-text-object.txt"; + private static final S3DirectoriesActions s3DirectoriesActions = new S3DirectoriesActions(); + + private static String stackName = "cfn-stack-s3-express-basics--" + UUID.randomUUID(); + + private static final Logger logger = LoggerFactory.getLogger(S3ExpressTests.class); + + @Test + @Tag("IntegrationTest") + @Order(1) + public void testSetUp() throws IOException { + assertDoesNotThrow(() -> { + // Retrieve user names from CDK stack outputs + Map stackOutputs = createUsersUsingCDK(stackName); + regularUser = stackOutputs.get("RegularUser"); + expressUser = stackOutputs.get("ExpressUser"); + + assertNotNull(regularUser, "Regular user should not be null"); + assertNotNull(expressUser, "Express user should not be null"); + + // Store the user names in a UserNames object + UserNames names = new UserNames(); + names.setRegularUserName(regularUser); + names.setExpressUserName(expressUser); + + // Create access keys for both users asynchronously + CreateAccessKeyResponse keyResponseRegular = s3DirectoriesActions.createAccessKeyAsync(regularUser).join(); + CreateAccessKeyResponse keyResponseExpress = s3DirectoriesActions.createAccessKeyAsync(expressUser).join(); + + assertNotNull(keyResponseRegular.accessKey(), "Access key for Regular User should not be null"); + assertNotNull(keyResponseExpress.accessKey(), "Access key for Express User should not be null"); + + // Extract access keys + String accessKeyIdForRegUser = keyResponseRegular.accessKey().accessKeyId(); + String secretAccessForRegUser = keyResponseRegular.accessKey().secretAccessKey(); + + String accessKeyIdForExpressUser = keyResponseExpress.accessKey().accessKeyId(); + String secretAccessForExpressUser = keyResponseExpress.accessKey().secretAccessKey(); + + // Ensure keys are valid + assertNotNull(accessKeyIdForRegUser, "Access Key ID for Regular User should not be null"); + assertNotNull(secretAccessForRegUser, "Secret Access Key for Regular User should not be null"); + assertNotNull(accessKeyIdForExpressUser, "Access Key ID for Express User should not be null"); + assertNotNull(secretAccessForExpressUser, "Secret Access Key for Express User should not be null"); + + // Create S3 clients asynchronously + mS3RegularClient = createS3ClientWithAccessKeyAsync(accessKeyIdForRegUser, secretAccessForRegUser).join(); + mS3ExpressClient = createS3ClientWithAccessKeyAsync(accessKeyIdForExpressUser, secretAccessForExpressUser).join(); + + assertNotNull(mS3RegularClient, "S3 client for Regular User should not be null"); + assertNotNull(mS3ExpressClient, "S3 client for Express User should not be null"); + }); + } + + @Test + @Tag("IntegrationTest") + @Order(2) + public void createBuckets() throws InterruptedException { + Thread.sleep(30000); + assertDoesNotThrow(() -> { + String zoneId = "usw2-az1"; + + // Generate bucket names + regularBucketName = "reg-bucket-" + System.currentTimeMillis(); + directoryBucketName = "test-bucket-" + System.currentTimeMillis() + "--" + zoneId + "--x-s3"; + + // Validate bucket names + assertNotNull(regularBucketName, "Regular bucket name should not be null"); + assertNotNull(directoryBucketName, "Directory bucket name should not be null"); + + // Create the regular bucket asynchronously + CompletableFuture> regularBucketFuture = s3DirectoriesActions.createBucketAsync(mS3RegularClient, regularBucketName); + + // Create the directory bucket asynchronously + CompletableFuture directoryBucketFuture = s3DirectoriesActions.createDirectoryBucketAsync(mS3ExpressClient, directoryBucketName, zoneId); + + // Wait for both operations to complete + CompletableFuture.allOf(regularBucketFuture, directoryBucketFuture).join(); + + }); + } + + + @Test + @Tag("IntegrationTest") + @Order(3) + public void createSessionAddObjectTest() { + assertDoesNotThrow(() -> { + s3DirectoriesActions.putObjectAsync(mS3RegularClient, regularBucketName, bucketObject, "Look Ma, I'm a bucket!").join(); + s3DirectoriesActions.createSessionAsync(mS3ExpressClient, directoryBucketName).join(); + s3DirectoriesActions.copyObjectAsync(mS3ExpressClient, regularBucketName, bucketObject, directoryBucketName, bucketObject).join(); + }); + } + + @Test + @Tag("IntegrationTest") + @Order(4) + public void demonstratePerformance() { + assertDoesNotThrow(() -> { + int downloads = 300; + long directoryTimeStart = System.nanoTime(); + for (int index = 0; index < downloads; index++) { + if (index % 50 == 0) { + System.out.println("Download " + index + " of " + downloads); + } + + + // Get the object from the directory bucket. + s3DirectoriesActions.getObjectAsync(mS3ExpressClient, directoryBucketName, bucketObject).join(); + } + + long directoryTimeDifference = System.nanoTime() - directoryTimeStart; + + // Download from the regular bucket. + System.out.println("Downloading from the regular bucket."); + long normalTimeStart = System.nanoTime(); + for (int index = 0; index < downloads; index++) { + if (index % 50 == 0) { + System.out.println("Download " + index + " of " + downloads); + } + + s3DirectoriesActions.getObjectAsync(mS3RegularClient, regularBucketName, bucketObject).join(); + + } + + long normalTimeDifference = System.nanoTime() - normalTimeStart; + System.out.println("The directory bucket took " + directoryTimeDifference + " nanoseconds, while the regular bucket took " + normalTimeDifference + " nanoseconds."); + long difference = normalTimeDifference - directoryTimeDifference; + System.out.println("That's a difference of " + difference + " nanoseconds, or"); + System.out.println(difference / 1_000_000_000.0 + " seconds."); + + if (difference < 0) { + System.out.println("The directory buckets were slower. This can happen if you are not running on the cloud within a VPC."); + } + }); + } + + @Test + @Tag("IntegrationTest") + @Order(5) + public void testCleanup() { + assertDoesNotThrow(() -> { + s3DirectoriesActions.deleteBucketAndObjectsAsync(mS3ExpressClient, directoryBucketName).join(); + s3DirectoriesActions.deleteBucketAndObjectsAsync(mS3RegularClient, regularBucketName).join(); + S3DirectoriesScenario.cleanUp(stackName); + }); + } +} +