diff --git a/.doc_gen/metadata/s3_metadata.yaml b/.doc_gen/metadata/s3_metadata.yaml index ac313b8cf5b..eb7cba7f897 100644 --- a/.doc_gen/metadata/s3_metadata.yaml +++ b/.doc_gen/metadata/s3_metadata.yaml @@ -236,6 +236,10 @@ s3_CopyObject: - description: snippet_tags: - S3.dotnet35.CopyObject + - description: Copy an object using a conditional request. + genai: some + snippet_tags: + - S3ConditionalRequests.dotnetv3.CopyObjectConditional C++: versions: - sdk_version: 1 @@ -854,6 +858,10 @@ s3_GetObject: - description: snippet_tags: - S3.dotnetv3.S3_Basics-DownloadObject + - description: Get an object using a conditional request. + genai: some + snippet_tags: + - S3ConditionalRequests.dotnetv3.GetObjectConditional C++: versions: - sdk_version: 1 @@ -1491,6 +1499,10 @@ s3_PutObject: - description: Upload an object with server-side encryption. snippet_tags: - S3.dotnetv3.ServerSideEncryptionExample + - description: Put an object using a conditional request. + genai: some + snippet_tags: + - S3ConditionalRequests.dotnetv3.PutObjectConditional C++: versions: - sdk_version: 1 @@ -3593,6 +3605,18 @@ s3_Scenario_ConditionalRequests: genai: some snippet_tags: - python.example_code.s3.S3ConditionalRequests.wrapper + .NET: + versions: + - sdk_version: 3 + github: dotnetv3/S3/scenarios/S3ConditionalRequestsScenario + sdkguide: + excerpts: + - description: Run an interactive scenario demonstrating &S3; conditional request features. + snippet_tags: + - S3ConditionalRequests.dotnetv3.Scenario + - description: A wrapper class for S3 functions. + snippet_tags: + - S3ConditionalRequests.dotnetv3.S3ActionsWrapper services: s3: {GetObject, PutObject, CopyObject} s3_Scenario_DownloadS3Directory: diff --git a/dotnetv3/S3/README.md b/dotnetv3/S3/README.md index f3bedcc6b54..9a43fea4de2 100644 --- a/dotnetv3/S3/README.md +++ b/dotnetv3/S3/README.md @@ -84,6 +84,7 @@ functions within the same service. - [Get started with encryption](SSEClientEncryptionExample/SSEClientEncryption.cs) - [Get started with tags](ObjectTagExample/ObjectTag.cs) - [Lock Amazon S3 objects](scenarios/S3ObjectLockScenario/S3ObjectLockWorkflow/S3ObjectLockWorkflow.cs) +- [Make conditional requests](scenarios/S3ConditionalRequestsScenario/S3ConditionalRequests/S3ConditionalRequestsScenario.cs) - [Manage access control lists (ACLs)](ManageACLsExample/ManageACLs.cs) - [Perform a multipart copy](MPUapiCopyObjExample/MPUapiCopyObj.cs) - [Transform data with S3 Object Lambda](../cross-service/S3ObjectLambdaFunction) @@ -209,6 +210,18 @@ This example shows you how to work with S3 object lock features. +#### Make conditional requests + +This example shows you how to add preconditions to Amazon S3 requests. + + + + + + + + + #### Manage access control lists (ACLs) This example shows you how to manage access control lists (ACLs) for Amazon S3 buckets. @@ -283,4 +296,4 @@ in the `dotnetv3` folder. Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -SPDX-License-Identifier: Apache-2.0 \ No newline at end of file +SPDX-License-Identifier: Apache-2.0 diff --git a/dotnetv3/S3/scenarios/S3ConditionalRequestsScenario/README.md b/dotnetv3/S3/scenarios/S3ConditionalRequestsScenario/README.md new file mode 100644 index 00000000000..6de6a77bd9f --- /dev/null +++ b/dotnetv3/S3/scenarios/S3ConditionalRequestsScenario/README.md @@ -0,0 +1,51 @@ +# Amazon S3 Conditional Requests Feature Scenario for the SDK for .NET + +## Overview + +This example demonstrates how to use the AWS SDK for Python (boto3) to work with Amazon Simple Storage Service (Amazon S3) conditional request features. The scenario demonstrates how to add preconditions to S3 operations, and how those operations will succeed or fail based on the conditional requests. + +[Amazon S3 Conditional Requests](https://docs.aws.amazon.com/AmazonS3/latest/userguide/conditional-requests.html) are used to add preconditions to S3 read, copy, or write requests. + +## ⚠ Important + +* Running this code might result in charges to your AWS account. +* Running the tests might result in charges to your AWS account. +* We recommend that you grant your code least privilege. At most, grant only the minimum permissions required to perform the task. For more information, see [Grant least privilege](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege). +* This code is not tested in every AWS Region. For more information, see [AWS Regional Services](https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services). + +## Scenario + +This example uses a feature scenario to demonstrate various aspects of S3 conditional requests. The scenario is divided into three stages: + +1. **Setup**: Create test buckets and objects. +2. **Conditional Reads and Writes**: Explore S3 conditional requests by listing objects, attempting to read or write with conditional requests, and viewing request results. +3. **Clean**: Delete all objects and buckets. + +### Prerequisites + +For general prerequisites, see the [README](../../../README.md) in the `dotnetv3` folder. + +### Resources + +The scenario steps create the buckets and objects needed for the example. No additional resources are required. + +### Instructions + +After the example compiles, you can run it from the command line. To do so, navigate to +the folder that contains the .sln file and run the following command: + +``` +dotnet run +``` + +Alternatively, you can run the example from within your IDE. + +This starts an interactive scenario that walks you through exploring conditional requests for read, write, and copy operations. + +## Additional resources + +- [Amazon S3 Developer Guide](https://docs.aws.amazon.com/AmazonS3/latest/userguide/conditional-requests.html) + +--- + +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 diff --git a/dotnetv3/S3/scenarios/S3ConditionalRequestsScenario/S3ConditionalRequests/Enums.cs b/dotnetv3/S3/scenarios/S3ConditionalRequestsScenario/S3ConditionalRequests/Enums.cs new file mode 100644 index 00000000000..2832bbe46db --- /dev/null +++ b/dotnetv3/S3/scenarios/S3ConditionalRequestsScenario/S3ConditionalRequests/Enums.cs @@ -0,0 +1,12 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +namespace S3ConditionalRequestsScenario; + +public enum S3ConditionType +{ + IfMatch, + IfNoneMatch, + IfModifiedSince, + IfUnmodifiedSince +} \ No newline at end of file diff --git a/dotnetv3/S3/scenarios/S3ConditionalRequestsScenario/S3ConditionalRequests/S3ActionsWrapper.cs b/dotnetv3/S3/scenarios/S3ConditionalRequestsScenario/S3ConditionalRequests/S3ActionsWrapper.cs new file mode 100644 index 00000000000..aa62c496e49 --- /dev/null +++ b/dotnetv3/S3/scenarios/S3ConditionalRequestsScenario/S3ConditionalRequests/S3ActionsWrapper.cs @@ -0,0 +1,375 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// snippet-start:[S3ConditionalRequests.dotnetv3.S3ActionsWrapper] + +using System.Net; +using Amazon.S3; +using Amazon.S3.Model; +using Microsoft.Extensions.Logging; + +namespace S3ConditionalRequestsScenario; + +/// +/// Encapsulate the Amazon S3 operations. +/// +public class S3ActionsWrapper +{ + private readonly IAmazonS3 _amazonS3; + private readonly ILogger _logger; + + /// + /// Constructor for the S3ActionsWrapper. + /// + /// The injected S3 client. + /// The class logger. + public S3ActionsWrapper(IAmazonS3 amazonS3, ILogger logger) + { + _amazonS3 = amazonS3; + _logger = logger; + } + + // snippet-start:[S3ConditionalRequests.dotnetv3.GetObjectConditional] + /// + /// Retrieves an object from Amazon S3 with a conditional request. + /// + /// The key of the object to retrieve. + /// The source bucket of the object. + /// The type of condition: 'IfMatch', 'IfNoneMatch', 'IfModifiedSince', 'IfUnmodifiedSince'. + /// The value to use for the condition for dates. + /// The value to use for the condition for etags. + /// True if the conditional read is successful, False otherwise. + public async Task GetObjectConditional(string objectKey, string sourceBucket, + S3ConditionType conditionType, DateTime? conditionDateValue = null, string? etagConditionalValue = null) + { + try + { + var getObjectRequest = new GetObjectRequest + { + BucketName = sourceBucket, + Key = objectKey + }; + + switch (conditionType) + { + case S3ConditionType.IfMatch: + getObjectRequest.EtagToMatch = etagConditionalValue; + break; + case S3ConditionType.IfNoneMatch: + getObjectRequest.EtagToNotMatch = etagConditionalValue; + break; + case S3ConditionType.IfModifiedSince: + getObjectRequest.ModifiedSinceDateUtc = conditionDateValue.GetValueOrDefault(); + break; + case S3ConditionType.IfUnmodifiedSince: + getObjectRequest.UnmodifiedSinceDateUtc = conditionDateValue.GetValueOrDefault(); + break; + default: + throw new ArgumentOutOfRangeException(nameof(conditionType), conditionType, null); + } + + var response = await _amazonS3.GetObjectAsync(getObjectRequest); + var sampleBytes = new byte[20]; + await response.ResponseStream.ReadAsync(sampleBytes, 0, 20); + _logger.LogInformation($"Conditional read successful. Here are the first 20 bytes of the object:\n{System.Text.Encoding.UTF8.GetString(sampleBytes)}"); + return true; + } + catch (AmazonS3Exception e) + { + if (e.ErrorCode == "PreconditionFailed") + { + _logger.LogError("Conditional read failed: Precondition failed"); + } + else if (e.ErrorCode == "NotModified") + { + _logger.LogError("Conditional read failed: Object not modified"); + } + else + { + _logger.LogError($"Unexpected error: {e.ErrorCode}"); + throw; + } + return false; + } + } + // snippet-end:[S3ConditionalRequests.dotnetv3.GetObjectConditional] + + // snippet-start:[S3ConditionalRequests.dotnetv3.PutObjectConditional] + /// + /// Uploads an object to Amazon S3 with a conditional request. Prevents overwrite using an IfNoneMatch condition for the object key. + /// + /// The key of the object to upload. + /// The source bucket of the object. + /// The content to upload as a string. + /// The ETag if the conditional write is successful, empty otherwise. + public async Task PutObjectConditional(string objectKey, string bucket, string content) + { + try + { + var putObjectRequest = new PutObjectRequest + { + BucketName = bucket, + Key = objectKey, + ContentBody = content, + IfNoneMatch = "*" + }; + + var putResult = await _amazonS3.PutObjectAsync(putObjectRequest); + _logger.LogInformation($"Conditional write successful for key {objectKey} in bucket {bucket}."); + return putResult.ETag; + } + catch (AmazonS3Exception e) + { + if (e.ErrorCode == "PreconditionFailed") + { + _logger.LogError("Conditional write failed: Precondition failed"); + } + else + { + _logger.LogError($"Unexpected error: {e.ErrorCode}"); + throw; + } + return string.Empty; + } + } + // snippet-end:[S3ConditionalRequests.dotnetv3.PutObjectConditional] + + // snippet-start:[S3ConditionalRequests.dotnetv3.CopyObjectConditional] + /// + /// Copies an object from one Amazon S3 bucket to another with a conditional request. + /// + /// The key of the source object to copy. + /// The key of the destination object. + /// The source bucket of the object. + /// The destination bucket of the object. + /// The type of condition to apply, e.g. 'CopySourceIfMatch', 'CopySourceIfNoneMatch', 'CopySourceIfModifiedSince', 'CopySourceIfUnmodifiedSince'. + /// The value to use for the condition for dates. + /// The value to use for the condition for etags. + /// True if the conditional copy is successful, False otherwise. + public async Task CopyObjectConditional(string sourceKey, string destKey, string sourceBucket, string destBucket, + S3ConditionType conditionType, DateTime? conditionDateValue = null, string? etagConditionalValue = null) + { + try + { + var copyObjectRequest = new CopyObjectRequest + { + DestinationBucket = destBucket, + DestinationKey = destKey, + SourceBucket = sourceBucket, + SourceKey = sourceKey + }; + + switch (conditionType) + { + case S3ConditionType.IfMatch: + copyObjectRequest.ETagToMatch = etagConditionalValue; + break; + case S3ConditionType.IfNoneMatch: + copyObjectRequest.ETagToNotMatch = etagConditionalValue; + break; + case S3ConditionType.IfModifiedSince: + copyObjectRequest.ModifiedSinceDateUtc = conditionDateValue.GetValueOrDefault(); + break; + case S3ConditionType.IfUnmodifiedSince: + copyObjectRequest.UnmodifiedSinceDateUtc = conditionDateValue.GetValueOrDefault(); + break; + default: + throw new ArgumentOutOfRangeException(nameof(conditionType), conditionType, null); + } + + await _amazonS3.CopyObjectAsync(copyObjectRequest); + _logger.LogInformation($"Conditional copy successful for key {destKey} in bucket {destBucket}."); + return true; + } + catch (AmazonS3Exception e) + { + if (e.ErrorCode == "PreconditionFailed") + { + _logger.LogError("Conditional copy failed: Precondition failed"); + } + else if (e.ErrorCode == "304") + { + _logger.LogError("Conditional copy failed: Object not modified"); + } + else + { + _logger.LogError($"Unexpected error: {e.ErrorCode}"); + throw; + } + return false; + } + } + // snippet-end:[S3ConditionalRequests.dotnetv3.CopyObjectConditional] + + /// + /// Create a new Amazon S3 bucket with a specified name and check that the bucket is ready. + /// + /// The name of the bucket to create. + /// True if successful. + public async Task CreateBucketWithName(string bucketName) + { + Console.WriteLine($"\tCreating bucket {bucketName}."); + try + { + var request = new PutBucketRequest + { + BucketName = bucketName, + UseClientRegion = true + }; + + await _amazonS3.PutBucketAsync(request); + var bucketReady = false; + var retries = 5; + while (!bucketReady && retries > 0) + { + Thread.Sleep(5000); + bucketReady = await Amazon.S3.Util.AmazonS3Util.DoesS3BucketExistV2Async(_amazonS3, bucketName); + retries--; + } + + return bucketReady; + } + catch (BucketAlreadyExistsException ex) + { + Console.WriteLine($"Bucket already exists: '{ex.Message}'"); + return true; + } + catch (AmazonS3Exception ex) + { + Console.WriteLine($"Error creating bucket: '{ex.Message}'"); + return false; + } + } + + /// + /// Cleans up objects and deletes the bucket by name. + /// + /// The name of the bucket. + /// Async task. + public async Task CleanupBucketByName(string bucketName) + { + try + { + var listObjectsResponse = await _amazonS3.ListObjectsV2Async(new ListObjectsV2Request { BucketName = bucketName }); + foreach (var obj in listObjectsResponse.S3Objects) + { + await _amazonS3.DeleteObjectAsync(new DeleteObjectRequest { BucketName = bucketName, Key = obj.Key }); + } + await _amazonS3.DeleteBucketAsync(new DeleteBucketRequest { BucketName = bucketName }); + Console.WriteLine($"Cleaned up bucket: {bucketName}."); + } + catch (AmazonS3Exception e) + { + if (e.ErrorCode == "NoSuchBucket") + { + Console.WriteLine($"Bucket {bucketName} does not exist, skipping cleanup."); + } + else + { + Console.WriteLine($"Error deleting bucket: {e.ErrorCode}"); + throw; + } + } + } + + /// + /// List the contents of the bucket with their ETag. + /// + /// The name of the bucket. + /// Async task. + public async Task> ListBucketContentsByName(string bucketName) + { + var results = new List(); + try + { + Console.WriteLine($"\t Items in bucket {bucketName}"); + var listObjectsResponse = await _amazonS3.ListObjectsV2Async(new ListObjectsV2Request { BucketName = bucketName }); + if (listObjectsResponse.S3Objects.Count == 0) + { + Console.WriteLine("\t\tNo objects found."); + } + else + { + foreach (var obj in listObjectsResponse.S3Objects) + { + Console.WriteLine($"\t\t object: {obj.Key} ETag {obj.ETag}"); + } + } + results = listObjectsResponse.S3Objects; + + } + catch (AmazonS3Exception e) + { + if (e.ErrorCode == "NoSuchBucket") + { + _logger.LogError($"Bucket {bucketName} does not exist."); + } + else + { + _logger.LogError($"Error listing bucket and objects: {e.ErrorCode}"); + throw; + } + } + + return results; + } + + /// + /// Delete an object from a specific bucket. + /// + /// The Amazon S3 bucket to use. + /// The key of the object to delete. + /// True if successful. + public async Task DeleteObjectFromBucket(string bucketName, string objectKey) + { + try + { + var request = new DeleteObjectRequest() + { + BucketName = bucketName, + Key = objectKey + }; + await _amazonS3.DeleteObjectAsync(request); + Console.WriteLine($"Deleted {objectKey} in {bucketName}."); + return true; + } + catch (AmazonS3Exception ex) + { + Console.WriteLine($"\tUnable to delete object {objectKey} in bucket {bucketName}: " + ex.Message); + return false; + } + } + + /// + /// Delete a specific bucket by deleting the objects and then the bucket itself. + /// + /// The Amazon S3 bucket to use. + /// The key of the object to delete. + /// Optional versionId. + /// True if successful. + public async Task CleanUpBucketByName(string bucketName) + { + try + { + var allFiles = await ListBucketContentsByName(bucketName); + + foreach (var fileInfo in allFiles) + { + await DeleteObjectFromBucket(fileInfo.BucketName, fileInfo.Key); + } + + var request = new DeleteBucketRequest() { BucketName = bucketName, }; + var response = await _amazonS3.DeleteBucketAsync(request); + Console.WriteLine($"\tDelete for {bucketName} complete."); + return response.HttpStatusCode == HttpStatusCode.OK; + } + catch (AmazonS3Exception ex) + { + Console.WriteLine($"\tUnable to delete bucket {bucketName}: " + ex.Message); + return false; + } + + } + +} +// snippet-end:[S3ConditionalRequests.dotnetv3.S3ActionsWrapper] \ No newline at end of file diff --git a/dotnetv3/S3/scenarios/S3ConditionalRequestsScenario/S3ConditionalRequests/S3ConditionalRequestsScenario.cs b/dotnetv3/S3/scenarios/S3ConditionalRequestsScenario/S3ConditionalRequests/S3ConditionalRequestsScenario.cs new file mode 100644 index 00000000000..eff162f721f --- /dev/null +++ b/dotnetv3/S3/scenarios/S3ConditionalRequestsScenario/S3ConditionalRequests/S3ConditionalRequestsScenario.cs @@ -0,0 +1,348 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// snippet-start:[S3ConditionalRequests.dotnetv3.Scenario] + +using Amazon.S3; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Console; +using Microsoft.Extensions.Logging.Debug; + +namespace S3ConditionalRequestsScenario; + +public static class S3ConditionalRequestsScenario +{ + /* + Before running this .NET code example, set up your development environment, including your credentials. + + This example demonstrates the use of conditional requests for S3 operations. + You can use conditional requests to add preconditions to S3 read requests to return or copy + an object based on its Entity tag (ETag), or last modified date. + You can use a conditional write requests to prevent overwrites by ensuring + there is no existing object with the same key. + */ + + public static S3ActionsWrapper _s3ActionsWrapper = null!; + public static IConfiguration _configuration = null!; + public static string _resourcePrefix = null!; + public static string _sourceBucketName = null!; + public static string _destinationBucketName = null!; + public static string _sampleObjectKey = null!; + public static string _sampleObjectEtag = null!; + public static bool _interactive = true; + + + public static async Task Main(string[] args) + { + // Set up dependency injection for the Amazon service. + using var host = Host.CreateDefaultBuilder(args) + .ConfigureLogging(logging => + logging.AddFilter("System", LogLevel.Debug) + .AddFilter("Microsoft", LogLevel.Information) + .AddFilter("Microsoft", LogLevel.Trace)) + .ConfigureServices((_, services) => + services.AddAWSService() + .AddTransient() + ) + .Build(); + + _configuration = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("settings.json") // Load settings from .json file. + .AddJsonFile("settings.local.json", + true) // Optionally, load local settings. + .Build(); + + ServicesSetup(host); + + try + { + Console.WriteLine(new string('-', 80)); + Console.WriteLine("Welcome to the Amazon Simple Storage Service (S3) Conditional Requests Feature Scenario."); + Console.WriteLine(new string('-', 80)); + ConfigurationSetup(); + _sampleObjectEtag = await Setup(_sourceBucketName, _destinationBucketName, _sampleObjectKey); + + await DisplayDemoChoices(_sourceBucketName, _destinationBucketName, _sampleObjectKey, _sampleObjectEtag, 0); + + Console.WriteLine(new string('-', 80)); + Console.WriteLine("Cleaning up resources."); + Console.WriteLine(new string('-', 80)); + await Cleanup(true); + + Console.WriteLine(new string('-', 80)); + Console.WriteLine("Amazon S3 Conditional Requests Feature Scenario is complete."); + Console.WriteLine(new string('-', 80)); + } + catch (Exception ex) + { + Console.WriteLine(new string('-', 80)); + Console.WriteLine($"There was a problem: {ex.Message}"); + await CleanupScenario(_sourceBucketName, _destinationBucketName); + Console.WriteLine(new string('-', 80)); + } + } + + /// + /// Populate the services for use within the console application. + /// + /// The services host. + private static void ServicesSetup(IHost host) + { + _s3ActionsWrapper = host.Services.GetRequiredService(); + } + + /// + /// Any setup operations needed. + /// + public static void ConfigurationSetup() + { + _resourcePrefix = _configuration["resourcePrefix"] ?? "dotnet-example"; + + _sourceBucketName = _resourcePrefix + "-source"; + _destinationBucketName = _resourcePrefix + "-dest"; + _sampleObjectKey = _resourcePrefix + "-sample-object.txt"; + } + + /// + /// Sets up the scenario by creating a source and destination bucket, and uploading a test file to the source bucket. + /// + /// The name of the source bucket. + /// The name of the destination bucket. + /// The name of the test file to add to the source bucket. + /// The ETag of the uploaded test file. + public static async Task Setup(string sourceBucket, string destBucket, string objectKey) + { + Console.WriteLine( + "\nFor this scenario, we will use the AWS SDK for .NET to create several S3\n" + + "buckets and files to demonstrate working with S3 conditional requests.\n" + + "This example demonstrates the use of conditional requests for S3 operations.\r\n" + + "You can use conditional requests to add preconditions to S3 read requests to return or copy\r\n" + + "an object based on its Entity tag (ETag), or last modified date. \r\n" + + "You can use a conditional write requests to prevent overwrites by ensuring \r\n" + + "there is no existing object with the same key. \r\n\r\n" + + "This example will allow you to perform conditional reads\r\n" + + "and writes that will succeed or fail based on your selected options.\r\n\r\n" + + "Sample buckets and a sample object will be created as part of the example."); + + Console.WriteLine(new string('-', 80)); + Console.WriteLine("Press Enter when you are ready to start."); + if (_interactive) + Console.ReadLine(); + + await _s3ActionsWrapper.CreateBucketWithName(sourceBucket); + await _s3ActionsWrapper.CreateBucketWithName(destBucket); + + var eTag = await _s3ActionsWrapper.PutObjectConditional(objectKey, sourceBucket, + "Test file content."); + + return eTag; + } + + /// + /// Cleans up the scenario by deleting the source and destination buckets. + /// + /// The name of the source bucket. + /// The name of the destination bucket. + public static async Task CleanupScenario(string sourceBucket, string destBucket) + { + await _s3ActionsWrapper.CleanupBucketByName(sourceBucket); + await _s3ActionsWrapper.CleanupBucketByName(destBucket); + } + + /// + /// Displays a list of the objects in the test buckets. + /// + /// The name of the source bucket. + /// The name of the destination bucket. + public static async Task DisplayBuckets(string sourceBucket, string destBucket) + { + await _s3ActionsWrapper.ListBucketContentsByName(sourceBucket); + await _s3ActionsWrapper.ListBucketContentsByName(destBucket); + } + + /// + /// Displays the menu of conditional request options for the user. + /// + /// The name of the source bucket. + /// The name of the destination bucket. + /// The key of the test object in the source bucket. + /// The ETag of the test object in the source bucket. + public static async Task DisplayDemoChoices(string sourceBucket, string destBucket, string objectKey, string etag, int defaultChoice) + { + var actions = new[] + { + "Print a list of bucket items.", + "Perform a conditional read.", + "Perform a conditional copy.", + "Perform a conditional write.", + "Clean up and exit." + }; + + var conditions = new[] + { + "If-Match: using the object's ETag. This condition should succeed.", + "If-None-Match: using the object's ETag. This condition should fail.", + "If-Modified-Since: using yesterday's date. This condition should succeed.", + "If-Unmodified-Since: using yesterday's date. This condition should fail." + }; + + var conditionTypes = new[] + { + S3ConditionType.IfMatch, + S3ConditionType.IfNoneMatch, + S3ConditionType.IfModifiedSince, + S3ConditionType.IfUnmodifiedSince, + }; + + var yesterdayDate = DateTime.UtcNow.AddDays(-1); + + int choice; + while ((choice = GetChoiceResponse("\nExplore the S3 conditional request features by selecting one of the following choices:", actions, defaultChoice)) != 4) + { + switch (choice) + { + case 0: + Console.WriteLine("Listing the objects and buckets."); + await DisplayBuckets(sourceBucket, destBucket); + break; + case 1: + int conditionTypeIndex = GetChoiceResponse("Perform a conditional read:", conditions, 1); + if (conditionTypeIndex == 0 || conditionTypeIndex == 1) + { + await _s3ActionsWrapper.GetObjectConditional(objectKey, sourceBucket, conditionTypes[conditionTypeIndex], null, _sampleObjectEtag); + } + else if (conditionTypeIndex == 2 || conditionTypeIndex == 3) + { + await _s3ActionsWrapper.GetObjectConditional(objectKey, sourceBucket, conditionTypes[conditionTypeIndex], yesterdayDate); + } + break; + case 2: + int copyConditionTypeIndex = GetChoiceResponse("Perform a conditional copy:", conditions, 1); + string destKey = GetStringResponse("Enter an object key:", "sampleObjectKey"); + if (copyConditionTypeIndex == 0 || copyConditionTypeIndex == 1) + { + await _s3ActionsWrapper.CopyObjectConditional(objectKey, destKey, sourceBucket, destBucket, conditionTypes[copyConditionTypeIndex], null, etag); + } + else if (copyConditionTypeIndex == 2 || copyConditionTypeIndex == 3) + { + await _s3ActionsWrapper.CopyObjectConditional(objectKey, destKey, sourceBucket, destBucket, conditionTypes[copyConditionTypeIndex], yesterdayDate); + } + break; + case 3: + Console.WriteLine("Perform a conditional write using IfNoneMatch condition on the object key."); + Console.WriteLine("If the key is a duplicate, the write will fail."); + string newObjectKey = GetStringResponse("Enter an object key:", "newObjectKey"); + await _s3ActionsWrapper.PutObjectConditional(newObjectKey, sourceBucket, "Conditional write example data."); + break; + } + + if (!_interactive) + { + break; + } + } + + Console.WriteLine("Proceeding to cleanup."); + } + + // + /// Clean up the resources from the scenario. + /// + /// True to run as interactive. + /// True if successful. + public static async Task Cleanup(bool interactive) + { + Console.WriteLine(new string('-', 80)); + + if (!interactive || GetYesNoResponse("Do you want to clean up all files and buckets? (y/n) ")) + { + await _s3ActionsWrapper.CleanUpBucketByName(_sourceBucketName); + await _s3ActionsWrapper.CleanUpBucketByName(_destinationBucketName); + + } + else + { + Console.WriteLine( + "Ok, we'll leave the resources intact.\n" + + "Don't forget to delete them when you're done with them or you might incur unexpected charges." + ); + } + + Console.WriteLine(new string('-', 80)); + return true; + } + + /// + /// Helper method to get a yes or no response from the user. + /// + /// The question string to print on the console. + /// True if the user responds with a yes. + private static bool GetYesNoResponse(string question) + { + Console.WriteLine(question); + var ynResponse = Console.ReadLine(); + var response = ynResponse != null && ynResponse.Equals("y", StringComparison.InvariantCultureIgnoreCase); + return response; + } + + /// + /// Helper method to get a choice response from the user. + /// + /// The question string to print on the console. + /// The choices to print on the console. + /// The index of the selected choice + private static int GetChoiceResponse(string? question, string[] choices, int defaultChoice) + { + if (question != null) + { + Console.WriteLine(question); + + for (int i = 0; i < choices.Length; i++) + { + Console.WriteLine($"\t{i + 1}. {choices[i]}"); + } + } + + if (!_interactive) + return defaultChoice; + + var choiceNumber = 0; + while (choiceNumber < 1 || choiceNumber > choices.Length) + { + var choice = Console.ReadLine(); + Int32.TryParse(choice, out choiceNumber); + } + + return choiceNumber - 1; + } + + /// + /// Get a string response from the user. + /// + /// The question to print. + /// A default answer to use when not interactive. + /// The string response. + public static string GetStringResponse(string? question, string defaultAnswer) + { + string? answer = ""; + if (_interactive) + { + do + { + Console.WriteLine(question); + answer = Console.ReadLine(); + } while (string.IsNullOrWhiteSpace(answer)); + } + else + { + answer = defaultAnswer; + } + + return answer; + } +} +// snippet-end:[S3ConditionalRequests.dotnetv3.Scenario] \ No newline at end of file diff --git a/dotnetv3/S3/scenarios/S3ConditionalRequestsScenario/S3ConditionalRequests/S3ConditionalRequestsScenario.csproj b/dotnetv3/S3/scenarios/S3ConditionalRequestsScenario/S3ConditionalRequests/S3ConditionalRequestsScenario.csproj new file mode 100644 index 00000000000..48505357654 --- /dev/null +++ b/dotnetv3/S3/scenarios/S3ConditionalRequestsScenario/S3ConditionalRequests/S3ConditionalRequestsScenario.csproj @@ -0,0 +1,30 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + settings.json + + + + diff --git a/dotnetv3/S3/scenarios/S3ConditionalRequestsScenario/S3ConditionalRequests/settings.json b/dotnetv3/S3/scenarios/S3ConditionalRequestsScenario/S3ConditionalRequests/settings.json new file mode 100644 index 00000000000..9dd2c6c96bd --- /dev/null +++ b/dotnetv3/S3/scenarios/S3ConditionalRequestsScenario/S3ConditionalRequests/settings.json @@ -0,0 +1,3 @@ +{ + "resourcePrefix": "dotnet-s3-conditional-requests-example" +} diff --git a/dotnetv3/S3/scenarios/S3ConditionalRequestsScenario/S3ConditionalRequestsScenario.sln b/dotnetv3/S3/scenarios/S3ConditionalRequestsScenario/S3ConditionalRequestsScenario.sln new file mode 100644 index 00000000000..6a61a441bc9 --- /dev/null +++ b/dotnetv3/S3/scenarios/S3ConditionalRequestsScenario/S3ConditionalRequestsScenario.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.7.34221.43 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "S3ConditionalRequestsScenario", "S3ConditionalRequests\S3ConditionalRequestsScenario.csproj", "{DE58C919-2A2E-4768-AC07-5A5C7E34CE53}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "S3ConditionalRequestsTests", "S3ConditionalRequestsTests\S3ConditionalRequestsTests.csproj", "{E1F6B013-7462-4C31-8103-EA359ECCC653}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {DE58C919-2A2E-4768-AC07-5A5C7E34CE53}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DE58C919-2A2E-4768-AC07-5A5C7E34CE53}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DE58C919-2A2E-4768-AC07-5A5C7E34CE53}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DE58C919-2A2E-4768-AC07-5A5C7E34CE53}.Release|Any CPU.Build.0 = Release|Any CPU + {E1F6B013-7462-4C31-8103-EA359ECCC653}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E1F6B013-7462-4C31-8103-EA359ECCC653}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E1F6B013-7462-4C31-8103-EA359ECCC653}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E1F6B013-7462-4C31-8103-EA359ECCC653}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {E2318334-46A6-4814-892D-EE6AE0307BEC} + EndGlobalSection +EndGlobal diff --git a/dotnetv3/S3/scenarios/S3ConditionalRequestsScenario/S3ConditionalRequestsTests/S3ConditionalRequestsScenarioTests.cs b/dotnetv3/S3/scenarios/S3ConditionalRequestsScenario/S3ConditionalRequestsTests/S3ConditionalRequestsScenarioTests.cs new file mode 100644 index 00000000000..cd8298e4781 --- /dev/null +++ b/dotnetv3/S3/scenarios/S3ConditionalRequestsScenario/S3ConditionalRequestsTests/S3ConditionalRequestsScenarioTests.cs @@ -0,0 +1,79 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon.S3; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using S3ConditionalRequestsScenario; + +namespace S3ConditionalRequestsTests; + +/// +/// Tests for the Conditional Requests example. +/// +public class S3ConditionalRequestsScenarioTests +{ + private readonly IConfiguration _configuration; + + private readonly S3ActionsWrapper _s3ActionsWrapper = null!; + private readonly string _resourcePrefix; + private readonly ILoggerFactory _loggerFactory; + + /// + /// Constructor for the test class. + /// + public S3ConditionalRequestsScenarioTests() + { + _configuration = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("testsettings.json") // Load test settings from .json file. + .AddJsonFile("testsettings.local.json", + true) // Optionally, load local settings. + .Build(); + + _loggerFactory = LoggerFactory.Create(builder => + { + builder.AddConsole(); + }); + + _resourcePrefix = _configuration["resourcePrefix"] ?? "dotnet-example-test"; + + _s3ActionsWrapper = new S3ActionsWrapper( + new AmazonS3Client(), new Logger(_loggerFactory)); + + S3ConditionalRequestsScenario.S3ConditionalRequestsScenario._s3ActionsWrapper = _s3ActionsWrapper; + S3ConditionalRequestsScenario.S3ConditionalRequestsScenario._configuration = _configuration; + } + + /// + /// Run the setup step of the workflow. Should return successful. + /// + /// Async task. + [Fact] + [Trait("Category", "Integration")] + public async Task TestScenario() + { + // Arrange. + S3ConditionalRequestsScenario.S3ConditionalRequestsScenario._interactive = false; + + // Act. + S3ConditionalRequestsScenario.S3ConditionalRequestsScenario.ConfigurationSetup(); + var sourceName = S3ConditionalRequestsScenario.S3ConditionalRequestsScenario + ._sourceBucketName; + var destName = S3ConditionalRequestsScenario.S3ConditionalRequestsScenario + ._destinationBucketName; + var objKey = S3ConditionalRequestsScenario.S3ConditionalRequestsScenario + ._sampleObjectKey; + var sampleObjectEtag = await S3ConditionalRequestsScenario.S3ConditionalRequestsScenario.Setup(sourceName, destName, objKey); + + // Run all the options of the demo. No exceptions should be thrown. + await S3ConditionalRequestsScenario.S3ConditionalRequestsScenario.DisplayDemoChoices(sourceName, destName, objKey, sampleObjectEtag, 1); + await S3ConditionalRequestsScenario.S3ConditionalRequestsScenario.DisplayDemoChoices(sourceName, destName, objKey, sampleObjectEtag, 2); + await S3ConditionalRequestsScenario.S3ConditionalRequestsScenario.DisplayDemoChoices(sourceName, destName, objKey, sampleObjectEtag, 3); + await S3ConditionalRequestsScenario.S3ConditionalRequestsScenario.DisplayDemoChoices(sourceName, destName, objKey, sampleObjectEtag, 4); + await S3ConditionalRequestsScenario.S3ConditionalRequestsScenario.Cleanup(false); + + // Assert. + Assert.NotNull(sampleObjectEtag); + } +} \ No newline at end of file diff --git a/dotnetv3/S3/scenarios/S3ConditionalRequestsScenario/S3ConditionalRequestsTests/S3ConditionalRequestsTests.csproj b/dotnetv3/S3/scenarios/S3ConditionalRequestsScenario/S3ConditionalRequestsTests/S3ConditionalRequestsTests.csproj new file mode 100644 index 00000000000..7e259f4ddf4 --- /dev/null +++ b/dotnetv3/S3/scenarios/S3ConditionalRequestsScenario/S3ConditionalRequestsTests/S3ConditionalRequestsTests.csproj @@ -0,0 +1,42 @@ + + + + net8.0 + enable + enable + + false + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + PreserveNewest + + + PreserveNewest + testsettings.json + + + + + + + + diff --git a/dotnetv3/S3/scenarios/S3ConditionalRequestsScenario/S3ConditionalRequestsTests/Usings.cs b/dotnetv3/S3/scenarios/S3ConditionalRequestsScenario/S3ConditionalRequestsTests/Usings.cs new file mode 100644 index 00000000000..47af9ec2f2c --- /dev/null +++ b/dotnetv3/S3/scenarios/S3ConditionalRequestsScenario/S3ConditionalRequestsTests/Usings.cs @@ -0,0 +1,4 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +global using Xunit; \ No newline at end of file diff --git a/dotnetv3/S3/scenarios/S3ConditionalRequestsScenario/S3ConditionalRequestsTests/testsettings.json b/dotnetv3/S3/scenarios/S3ConditionalRequestsScenario/S3ConditionalRequestsTests/testsettings.json new file mode 100644 index 00000000000..e5cdbb2ebd2 --- /dev/null +++ b/dotnetv3/S3/scenarios/S3ConditionalRequestsScenario/S3ConditionalRequestsTests/testsettings.json @@ -0,0 +1,3 @@ +{ + "resourcePrefix": "dotnet-s3-conditional-requests-example-test" +} diff --git a/workflows/s3_conditional_requests/README.md b/workflows/s3_conditional_requests/README.md index 7eb52dcbe57..8fef0170182 100644 --- a/workflows/s3_conditional_requests/README.md +++ b/workflows/s3_conditional_requests/README.md @@ -21,6 +21,7 @@ The scenario steps create the buckets and objects needed for the example. No add This example is implemented in the following languages: - [Python](../../python/example_code/s3/scenarios/conditional_requests/README.md) +- [.NET](../../dotnetv3/S3/scenarios/S3ConditionalRequestsScenario/README.md) ## Additional reading