diff --git a/.doc_gen/metadata/s3_metadata.yaml b/.doc_gen/metadata/s3_metadata.yaml index 462f07881e0..b3e0532a61e 100644 --- a/.doc_gen/metadata/s3_metadata.yaml +++ b/.doc_gen/metadata/s3_metadata.yaml @@ -2776,6 +2776,12 @@ s3_Scenario_PresignedUrl: - description: Generate a presigned URL and perform an upload using that URL. snippet_tags: - S3.dotnetv3.UploadUsingPresignedURLExample + - sdk_version: 4 + github: dotnetv4/S3/Scenarios/S3_CreatePresignedPost + excerpts: + - description: Create and use presigned POST URLs for direct browser uploads. + snippet_tags: + - S3.dotnetv4.CreatePresignedPostBasics Java: versions: - sdk_version: 2 @@ -3705,3 +3711,17 @@ s3_Scenario_DoesBucketExist: - s3.java2.does-bucket-exist-main services: s3: {GetBucketAcl} + +s3_CreatePresignedPost: + languages: + .NET: + versions: + - sdk_version: 4 + github: dotnetv4/S3 + excerpts: + - description: Create a presigned POST URL. + genai: most + snippet_tags: + - S3.dotnetv4.Scenario_CreatePresignedPostAsync + services: + s3: {CreatePresignedPost} diff --git a/dotnetv4/Bedrock-runtime/Actions/HelloBedrockRuntime.cs b/dotnetv4/Bedrock-runtime/Actions/HelloBedrockRuntime.cs index 1ee5a0bf9ea..e29cb38778f 100644 --- a/dotnetv4/Bedrock-runtime/Actions/HelloBedrockRuntime.cs +++ b/dotnetv4/Bedrock-runtime/Actions/HelloBedrockRuntime.cs @@ -28,7 +28,8 @@ private static async Task Invoke(string modelId, string prompt) default: Console.WriteLine($"Unknown model ID: {modelId}. Valid model IDs are: {CLAUDE}."); break; - }; + } + ; } } } \ No newline at end of file diff --git a/dotnetv4/DotNetV4Examples.sln b/dotnetv4/DotNetV4Examples.sln index 9c3a74c6cc2..8b243f70231 100644 --- a/dotnetv4/DotNetV4Examples.sln +++ b/dotnetv4/DotNetV4Examples.sln @@ -149,7 +149,13 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Basics", "DynamoDB\Scenario EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DynamoDBActions", "DynamoDB\Actions\DynamoDBActions.csproj", "{B0F91FE2-6AC5-4FA8-B321-54623A516D4D}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Scenarios", "Scenarios", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "S3", "S3", "{F929DB74-DD0E-B0EF-AA66-D8703D547BBD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "S3Tests", "S3\Tests\S3Tests.csproj", "{11497EB7-B702-B537-3CBE-BA2F4F85F313}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Scenarios", "Scenarios", "{A65C33EA-4F2E-DE85-7501-4389A2100813}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Basics", "S3\Scenarios\S3_CreatePresignedPost\Basics.csproj", "{2B6F24A0-4569-E8A2-81B4-3925FA4F0320}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -365,6 +371,14 @@ Global {B0F91FE2-6AC5-4FA8-B321-54623A516D4D}.Debug|Any CPU.Build.0 = Debug|Any CPU {B0F91FE2-6AC5-4FA8-B321-54623A516D4D}.Release|Any CPU.ActiveCfg = Release|Any CPU {B0F91FE2-6AC5-4FA8-B321-54623A516D4D}.Release|Any CPU.Build.0 = Release|Any CPU + {11497EB7-B702-B537-3CBE-BA2F4F85F313}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {11497EB7-B702-B537-3CBE-BA2F4F85F313}.Debug|Any CPU.Build.0 = Debug|Any CPU + {11497EB7-B702-B537-3CBE-BA2F4F85F313}.Release|Any CPU.ActiveCfg = Release|Any CPU + {11497EB7-B702-B537-3CBE-BA2F4F85F313}.Release|Any CPU.Build.0 = Release|Any CPU + {2B6F24A0-4569-E8A2-81B4-3925FA4F0320}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2B6F24A0-4569-E8A2-81B4-3925FA4F0320}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2B6F24A0-4569-E8A2-81B4-3925FA4F0320}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2B6F24A0-4569-E8A2-81B4-3925FA4F0320}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -425,6 +439,7 @@ Global {3F159C49-3DE7-42F5-AF14-E64C03AF19E8} = {EE6D1933-1E38-406A-B691-446326310D1F} {D44D50E1-EC65-4A1C-AAA1-C360E4FC563F} = {EE6D1933-1E38-406A-B691-446326310D1F} {7485EAED-F81C-4119-BABC-E009A21ACE46} = {EE6D1933-1E38-406A-B691-446326310D1F} + {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {A1B2C3D4-E5F6-7890-ABCD-EF1234567890} {43C5E98B-5EC4-9F2B-2676-8F1E34969855} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} {6BE1D9A4-1832-49F5-8682-6DEE4A7D6232} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} {6B1F00FF-7F1D-C5D8-A8D3-E0EF2886B8C6} = {6BE1D9A4-1832-49F5-8682-6DEE4A7D6232} @@ -432,7 +447,9 @@ Global {F578CA07-E74F-4F47-9203-C67777D9BB78} = {A1B2C3D4-E5F6-7890-ABCD-EF1234567890} {E10920BB-6409-41BB-9A9D-813BC37CC3C0} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} {B0F91FE2-6AC5-4FA8-B321-54623A516D4D} = {A1B2C3D4-E5F6-7890-ABCD-EF1234567890} - {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {A1B2C3D4-E5F6-7890-ABCD-EF1234567890} + {11497EB7-B702-B537-3CBE-BA2F4F85F313} = {F929DB74-DD0E-B0EF-AA66-D8703D547BBD} + {A65C33EA-4F2E-DE85-7501-4389A2100813} = {F929DB74-DD0E-B0EF-AA66-D8703D547BBD} + {2B6F24A0-4569-E8A2-81B4-3925FA4F0320} = {A65C33EA-4F2E-DE85-7501-4389A2100813} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {08502818-E8E1-4A91-A51C-4C8C8D4FF9CA} diff --git a/dotnetv4/S3/README.md b/dotnetv4/S3/README.md new file mode 100644 index 00000000000..cf1e626503e --- /dev/null +++ b/dotnetv4/S3/README.md @@ -0,0 +1,97 @@ +# Amazon S3 code examples for the SDK for .NET (v4) + +## Overview + +Shows how to use the AWS SDK for .NET (v4) to work with Amazon Simple Storage Service (Amazon S3). + + + + +_Amazon S3 is storage for the internet. You can use Amazon S3 to store and retrieve any amount of data at any time, from anywhere on the web._ + +## ⚠ Important + +* Running this code might result in charges to your AWS account. For more details, see [AWS Pricing](https://aws.amazon.com/pricing/) and [Free Tier](https://aws.amazon.com/free/). +* 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). + + + + +## Code examples + +### Prerequisites + +For prerequisites, see the [README](../README.md#Prerequisites) in the `dotnetv4` folder. + + + + + +### Single actions + +Code excerpts that show you how to call individual service functions. + +- [CreatePresignedPost](Scenarios/S3_CreatePresignedPost/S3Wrapper.cs#L35) + +### Scenarios + +Code examples that show you how to accomplish a specific task by calling multiple +functions within the same service. + +- [Create a presigned URL](Scenarios/S3_CreatePresignedPost/CreatePresignedPostBasics.cs) + + + + + +## Run the examples + +### Instructions + + + + + + + +#### Create a presigned URL + +This example shows you how to create a presigned URL for Amazon S3 and upload an object. + + + + + + + + + +### Tests + +⚠ Running tests might result in charges to your AWS account. + + +To find instructions for running these tests, see the [README](../README.md#Tests) +in the `dotnetv4` folder. + + + + + + +## Additional resources + +- [Amazon S3 User Guide](https://docs.aws.amazon.com/AmazonS3/latest/userguide/Welcome.html) +- [Amazon S3 API Reference](https://docs.aws.amazon.com/AmazonS3/latest/API/Welcome.html) +- [SDK for .NET (v4) Amazon S3 reference](https://docs.aws.amazon.com/sdkfornet/v4/apidocs/items/S3/NS3.html) + + + + +--- + +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 diff --git a/dotnetv4/S3/S3Examples.sln b/dotnetv4/S3/S3Examples.sln new file mode 100644 index 00000000000..10c6bc5d832 --- /dev/null +++ b/dotnetv4/S3/S3Examples.sln @@ -0,0 +1,38 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.33414.496 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Scenarios", "Scenarios", "{C13DDD1A-438D-4E52-90FB-A496A54516C7}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{C51625C8-3B42-4810-BF1B-0E3C6C716FA6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Basics", "Scenarios\S3_CreatePresignedPost\Basics.csproj", "{22C217CC-E2D9-9F79-EE15-24F9D262655E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "S3Tests", "Tests\S3Tests.csproj", "{FB41CEEB-5F32-3E4A-F9C6-1FACCBDAFF3C}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {22C217CC-E2D9-9F79-EE15-24F9D262655E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {22C217CC-E2D9-9F79-EE15-24F9D262655E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {22C217CC-E2D9-9F79-EE15-24F9D262655E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {22C217CC-E2D9-9F79-EE15-24F9D262655E}.Release|Any CPU.Build.0 = Release|Any CPU + {FB41CEEB-5F32-3E4A-F9C6-1FACCBDAFF3C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FB41CEEB-5F32-3E4A-F9C6-1FACCBDAFF3C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FB41CEEB-5F32-3E4A-F9C6-1FACCBDAFF3C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FB41CEEB-5F32-3E4A-F9C6-1FACCBDAFF3C}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {22C217CC-E2D9-9F79-EE15-24F9D262655E} = {C13DDD1A-438D-4E52-90FB-A496A54516C7} + {FB41CEEB-5F32-3E4A-F9C6-1FACCBDAFF3C} = {C51625C8-3B42-4810-BF1B-0E3C6C716FA6} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {5ACB5B08-E8F8-453C-B63B-6C0C9DE67780} + EndGlobalSection +EndGlobal diff --git a/dotnetv4/S3/Scenarios/S3_CreatePresignedPost/Basics.csproj b/dotnetv4/S3/Scenarios/S3_CreatePresignedPost/Basics.csproj new file mode 100644 index 00000000000..75694d45011 --- /dev/null +++ b/dotnetv4/S3/Scenarios/S3_CreatePresignedPost/Basics.csproj @@ -0,0 +1,18 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + diff --git a/dotnetv4/S3/Scenarios/S3_CreatePresignedPost/CreatePresignedPostBasics.cs b/dotnetv4/S3/Scenarios/S3_CreatePresignedPost/CreatePresignedPostBasics.cs new file mode 100644 index 00000000000..5d8d70ead1e --- /dev/null +++ b/dotnetv4/S3/Scenarios/S3_CreatePresignedPost/CreatePresignedPostBasics.cs @@ -0,0 +1,296 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +namespace S3Scenarios; + +// snippet-start:[S3.dotnetv4.CreatePresignedPostBasics] +/// +/// Scenario demonstrating the complete workflow for presigned POST URLs: +/// 1. Create an S3 bucket +/// 2. Create a presigned POST URL +/// 3. Upload a file using the presigned POST URL +/// 4. Clean up resources +/// +public class CreatePresignedPostBasics +{ + public static ILogger _logger = null!; + public static S3Wrapper _s3Wrapper = null!; + public static UiMethods _uiMethods = null!; + public static IHttpClientFactory _httpClientFactory = null!; + public static bool _isInteractive = true; + public static string? _bucketName; + public static string? _objectKey; + + /// + /// Set up the services and logging. + /// + /// The IHost instance. + public static void SetUpServices(IHost host) + { + var loggerFactory = LoggerFactory.Create(builder => + { + builder.AddConsole(); + }); + _logger = new Logger(loggerFactory); + + _s3Wrapper = host.Services.GetRequiredService(); + _httpClientFactory = host.Services.GetRequiredService(); + _uiMethods = new UiMethods(); + } + + /// + /// Perform the actions defined for the Amazon S3 Presigned POST scenario. + /// + /// Command line arguments. + /// A Task object. + public static async Task Main(string[] args) + { + _isInteractive = !args.Contains("--non-interactive"); + + // Set up dependency injection for Amazon S3 + using var host = Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder(args) + .ConfigureServices((_, services) => + services.AddAWSService() + .AddTransient() + .AddHttpClient() + ) + .Build(); + + SetUpServices(host); + + try + { + // Display overview + _uiMethods.DisplayOverview(); + _uiMethods.PressEnter(_isInteractive); + + // Step 1: Create bucket + await CreateBucketAsync(); + _uiMethods.PressEnter(_isInteractive); + + // Step 2: Create presigned URL + _uiMethods.DisplayTitle("Step 2: Create presigned POST URL"); + var response = await CreatePresignedPostAsync(); + _uiMethods.PressEnter(_isInteractive); + + // Step 3: Display URL and fields + _uiMethods.DisplayTitle("Step 3: Presigned POST URL details"); + DisplayPresignedPostFields(response); + _uiMethods.PressEnter(_isInteractive); + + // Step 4: Upload file + _uiMethods.DisplayTitle("Step 4: Upload test file using presigned POST URL"); + await UploadFileAsync(response); + _uiMethods.PressEnter(_isInteractive); + + // Step 5: Verify file exists + await VerifyFileExistsAsync(); + _uiMethods.PressEnter(_isInteractive); + + // Step 6: Cleanup + _uiMethods.DisplayTitle("Step 6: Clean up resources"); + await CleanupAsync(); + + _uiMethods.DisplayTitle("S3 Presigned POST Scenario completed successfully!"); + _uiMethods.PressEnter(_isInteractive); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error in scenario"); + Console.WriteLine($"Error: {ex.Message}"); + + // Attempt cleanup if there was an error + if (!string.IsNullOrEmpty(_bucketName)) + { + _uiMethods.DisplayTitle("Cleaning up resources after error"); + await _s3Wrapper.DeleteBucketAsync(_bucketName); + Console.WriteLine($"Cleaned up bucket: {_bucketName}"); + } + } + } + + /// + /// Create an S3 bucket for the scenario. + /// + private static async Task CreateBucketAsync() + { + _uiMethods.DisplayTitle("Step 1: Create an S3 bucket"); + + // Generate a default bucket name for the scenario + var defaultBucketName = $"presigned-post-demo-{DateTime.Now:yyyyMMddHHmmss}".ToLower(); + + // Prompt user for bucket name or use default in non-interactive mode + _bucketName = _uiMethods.GetUserInput( + $"Enter S3 bucket name (or press Enter for '{defaultBucketName}'): ", + defaultBucketName, + _isInteractive); + + // Basic validation to ensure bucket name is not empty + if (string.IsNullOrWhiteSpace(_bucketName)) + { + _bucketName = defaultBucketName; + } + + Console.WriteLine($"Creating bucket: {_bucketName}"); + + await _s3Wrapper.CreateBucketAsync(_bucketName); + + Console.WriteLine($"Successfully created bucket: {_bucketName}"); + } + + + /// + /// Create a presigned POST URL. + /// + private static async Task CreatePresignedPostAsync() + { + _objectKey = "example-upload.txt"; + var expiration = DateTime.UtcNow.AddMinutes(10); // Short expiration for the demo + + Console.WriteLine($"Creating presigned POST URL for {_bucketName}/{_objectKey}"); + Console.WriteLine($"Expiration: {expiration} UTC"); + + var s3Client = _s3Wrapper.GetS3Client(); + + var response = await _s3Wrapper.CreatePresignedPostAsync( + s3Client, _bucketName!, _objectKey, expiration); + + Console.WriteLine("Successfully created presigned POST URL"); + return response; + } + + /// + /// Upload a file using the presigned POST URL. + /// + private static async Task UploadFileAsync(CreatePresignedPostResponse response) + { + + // Create a temporary test file to upload + string testFilePath = Path.GetTempFileName(); + string testContent = "This is a test file for the S3 presigned POST scenario."; + + await File.WriteAllTextAsync(testFilePath, testContent); + Console.WriteLine($"Created test file at: {testFilePath}"); + + // Upload the file using the presigned POST URL + Console.WriteLine("\nUploading file using the presigned POST URL..."); + var uploadResult = await UploadFileWithPresignedPostAsync(response, testFilePath); + + // Display the upload result + if (uploadResult.Success) + { + Console.WriteLine($"Upload successful! Status code: {uploadResult.StatusCode}"); + } + else + { + Console.WriteLine($"Upload failed with status code: {uploadResult.StatusCode}"); + Console.WriteLine($"Error: {uploadResult.Response}"); + throw new Exception("File upload failed"); + } + + // Clean up the temporary file + File.Delete(testFilePath); + Console.WriteLine("Temporary file deleted"); + } + + /// + /// Helper method to upload a file using a presigned POST URL. + /// + private static async Task<(bool Success, HttpStatusCode StatusCode, string Response)> UploadFileWithPresignedPostAsync( + CreatePresignedPostResponse response, + string filePath) + { + try + { + _logger.LogInformation("Uploading file {filePath} using presigned POST URL", filePath); + + using var httpClient = _httpClientFactory.CreateClient(); + using var formContent = new MultipartFormDataContent(); + + // Add all the fields from the presigned POST response + foreach (var field in response.Fields) + { + formContent.Add(new StringContent(field.Value), field.Key); + } + + // Add the file content + var fileStream = File.OpenRead(filePath); + var fileName = Path.GetFileName(filePath); + var fileContent = new StreamContent(fileStream); + fileContent.Headers.ContentType = new MediaTypeHeaderValue("text/plain"); + formContent.Add(fileContent, "file", fileName); + + // Send the POST request + var httpResponse = await httpClient.PostAsync(response.Url, formContent); + var responseContent = await httpResponse.Content.ReadAsStringAsync(); + + // Log and return the result + _logger.LogInformation("Upload completed with status code {statusCode}", httpResponse.StatusCode); + + return (httpResponse.IsSuccessStatusCode, httpResponse.StatusCode, responseContent); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error uploading file"); + return (false, HttpStatusCode.InternalServerError, ex.Message); + } + } + + /// + /// Verify that the uploaded file exists in the S3 bucket. + /// + private static async Task VerifyFileExistsAsync() + { + _uiMethods.DisplayTitle("Step 5: Verify uploaded file exists"); + + Console.WriteLine($"Checking if file exists at {_bucketName}/{_objectKey}..."); + + try + { + var metadata = await _s3Wrapper.GetObjectMetadataAsync(_bucketName!, _objectKey!); + + Console.WriteLine($"File verification successful! File exists in the bucket."); + Console.WriteLine($"File size: {metadata.ContentLength} bytes"); + Console.WriteLine($"File type: {metadata.Headers.ContentType}"); + Console.WriteLine($"Last modified: {metadata.LastModified}"); + } + catch (AmazonS3Exception ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound) + { + Console.WriteLine($"Error: File was not found in the bucket."); + throw; + } + } + + private static void DisplayPresignedPostFields(CreatePresignedPostResponse response) + { + Console.WriteLine($"Presigned POST URL: {response.Url}"); + Console.WriteLine("Form fields to include:"); + + foreach (var field in response.Fields) + { + Console.WriteLine($" {field.Key}: {field.Value}"); + } + } + + /// + /// Clean up resources created by the scenario. + /// + private static async Task CleanupAsync() + { + if (!string.IsNullOrEmpty(_bucketName)) + { + Console.WriteLine($"Deleting bucket {_bucketName} and its contents..."); + bool result = await _s3Wrapper.DeleteBucketAsync(_bucketName); + + if (result) + { + Console.WriteLine("Bucket deleted successfully"); + } + else + { + Console.WriteLine("Failed to delete bucket - it may have been already deleted"); + } + } + } +} +// snippet-end:[S3.dotnetv4.CreatePresignedPostBasics] \ No newline at end of file diff --git a/dotnetv4/S3/Scenarios/S3_CreatePresignedPost/S3Wrapper.cs b/dotnetv4/S3/Scenarios/S3_CreatePresignedPost/S3Wrapper.cs new file mode 100644 index 00000000000..12e629ed827 --- /dev/null +++ b/dotnetv4/S3/Scenarios/S3_CreatePresignedPost/S3Wrapper.cs @@ -0,0 +1,174 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +namespace S3Scenarios; + +/// +/// Wrapper methods for common Amazon Simple Storage Service (Amazon S3) +/// operations. +/// +public class S3Wrapper +{ + private readonly IAmazonS3 _s3Client; + private readonly ILogger _logger; + + /// + /// Constructor for the wrapper class. + /// + /// The injected S3 client. + /// The injected logger for use with this class. + public S3Wrapper(IAmazonS3 s3Client, ILogger logger) + { + _s3Client = s3Client; + _logger = logger; + } + + /// + /// Get the Amazon S3 client. + /// + /// The Amazon S3 client. + public IAmazonS3 GetS3Client() + { + return _s3Client; + } + + // snippet-start:[S3.dotnetv4.Scenario_CreatePresignedPostAsync] + /// + /// Create a presigned POST URL with conditions. + /// + /// The Amazon S3 client. + /// The name of the bucket. + /// The object key (path) where the uploaded file will be stored. + /// When the presigned URL expires. + /// Dictionary of fields to add to the form. + /// List of conditions to apply. + /// A CreatePresignedPostResponse object with URL and form fields. + public async Task CreatePresignedPostAsync( + IAmazonS3 s3Client, + string bucketName, + string objectKey, + DateTime expires, + Dictionary? fields = null, + List? conditions = null) + { + var request = new CreatePresignedPostRequest + { + BucketName = bucketName, + Key = objectKey, + Expires = expires + }; + + // Add custom fields if provided + if (fields != null) + { + foreach (var field in fields) + { + request.Fields.Add(field.Key, field.Value); + } + } + + // Add conditions if provided + if (conditions != null) + { + foreach (var condition in conditions) + { + request.Conditions.Add(condition); + } + } + + return await s3Client.CreatePresignedPostAsync(request); + } + // snippet-end:[S3.dotnetv4.Scenario_CreatePresignedPostAsync] + + /// + /// Create a bucket and wait until it's ready to use. + /// + /// The name of the bucket to create. + /// The name of the newly created bucket. + public async Task CreateBucketAsync(string bucketName) + { + _logger.LogInformation("Creating bucket {bucket}", bucketName); + + var request = new PutBucketRequest + { + BucketName = bucketName + }; + + var response = await _s3Client.PutBucketAsync(request); + + _logger.LogInformation("Created bucket {bucket} with status {status}", + bucketName, response.HttpStatusCode); + + // Wait for the bucket to be available + var exist = await Amazon.S3.Util.AmazonS3Util.DoesS3BucketExistV2Async(_s3Client, bucketName); + + if (!exist) + { + _logger.LogInformation("Waiting for bucket {bucket} to be ready", bucketName); + + while (!exist) + { + await Task.Delay(2000); + exist = await Amazon.S3.Util.AmazonS3Util.DoesS3BucketExistV2Async(_s3Client, bucketName); + } + } + + return bucketName; + } + + /// + /// Delete an object from an S3 bucket. + /// + /// The name of the bucket. + /// The object key to delete. + /// The response from the DeleteObjectAsync call. + public async Task DeleteObjectAsync( + string bucketName, string objectKey) + { + var request = new DeleteObjectRequest + { + BucketName = bucketName, + Key = objectKey + }; + + return await _s3Client.DeleteObjectAsync(request); + } + + /// + /// Delete an S3 bucket and all its objects. + /// + /// The name of the bucket to delete. + /// A boolean value indicating the success of the operation. + public async Task DeleteBucketAsync(string bucketName) + { + try + { + // Delete all objects in the bucket + await AmazonS3Util.DeleteS3BucketWithObjectsAsync(_s3Client, bucketName); + return true; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error deleting bucket {bucket}", bucketName); + return false; + } + } + + /// + /// Get object metadata. + /// + /// The name of the bucket. + /// The object key. + /// Object metadata. + public async Task GetObjectMetadataAsync( + string bucketName, string objectKey) + { + var request = new GetObjectMetadataRequest + { + BucketName = bucketName, + Key = objectKey + }; + + return await _s3Client.GetObjectMetadataAsync(request); + } +} \ No newline at end of file diff --git a/dotnetv4/S3/Scenarios/S3_CreatePresignedPost/UiMethods.cs b/dotnetv4/S3/Scenarios/S3_CreatePresignedPost/UiMethods.cs new file mode 100644 index 00000000000..fdedb6b30d5 --- /dev/null +++ b/dotnetv4/S3/Scenarios/S3_CreatePresignedPost/UiMethods.cs @@ -0,0 +1,72 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +namespace S3Scenarios; + +/// +/// UI helper methods for the S3 presigned POST scenario. +/// +public class UiMethods +{ + public readonly string SepBar = new string('-', 88); + + /// + /// Show information about the scenario. + /// + public void DisplayOverview() + { + DisplayTitle("Welcome to the Amazon S3 Presigned POST URL Scenario"); + + Console.WriteLine("This example application does the following:"); + Console.WriteLine("\t 1. Creates an S3 bucket with a unique name"); + Console.WriteLine("\t 2. Creates a presigned POST URL for the bucket"); + Console.WriteLine("\t 3. Displays the URL and form fields needed for browser uploads"); + Console.WriteLine("\t 4. Uploads a test file using the presigned POST URL"); + Console.WriteLine("\t 5. Verifies the file was successfully uploaded to S3"); + Console.WriteLine("\t 6. Cleans up the resources (bucket and test file)"); + } + + /// + /// Display a message and wait until the user presses enter if in interactive mode. + /// + public void PressEnter(bool interactive) + { + Console.Write("\nPlease press to continue. "); + if (interactive) + _ = Console.ReadLine(); + } + + /// + /// Display a line of hyphens, the text of the title and another + /// line of hyphens. + /// + /// The string to be displayed. + public void DisplayTitle(string strTitle) + { + Console.WriteLine(SepBar); + Console.WriteLine(strTitle); + Console.WriteLine(SepBar); + } + + /// + /// Get user input with an optional default value. + /// + /// The prompt to display to the user. + /// The default value to use if user doesn't provide input. + /// Whether to wait for user input or use default. + /// The user input or default value. + public string GetUserInput(string prompt, string? defaultValue = null, bool isInteractive = true) + { + Console.Write(prompt); + if (isInteractive) + { + var input = Console.ReadLine(); + return string.IsNullOrWhiteSpace(input) ? defaultValue ?? "" : input.Trim(); + } + else + { + Console.WriteLine(defaultValue ?? ""); + return defaultValue ?? ""; + } + } +} \ No newline at end of file diff --git a/dotnetv4/S3/Scenarios/S3_CreatePresignedPost/Usings.cs b/dotnetv4/S3/Scenarios/S3_CreatePresignedPost/Usings.cs new file mode 100644 index 00000000000..f3e8a913cc9 --- /dev/null +++ b/dotnetv4/S3/Scenarios/S3_CreatePresignedPost/Usings.cs @@ -0,0 +1,11 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +global using System.Net; +global using System.Net.Http.Headers; +global using Amazon.S3; +global using Amazon.S3.Model; +global using Amazon.S3.Util; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Hosting; +global using Microsoft.Extensions.Logging; \ No newline at end of file diff --git a/dotnetv4/S3/Tests/CreatePresignedPostBasicsTests.cs b/dotnetv4/S3/Tests/CreatePresignedPostBasicsTests.cs new file mode 100644 index 00000000000..2c44f6ff5d7 --- /dev/null +++ b/dotnetv4/S3/Tests/CreatePresignedPostBasicsTests.cs @@ -0,0 +1,77 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +namespace S3Tests; + +/// +/// Integration tests for the Amazon Simple Storage Service (Amazon S3) +/// presigned POST URL scenario (CreatePresignedPostBasics). +/// +public class CreatePresignedPostBasicsTests +{ + private AmazonS3Client _client = null!; + private S3Wrapper _s3Wrapper = null!; + + /// + /// Verifies the presigned POST URL scenario with an integration test. No errors should be logged. + /// + /// Async task. + [Fact] + [Trait("Category", "Integration")] + public async Task TestScenario() + { + // Arrange. + var loggerScenarioMock = new Mock>(); + var loggerWrapperMock = new Mock>(); + var uiMethods = new S3Scenarios.UiMethods(); + + _client = new AmazonS3Client(); + _s3Wrapper = new S3Wrapper(_client, loggerWrapperMock.Object); + + // Set up the static fields directly + S3Scenarios.CreatePresignedPostBasics._logger = loggerScenarioMock.Object; + S3Scenarios.CreatePresignedPostBasics._s3Wrapper = _s3Wrapper; + S3Scenarios.CreatePresignedPostBasics._uiMethods = uiMethods; + S3Scenarios.CreatePresignedPostBasics._isInteractive = false; + + // Set up verification for error logging. + loggerScenarioMock.Setup(logger => logger.Log( + It.Is(logLevel => logLevel == LogLevel.Error), + It.IsAny(), + It.Is((@object, @type) => true), + It.IsAny(), + It.IsAny>() + )); + + loggerWrapperMock.Setup(logger => logger.Log( + It.Is(logLevel => logLevel == LogLevel.Error), + It.IsAny(), + It.Is((@object, @type) => true), + It.IsAny(), + It.IsAny>() + )); + + // Act. + // Call the static Main method with --non-interactive flag to match previous behavior + await S3Scenarios.CreatePresignedPostBasics.Main(new string[] { "--non-interactive" }); + + // Assert no exceptions or errors logged. + loggerScenarioMock.Verify( + logger => logger.Log( + It.Is(logLevel => logLevel == LogLevel.Error), + It.IsAny(), + It.Is((@object, @type) => true), + It.IsAny(), + It.IsAny>()), + Times.Never); + + loggerWrapperMock.Verify( + logger => logger.Log( + It.Is(logLevel => logLevel == LogLevel.Error), + It.IsAny(), + It.Is((@object, @type) => true), + It.IsAny(), + It.IsAny>()), + Times.Never); + } +} \ No newline at end of file diff --git a/dotnetv4/S3/Tests/S3Tests.csproj b/dotnetv4/S3/Tests/S3Tests.csproj new file mode 100644 index 00000000000..81cfde53213 --- /dev/null +++ b/dotnetv4/S3/Tests/S3Tests.csproj @@ -0,0 +1,30 @@ + + + + net8.0 + enable + enable + + false + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + diff --git a/dotnetv4/S3/Tests/Usings.cs b/dotnetv4/S3/Tests/Usings.cs new file mode 100644 index 00000000000..7fa523be4c4 --- /dev/null +++ b/dotnetv4/S3/Tests/Usings.cs @@ -0,0 +1,8 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +global using Amazon.S3; +global using Microsoft.Extensions.Logging; +global using Moq; +global using S3Scenarios; +global using Xunit; \ No newline at end of file