From 776530305e6c28b51bf86d9ba66d42415de3fc48 Mon Sep 17 00:00:00 2001 From: Garrett Beatty Date: Mon, 1 Dec 2025 11:20:38 -0500 Subject: [PATCH 1/3] DownloadDirectoryProgress events --- .../Internal/DownloadDirectoryCommand.cs | 33 ++ .../Internal/_async/DownloadCommand.async.cs | 6 +- .../_async/SimpleUploadCommand.async.cs | 4 +- .../DownloadDirectoryCommand.cs | 66 ++-- ...TransferUtilityDownloadDirectoryRequest.cs | 222 +++++++++++- ...rUtilityDownloadDirectoryLifecycleTests.cs | 342 ++++++++++++++++++ 6 files changed, 640 insertions(+), 33 deletions(-) create mode 100644 sdk/test/Services/S3/IntegrationTests/TransferUtilityDownloadDirectoryLifecycleTests.cs diff --git a/sdk/src/Services/S3/Custom/Transfer/Internal/DownloadDirectoryCommand.cs b/sdk/src/Services/S3/Custom/Transfer/Internal/DownloadDirectoryCommand.cs index 77f868a520fa..4c897a5e9f1b 100644 --- a/sdk/src/Services/S3/Custom/Transfer/Internal/DownloadDirectoryCommand.cs +++ b/sdk/src/Services/S3/Custom/Transfer/Internal/DownloadDirectoryCommand.cs @@ -48,6 +48,39 @@ internal partial class DownloadDirectoryCommand : BaseCommand ExecuteAsync(CancellationToken cancellationToken) { - ValidateRequest(); - FireTransferInitiatedEvent(); - + + ValidateRequest(); + GetObjectRequest getRequest = ConvertToGetObjectRequest(this._request); var maxRetries = _s3Client.Config.MaxErrorRetry; diff --git a/sdk/src/Services/S3/Custom/Transfer/Internal/_async/SimpleUploadCommand.async.cs b/sdk/src/Services/S3/Custom/Transfer/Internal/_async/SimpleUploadCommand.async.cs index 1d936f0bdf5c..3b350c1bd877 100644 --- a/sdk/src/Services/S3/Custom/Transfer/Internal/_async/SimpleUploadCommand.async.cs +++ b/sdk/src/Services/S3/Custom/Transfer/Internal/_async/SimpleUploadCommand.async.cs @@ -32,14 +32,14 @@ public override async Task ExecuteAsync(Cancellat { try { + FireTransferInitiatedEvent(); + if (AsyncThrottler != null) { await this.AsyncThrottler.WaitAsync(cancellationToken) .ConfigureAwait(continueOnCapturedContext: false); } - FireTransferInitiatedEvent(); - var putRequest = ConstructRequest(); var response = await _s3Client.PutObjectAsync(putRequest, cancellationToken) .ConfigureAwait(continueOnCapturedContext: false); diff --git a/sdk/src/Services/S3/Custom/Transfer/Internal/_bcl+netstandard/DownloadDirectoryCommand.cs b/sdk/src/Services/S3/Custom/Transfer/Internal/_bcl+netstandard/DownloadDirectoryCommand.cs index f9a44ec2b83a..3e426198ff0a 100644 --- a/sdk/src/Services/S3/Custom/Transfer/Internal/_bcl+netstandard/DownloadDirectoryCommand.cs +++ b/sdk/src/Services/S3/Custom/Transfer/Internal/_bcl+netstandard/DownloadDirectoryCommand.cs @@ -48,40 +48,52 @@ internal DownloadDirectoryCommand(IAmazonS3 s3Client, TransferUtilityDownloadDir public override async Task ExecuteAsync(CancellationToken cancellationToken) { - Logger.DebugFormat("DownloadDirectoryCommand.ExecuteAsync: Starting - DownloadFilesConcurrently={0}, UseMultipartDownload={1}, ConcurrentServiceRequests={2}", - DownloadFilesConcurrently, this._useMultipartDownload, this._config.ConcurrentServiceRequests); + try + { + FireTransferInitiatedEvent(); - // Step 1: Validate and setup - ValidateRequest(); - EnsureDirectoryExists(new DirectoryInfo(this._request.LocalDirectory)); + Logger.DebugFormat("DownloadDirectoryCommand.ExecuteAsync: Starting - DownloadFilesConcurrently={0}, UseMultipartDownload={1}, ConcurrentServiceRequests={2}", + DownloadFilesConcurrently, this._useMultipartDownload, this._config.ConcurrentServiceRequests); - // Step 2: List S3 objects - var (s3Objects, prefixLength) = await ListS3ObjectsAsync(cancellationToken) - .ConfigureAwait(false); + // Step 1: Validate and setup + ValidateRequest(); + EnsureDirectoryExists(new DirectoryInfo(this._request.LocalDirectory)); - this._totalNumberOfFilesToDownload = s3Objects.Count; - Logger.DebugFormat("DownloadDirectoryCommand.ExecuteAsync: Found {0} total objects, TotalBytes={1}", - s3Objects.Count, this._totalBytes); + // Step 2: List S3 objects + var (s3Objects, prefixLength) = await ListS3ObjectsAsync(cancellationToken) + .ConfigureAwait(false); - // Step 3: Filter to actual files (exclude directory markers) - var objectsToDownload = FilterObjectsToDownload(s3Objects); + this._totalNumberOfFilesToDownload = s3Objects.Count; + Logger.DebugFormat("DownloadDirectoryCommand.ExecuteAsync: Found {0} total objects, TotalBytes={1}", + s3Objects.Count, this._totalBytes); - // Step 4: Setup resources and execute downloads - using (var resources = CreateDownloadResources(cancellationToken)) - { - await ExecuteParallelDownloadsAsync( - objectsToDownload, - prefixLength, - resources, - cancellationToken) - .ConfigureAwait(false); - } + // Step 3: Filter to actual files (exclude directory markers) + var objectsToDownload = FilterObjectsToDownload(s3Objects); - // Step 5: Build response - Logger.DebugFormat("DownloadDirectoryCommand.ExecuteAsync: Completed - ObjectsDownloaded={0}, ObjectsFailed={1}", - _numberOfFilesDownloaded, _errors.Count); + // Step 4: Setup resources and execute downloads + using (var resources = CreateDownloadResources(cancellationToken)) + { + await ExecuteParallelDownloadsAsync( + objectsToDownload, + prefixLength, + resources, + cancellationToken) + .ConfigureAwait(false); + } + + // Step 5: Build response + Logger.DebugFormat("DownloadDirectoryCommand.ExecuteAsync: Completed - ObjectsDownloaded={0}, ObjectsFailed={1}", + _numberOfFilesDownloaded, _errors.Count); - return BuildResponse(); + var response = BuildResponse(); + FireTransferCompletedEvent(response); + return response; + } + catch + { + FireTransferFailedEvent(); + throw; + } } /// diff --git a/sdk/src/Services/S3/Custom/Transfer/TransferUtilityDownloadDirectoryRequest.cs b/sdk/src/Services/S3/Custom/Transfer/TransferUtilityDownloadDirectoryRequest.cs index 9931d29c1e8c..c36b8e03cb3d 100644 --- a/sdk/src/Services/S3/Custom/Transfer/TransferUtilityDownloadDirectoryRequest.cs +++ b/sdk/src/Services/S3/Custom/Transfer/TransferUtilityDownloadDirectoryRequest.cs @@ -96,6 +96,97 @@ internal void OnRaiseObjectDownloadFailedEvent(ObjectDownloadFailedEventArgs arg ObjectDownloadFailedEvent?.Invoke(this, args); } + /// + /// Occurs when the download directory operation is initiated. + /// + /// + /// + /// The DownloadDirectoryInitiatedEvent is fired when the download directory operation begins. + /// The DownloadDirectoryInitiatedEventArgs contains the original request information. + /// + /// + /// Attach event handlers to this event if you are interested in receiving + /// DownloadDirectoryInitiatedEvent notifications. + /// + /// + /// + /// private void downloadStarted(object sender, DownloadDirectoryInitiatedEventArgs args) + /// { + /// Console.WriteLine("Download directory started for bucket {0}", args.Request.BucketName); + /// } + /// + public event EventHandler DownloadDirectoryInitiatedEvent; + + /// + /// Occurs when the download directory operation is completed. + /// + /// + /// + /// The DownloadDirectoryCompletedEvent is fired when the download directory operation is completed successfully. + /// The DownloadDirectoryCompletedEventArgs contains a snapshot of the transfer state at completion. + /// + /// + /// Attach event handlers to this event if you are interested in receiving + /// DownloadDirectoryCompletedEvent notifications. + /// + /// + /// + /// private void downloadCompleted(object sender, DownloadDirectoryCompletedEventArgs args) + /// { + /// Console.WriteLine("Download directory completed with {0} files downloaded", args.TransferredFiles); + /// } + /// + public event EventHandler DownloadDirectoryCompletedEvent; + + /// + /// Occurs when the download directory operation fails. + /// + /// + /// + /// The DownloadDirectoryFailedEvent is fired when the download directory operation fails. + /// The DownloadDirectoryFailedEventArgs contains a snapshot of the transfer state at failure. + /// + /// + /// Attach event handlers to this event if you are interested in receiving + /// DownloadDirectoryFailedEvent notifications. + /// + /// + /// + /// private void downloadFailed(object sender, DownloadDirectoryFailedEventArgs args) + /// { + /// Console.WriteLine("Download directory failed with {0} files downloaded out of {1} total", + /// args.TransferredFiles, args.TotalFiles); + /// } + /// + public event EventHandler DownloadDirectoryFailedEvent; + + /// + /// Raises the DownloadDirectoryInitiatedEvent. + /// + /// DownloadDirectoryInitiatedEventArgs args + internal void OnRaiseDownloadDirectoryInitiatedEvent(DownloadDirectoryInitiatedEventArgs args) + { + DownloadDirectoryInitiatedEvent?.Invoke(this, args); + } + + /// + /// Raises the DownloadDirectoryCompletedEvent. + /// + /// DownloadDirectoryCompletedEventArgs args + internal void OnRaiseDownloadDirectoryCompletedEvent(DownloadDirectoryCompletedEventArgs args) + { + DownloadDirectoryCompletedEvent?.Invoke(this, args); + } + + /// + /// Raises the DownloadDirectoryFailedEvent. + /// + /// DownloadDirectoryFailedEventArgs args + internal void OnRaiseDownloadDirectoryFailedEvent(DownloadDirectoryFailedEventArgs args) + { + DownloadDirectoryFailedEvent?.Invoke(this, args); + } + /// /// Gets or sets the name of the bucket. /// @@ -668,4 +759,133 @@ internal ObjectDownloadFailedEventArgs( /// public Exception Exception { get; private set; } } -} \ No newline at end of file + + /// + /// Provides data for + /// which is raised when a download directory operation is initiated. + /// + public class DownloadDirectoryInitiatedEventArgs : EventArgs + { + /// + /// Initializes a new instance of the DownloadDirectoryInitiatedEventArgs class. + /// + /// The transfer request + internal DownloadDirectoryInitiatedEventArgs(TransferUtilityDownloadDirectoryRequest request) + { + Request = request; + } + + /// + /// Gets the request associated with this transfer operation. + /// + public TransferUtilityDownloadDirectoryRequest Request { get; private set; } + } + + /// + /// Provides data for + /// which is raised when a download directory operation is completed successfully. + /// + public class DownloadDirectoryCompletedEventArgs : EventArgs + { + /// + /// Initializes a new instance of the DownloadDirectoryCompletedEventArgs class. + /// + /// The transfer request + /// The transfer response + /// The total number of bytes that have been transferred so far + /// The total size for all objects + /// The total number of files that have been transferred so far + /// The total number of files + internal DownloadDirectoryCompletedEventArgs(TransferUtilityDownloadDirectoryRequest request, + TransferUtilityDownloadDirectoryResponse response, long transferredBytes, long totalBytes, + long transferredFiles, long totalFiles) + { + Request = request; + Response = response; + TransferredBytes = transferredBytes; + TotalBytes = totalBytes; + TransferredFiles = transferredFiles; + TotalFiles = totalFiles; + } + + /// + /// Gets the request associated with this transfer operation. + /// + public TransferUtilityDownloadDirectoryRequest Request { get; private set; } + + /// + /// Gets the response from the transfer operation. + /// + public TransferUtilityDownloadDirectoryResponse Response { get; private set; } + + /// + /// Gets the total number of bytes that have been transferred so far. + /// + public long TransferredBytes { get; private set; } + + /// + /// Gets the total size for all objects. Returns -1 if unknown. + /// + public long TotalBytes { get; private set; } + + /// + /// Gets the total number of files that have been transferred so far. + /// + public long TransferredFiles { get; private set; } + + /// + /// Gets the total number of files. Returns -1 if unknown. + /// + public long TotalFiles { get; private set; } + } + + /// + /// Provides data for + /// which is raised when a download directory operation fails. + /// + public class DownloadDirectoryFailedEventArgs : EventArgs + { + /// + /// Initializes a new instance of the DownloadDirectoryFailedEventArgs class. + /// + /// The transfer request + /// The total number of bytes that have been transferred so far + /// The total size for all objects + /// The total number of files that have been transferred so far + /// The total number of files + internal DownloadDirectoryFailedEventArgs(TransferUtilityDownloadDirectoryRequest request, + long transferredBytes, long totalBytes, long transferredFiles, long totalFiles) + { + Request = request; + TransferredBytes = transferredBytes; + TotalBytes = totalBytes; + TransferredFiles = transferredFiles; + TotalFiles = totalFiles; + } + + /// + /// Gets the request associated with this transfer operation. + /// + public TransferUtilityDownloadDirectoryRequest Request { get; private set; } + + /// + /// Gets the total number of bytes that have been transferred so far. + /// + public long TransferredBytes { get; private set; } + + /// + /// Gets the total size for all objects. Returns -1 if unknown. + /// + public long TotalBytes { get; private set; } + + /// + /// Gets the total number of files that have been transferred so far. + /// + public long TransferredFiles { get; private set; } + + /// + /// Gets the total number of files. Returns -1 if unknown. + /// + public long TotalFiles { get; private set; } + } +} diff --git a/sdk/test/Services/S3/IntegrationTests/TransferUtilityDownloadDirectoryLifecycleTests.cs b/sdk/test/Services/S3/IntegrationTests/TransferUtilityDownloadDirectoryLifecycleTests.cs new file mode 100644 index 000000000000..874846b26753 --- /dev/null +++ b/sdk/test/Services/S3/IntegrationTests/TransferUtilityDownloadDirectoryLifecycleTests.cs @@ -0,0 +1,342 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Amazon.S3; +using Amazon.S3.Model; +using Amazon.S3.Transfer; +using Amazon.S3.Util; +using AWSSDK_DotNet.IntegrationTests.Utils; + +namespace AWSSDK_DotNet.IntegrationTests.Tests.S3 +{ + /// + /// Integration tests for TransferUtility download directory lifecycle events. + /// Tests the initiated, completed, and failed events for directory downloads. + /// + [TestClass] + public class TransferUtilityDownloadDirectoryLifecycleTests : TestBase + { + public static readonly long MEG_SIZE = (int)Math.Pow(2, 20); + public static readonly long KILO_SIZE = (int)Math.Pow(2, 10); + public static readonly string BasePath = Path.Combine(Path.GetTempPath(), "transferutility", "downloaddirectorylifecycle"); + + private static string bucketName; + private static string plainTextContentType = "text/plain"; + + [ClassInitialize()] + public static void ClassInitialize(TestContext a) + { + bucketName = S3TestUtils.CreateBucketWithWait(Client); + } + + [ClassCleanup] + public static void ClassCleanup() + { + AmazonS3Util.DeleteS3BucketWithObjects(Client, bucketName); + BaseClean(); + if (Directory.Exists(BasePath)) + { + Directory.Delete(BasePath, true); + } + } + + [TestMethod] + [TestCategory("S3")] + public void DownloadDirectoryInitiatedEventTest() + { + var eventValidator = new TransferLifecycleEventValidator + { + Validate = (args) => + { + Assert.IsNotNull(args.Request); + Assert.IsNotNull(args.Request.BucketName); + Assert.IsNotNull(args.Request.S3Directory); + Assert.IsNotNull(args.Request.LocalDirectory); + + // Verify that progress properties are NOT present (consistent with our changes) + // The initiated event should only contain the request information + Console.WriteLine($"DownloadDirectoryInitiated - BucketName: {args.Request.BucketName}"); + Console.WriteLine($"DownloadDirectoryInitiated - S3Directory: {args.Request.S3Directory}"); + Console.WriteLine($"DownloadDirectoryInitiated - LocalDirectory: {args.Request.LocalDirectory}"); + } + }; + DownloadDirectoryWithLifecycleEvents(10 * MEG_SIZE, eventValidator, null, null); + eventValidator.AssertEventFired(); + } + + [TestMethod] + [TestCategory("S3")] + public void DownloadDirectoryCompletedEventTest() + { + var eventValidator = new TransferLifecycleEventValidator + { + Validate = (args) => + { + Assert.IsNotNull(args.Request); + Assert.IsNotNull(args.Response); + + // Verify progress information is available in completed event + Assert.IsTrue(args.TotalFiles > 0, "TotalFiles should be greater than 0"); + Assert.AreEqual(args.TransferredFiles, args.TotalFiles, "All files should be transferred"); + Assert.IsTrue(args.TotalBytes > 0, "TotalBytes should be greater than 0"); + Assert.AreEqual(args.TransferredBytes, args.TotalBytes, "All bytes should be transferred"); + + Console.WriteLine($"DownloadDirectoryCompleted - TotalFiles: {args.TotalFiles}"); + Console.WriteLine($"DownloadDirectoryCompleted - TransferredFiles: {args.TransferredFiles}"); + Console.WriteLine($"DownloadDirectoryCompleted - TotalBytes: {args.TotalBytes}"); + Console.WriteLine($"DownloadDirectoryCompleted - TransferredBytes: {args.TransferredBytes}"); + } + }; + DownloadDirectoryWithLifecycleEvents(12 * MEG_SIZE, null, eventValidator, null); + eventValidator.AssertEventFired(); + } + + [TestMethod] + [TestCategory("S3")] + public void DownloadDirectoryFailedEventTest() + { + var eventValidator = new TransferLifecycleEventValidator + { + Validate = (args) => + { + Assert.IsNotNull(args.Request); + + // The DownloadDirectoryFailedEventArgs contains transfer progress information + // but does not contain exception details (unlike individual object failures) + Console.WriteLine($"DownloadDirectoryFailed - Request: {args.Request.BucketName}"); + Console.WriteLine($"DownloadDirectoryFailed - S3Directory: {args.Request.S3Directory}"); + + // Progress information may or may not be available depending on when the failure occurred + Console.WriteLine($"DownloadDirectoryFailed - TotalFiles: {args.TotalFiles}"); + Console.WriteLine($"DownloadDirectoryFailed - TransferredFiles: {args.TransferredFiles}"); + Console.WriteLine($"DownloadDirectoryFailed - TotalBytes: {args.TotalBytes}"); + Console.WriteLine($"DownloadDirectoryFailed - TransferredBytes: {args.TransferredBytes}"); + } + }; + + // Use an invalid bucket name to force a real exception + // Bucket names with uppercase letters are invalid and will cause an exception + var invalidBucketName = "INVALID-BUCKET-NAME-" + Guid.NewGuid().ToString(); + var testDirectory = "test-directory"; + + var localDirectory = GenerateDirectoryPath(); + + var transferUtility = new TransferUtility(Client); + var request = new TransferUtilityDownloadDirectoryRequest + { + BucketName = invalidBucketName, // This will cause an exception due to invalid bucket name + LocalDirectory = localDirectory, + S3Directory = testDirectory + }; + + request.DownloadDirectoryFailedEvent += eventValidator.OnEventFired; + + try + { + transferUtility.DownloadDirectory(request); + Assert.Fail("Expected an exception to be thrown for invalid bucket name"); + } + catch (Exception ex) + { + // Expected exception - the failed event should have been fired + Console.WriteLine($"Expected exception caught: {ex.GetType().Name} - {ex.Message}"); + } + + eventValidator.AssertEventFired(); + } + + [TestMethod] + [TestCategory("S3")] + public void DownloadDirectoryCompleteLifecycleTest() + { + var initiatedValidator = new TransferLifecycleEventValidator + { + Validate = (args) => + { + Assert.IsNotNull(args.Request); + Assert.AreEqual(bucketName, args.Request.BucketName); + Assert.IsNotNull(args.Request.S3Directory); + Assert.IsNotNull(args.Request.LocalDirectory); + } + }; + + var completedValidator = new TransferLifecycleEventValidator + { + Validate = (args) => + { + Assert.IsNotNull(args.Request); + Assert.IsNotNull(args.Response); + Assert.AreEqual(args.TransferredFiles, args.TotalFiles); + Assert.AreEqual(args.TransferredBytes, args.TotalBytes); + Assert.IsTrue(args.TotalFiles > 0, "Should have downloaded at least one file"); + } + }; + + DownloadDirectoryWithLifecycleEvents(15 * MEG_SIZE, initiatedValidator, completedValidator, null); + + initiatedValidator.AssertEventFired(); + completedValidator.AssertEventFired(); + } + + #region Helper Methods + + void DownloadDirectoryWithLifecycleEvents(long fileSize, + TransferLifecycleEventValidator initiatedValidator, + TransferLifecycleEventValidator completedValidator, + TransferLifecycleEventValidator failedValidator) + { + // First create and upload a test directory + var testDirectory = CreateAndUploadTestDirectory(fileSize); + var s3Directory = testDirectory.Name; + + DownloadDirectoryWithLifecycleEventsAndS3Directory(s3Directory, initiatedValidator, completedValidator, failedValidator); + } + + void DownloadDirectoryWithLifecycleEventsAndS3Directory(string s3Directory, + TransferLifecycleEventValidator initiatedValidator, + TransferLifecycleEventValidator completedValidator, + TransferLifecycleEventValidator failedValidator) + { + var localDirectory = GenerateDirectoryPath(); + + var transferUtility = new TransferUtility(Client); + var request = new TransferUtilityDownloadDirectoryRequest + { + BucketName = bucketName, + LocalDirectory = localDirectory, + S3Directory = s3Directory + }; + + if (initiatedValidator != null) + { + request.DownloadDirectoryInitiatedEvent += initiatedValidator.OnEventFired; + } + + if (completedValidator != null) + { + request.DownloadDirectoryCompletedEvent += completedValidator.OnEventFired; + } + + if (failedValidator != null) + { + request.DownloadDirectoryFailedEvent += failedValidator.OnEventFired; + } + + transferUtility.DownloadDirectory(request); + + // Validate downloaded directory contents if it was successful + if (Directory.Exists(localDirectory)) + { + var downloadedFiles = Directory.GetFiles(localDirectory, "*", SearchOption.AllDirectories); + Console.WriteLine($"Downloaded {downloadedFiles.Length} files to {localDirectory}"); + } + } + + DirectoryInfo CreateAndUploadTestDirectory(long fileSize, int numberOfTestFiles = 3) + { + var directory = CreateTestDirectory(fileSize, numberOfTestFiles); + var keyPrefix = directory.Name; + var directoryPath = directory.FullName; + + var transferUtility = new TransferUtility(Client); + var request = new TransferUtilityUploadDirectoryRequest + { + BucketName = bucketName, + Directory = directoryPath, + KeyPrefix = keyPrefix, + ContentType = plainTextContentType, + SearchPattern = "*", + SearchOption = SearchOption.AllDirectories, + }; + + transferUtility.UploadDirectory(request); + + // Validate the upload was successful + ValidateDirectoryContentsInS3(Client, bucketName, keyPrefix, directory); + + return directory; + } + + public static DirectoryInfo CreateTestDirectory(long fileSize = 0, int numberOfTestFiles = 3) + { + if (fileSize == 0) + fileSize = 1 * MEG_SIZE; + + var directoryPath = GenerateDirectoryPath(); + for (int i = 0; i < numberOfTestFiles; i++) + { + var filePath = Path.Combine(Path.Combine(directoryPath, i.ToString()), "file.txt"); + UtilityMethods.GenerateFile(filePath, fileSize); + } + + return new DirectoryInfo(directoryPath); + } + + public static string GenerateDirectoryPath(string baseName = "DownloadDirectoryLifecycleTest") + { + var directoryName = UtilityMethods.GenerateName(baseName); + var directoryPath = Path.Combine(BasePath, directoryName); + return directoryPath; + } + + public static void ValidateDirectoryContentsInS3(IAmazonS3 s3client, string bucketName, string keyPrefix, DirectoryInfo sourceDirectory) + { + var directoryPath = sourceDirectory.FullName; + var files = sourceDirectory.GetFiles("*", SearchOption.AllDirectories); + foreach (var file in files) + { + var filePath = file.FullName; + var relativePath = filePath.Substring(directoryPath.Length + 1); + var key = (!string.IsNullOrEmpty(keyPrefix) ? keyPrefix + "/" : string.Empty) + relativePath.Replace("\\", "/"); + + // Verify the object exists in S3 + var metadata = s3client.GetObjectMetadata(new GetObjectMetadataRequest + { + BucketName = bucketName, + Key = key + }); + Assert.IsNotNull(metadata, $"Object {key} should exist in S3"); + } + } + + #endregion + + #region Shared Helper Classes + + class TransferLifecycleEventValidator + { + public Action Validate { get; set; } + public bool EventFired { get; private set; } + public Exception EventException { get; private set; } + + public void OnEventFired(object sender, T eventArgs) + { + try + { + Console.WriteLine("Lifecycle Event Fired: {0}", typeof(T).Name); + Validate?.Invoke(eventArgs); + EventFired = true; // Only set if validation passes + } + catch (Exception ex) + { + EventException = ex; + EventFired = false; // Ensure we don't mark as fired on failure + Console.WriteLine("Exception caught in lifecycle event: {0}", ex.Message); + // Don't re-throw, let AssertEventFired() handle it + } + } + + public void AssertEventFired() + { + if (EventException != null) + throw EventException; + Assert.IsTrue(EventFired, $"{typeof(T).Name} event was not fired"); + } + } + + #endregion + } +} From d762cba9e4e2e663effb90224251273c8d8ec884 Mon Sep 17 00:00:00 2001 From: Garrett Beatty Date: Wed, 3 Dec 2025 12:01:17 -0500 Subject: [PATCH 2/3] remove console.writleline --- ...rUtilityDownloadDirectoryLifecycleTests.cs | 22 ------------------- 1 file changed, 22 deletions(-) diff --git a/sdk/test/Services/S3/IntegrationTests/TransferUtilityDownloadDirectoryLifecycleTests.cs b/sdk/test/Services/S3/IntegrationTests/TransferUtilityDownloadDirectoryLifecycleTests.cs index 874846b26753..a3f4fa660324 100644 --- a/sdk/test/Services/S3/IntegrationTests/TransferUtilityDownloadDirectoryLifecycleTests.cs +++ b/sdk/test/Services/S3/IntegrationTests/TransferUtilityDownloadDirectoryLifecycleTests.cs @@ -55,12 +55,6 @@ public void DownloadDirectoryInitiatedEventTest() Assert.IsNotNull(args.Request.BucketName); Assert.IsNotNull(args.Request.S3Directory); Assert.IsNotNull(args.Request.LocalDirectory); - - // Verify that progress properties are NOT present (consistent with our changes) - // The initiated event should only contain the request information - Console.WriteLine($"DownloadDirectoryInitiated - BucketName: {args.Request.BucketName}"); - Console.WriteLine($"DownloadDirectoryInitiated - S3Directory: {args.Request.S3Directory}"); - Console.WriteLine($"DownloadDirectoryInitiated - LocalDirectory: {args.Request.LocalDirectory}"); } }; DownloadDirectoryWithLifecycleEvents(10 * MEG_SIZE, eventValidator, null, null); @@ -83,11 +77,6 @@ public void DownloadDirectoryCompletedEventTest() Assert.AreEqual(args.TransferredFiles, args.TotalFiles, "All files should be transferred"); Assert.IsTrue(args.TotalBytes > 0, "TotalBytes should be greater than 0"); Assert.AreEqual(args.TransferredBytes, args.TotalBytes, "All bytes should be transferred"); - - Console.WriteLine($"DownloadDirectoryCompleted - TotalFiles: {args.TotalFiles}"); - Console.WriteLine($"DownloadDirectoryCompleted - TransferredFiles: {args.TransferredFiles}"); - Console.WriteLine($"DownloadDirectoryCompleted - TotalBytes: {args.TotalBytes}"); - Console.WriteLine($"DownloadDirectoryCompleted - TransferredBytes: {args.TransferredBytes}"); } }; DownloadDirectoryWithLifecycleEvents(12 * MEG_SIZE, null, eventValidator, null); @@ -103,17 +92,6 @@ public void DownloadDirectoryFailedEventTest() Validate = (args) => { Assert.IsNotNull(args.Request); - - // The DownloadDirectoryFailedEventArgs contains transfer progress information - // but does not contain exception details (unlike individual object failures) - Console.WriteLine($"DownloadDirectoryFailed - Request: {args.Request.BucketName}"); - Console.WriteLine($"DownloadDirectoryFailed - S3Directory: {args.Request.S3Directory}"); - - // Progress information may or may not be available depending on when the failure occurred - Console.WriteLine($"DownloadDirectoryFailed - TotalFiles: {args.TotalFiles}"); - Console.WriteLine($"DownloadDirectoryFailed - TransferredFiles: {args.TransferredFiles}"); - Console.WriteLine($"DownloadDirectoryFailed - TotalBytes: {args.TotalBytes}"); - Console.WriteLine($"DownloadDirectoryFailed - TransferredBytes: {args.TransferredBytes}"); } }; From 2d1b0786259c94f89dc7b797bff802cf2821bd42 Mon Sep 17 00:00:00 2001 From: Garrett Beatty Date: Wed, 3 Dec 2025 12:04:39 -0500 Subject: [PATCH 3/3] dev config --- .../7f23582e-3225-487b-83e7-167cf17cb234.json | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 generator/.DevConfigs/7f23582e-3225-487b-83e7-167cf17cb234.json diff --git a/generator/.DevConfigs/7f23582e-3225-487b-83e7-167cf17cb234.json b/generator/.DevConfigs/7f23582e-3225-487b-83e7-167cf17cb234.json new file mode 100644 index 000000000000..179605fd4aab --- /dev/null +++ b/generator/.DevConfigs/7f23582e-3225-487b-83e7-167cf17cb234.json @@ -0,0 +1,11 @@ +{ + "services": [ + { + "serviceName": "S3", + "type": "minor", + "changeLogMessages": [ + "Added DownloadDirectoryInitiatedEvent, DownloadDirectoryCompletedEvent, and DownloadDirectoryFailedEvent for Amazon.S3.Transfer.TransferUtility.DownloadDirectory." + ] + } + ] +} \ No newline at end of file