Skip to content

Commit 135eacc

Browse files
committed
Add DownloadDirectoryWithResponse
1 parent f901ac8 commit 135eacc

16 files changed

+2148
-33
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"services": [
3+
{
4+
"serviceName": "S3",
5+
"type": "minor",
6+
"changeLogMessages": [
7+
"Created new DownloadDirectoryWithResponseAsync methods on the Amazon.S3.Transfer.TransferUtility class. The new operations support downloading directories using multipart download for files and return response metadata."
8+
]
9+
}
10+
]
11+
}

sdk/src/Services/S3/Custom/Transfer/Internal/DownloadDirectoryCommand.cs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,20 +38,35 @@ internal partial class DownloadDirectoryCommand : BaseCommand<TransferUtilityDow
3838
private readonly IAmazonS3 _s3Client;
3939
private readonly TransferUtilityDownloadDirectoryRequest _request;
4040
private readonly bool _skipEncryptionInstructionFiles;
41+
private readonly bool _useMultipartDownload;
4142
int _totalNumberOfFilesToDownload;
4243
int _numberOfFilesDownloaded;
4344
long _totalBytes;
4445
long _transferredBytes;
4546
string _currentFile;
4647

4748
internal DownloadDirectoryCommand(IAmazonS3 s3Client, TransferUtilityDownloadDirectoryRequest request)
49+
: this(s3Client, request, useMultipartDownload: false)
50+
{
51+
}
52+
53+
internal DownloadDirectoryCommand(IAmazonS3 s3Client, TransferUtilityDownloadDirectoryRequest request, bool useMultipartDownload)
4854
{
4955
if (s3Client == null)
50-
throw new ArgumentNullException("s3Client");
56+
throw new ArgumentNullException(nameof(s3Client));
57+
if (request == null)
58+
throw new ArgumentNullException(nameof(request));
5159

5260
this._s3Client = s3Client;
5361
this._request = request;
5462
this._skipEncryptionInstructionFiles = s3Client is Amazon.S3.Internal.IAmazonS3Encryption;
63+
this._useMultipartDownload = useMultipartDownload;
64+
}
65+
66+
internal DownloadDirectoryCommand(IAmazonS3 s3Client, TransferUtilityDownloadDirectoryRequest request, TransferUtilityConfig config, bool useMultipartDownload)
67+
: this(s3Client, request, useMultipartDownload)
68+
{
69+
this._config = config;
5570
}
5671

5772
private void downloadedProgressEventCallback(object sender, WriteObjectProgressArgs e)

sdk/src/Services/S3/Custom/Transfer/Internal/FilePartDataHandler.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ private Logger Logger
4747
get { return Logger.GetLogger(typeof(TransferUtility)); }
4848
}
4949

50+
/// <summary>
51+
/// Initializes a new instance for file downloads.
52+
/// Writes parts directly to disk without memory buffering.
53+
/// </summary>
5054
public FilePartDataHandler(FileDownloadConfiguration config)
5155
{
5256
_config = config ?? throw new ArgumentNullException(nameof(config));
@@ -90,14 +94,16 @@ await WritePartToFileAsync(offset, response, cancellationToken)
9094
/// <inheritdoc/>
9195
public Task WaitForCapacityAsync(CancellationToken cancellationToken)
9296
{
93-
// No backpressure needed - OS handles concurrent file access
97+
// No-op: FilePartDataHandler writes directly to disk without buffering parts in memory.
98+
// Memory throttling is only needed for BufferedPartDataHandler which keeps parts in memory.
9499
return Task.CompletedTask;
95100
}
96101

97102
/// <inheritdoc/>
98103
public void ReleaseCapacity()
99104
{
100-
// No-op
105+
// No-op: FilePartDataHandler writes directly to disk without buffering parts in memory.
106+
// Memory throttling is only needed for BufferedPartDataHandler which keeps parts in memory.
101107
}
102108

103109
/// <inheritdoc/>

sdk/src/Services/S3/Custom/Transfer/Internal/MultipartDownloadCommand.cs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
*
2121
*/
2222
using System;
23+
using System.Threading;
2324
using Amazon.Runtime.Internal.Util;
2425
using Amazon.S3.Model;
2526
using Amazon.S3.Util;
@@ -36,6 +37,7 @@ internal partial class MultipartDownloadCommand : BaseCommand<TransferUtilityDow
3637
private readonly IAmazonS3 _s3Client;
3738
private readonly TransferUtilityDownloadRequest _request;
3839
private readonly TransferUtilityConfig _config;
40+
private readonly SemaphoreSlim _sharedHttpThrottler;
3941

4042
// Track last known transferred bytes from coordinator's progress events
4143
private long _lastKnownTransferredBytes;
@@ -49,16 +51,29 @@ private static Logger Logger
4951
}
5052

5153
/// <summary>
52-
/// Initializes a new instance of the MultipartDownloadCommand class.
54+
/// Initializes a new instance of the MultipartDownloadCommand class for single file downloads.
5355
/// </summary>
5456
/// <param name="s3Client">The S3 client to use for downloads.</param>
5557
/// <param name="request">The download request containing configuration.</param>
5658
/// <param name="config">The TransferUtility configuration.</param>
5759
internal MultipartDownloadCommand(IAmazonS3 s3Client, TransferUtilityDownloadRequest request, TransferUtilityConfig config)
60+
: this(s3Client, request, config, null)
61+
{
62+
}
63+
64+
/// <summary>
65+
/// Initializes a new instance of the MultipartDownloadCommand class for directory downloads.
66+
/// </summary>
67+
/// <param name="s3Client">The S3 client to use for downloads.</param>
68+
/// <param name="request">The download request containing configuration.</param>
69+
/// <param name="config">The TransferUtility configuration.</param>
70+
/// <param name="sharedHttpThrottler">Shared HTTP concurrency throttler for directory operations, or null for single file downloads.</param>
71+
internal MultipartDownloadCommand(IAmazonS3 s3Client, TransferUtilityDownloadRequest request, TransferUtilityConfig config, SemaphoreSlim sharedHttpThrottler)
5872
{
5973
_s3Client = s3Client ?? throw new ArgumentNullException(nameof(s3Client));
6074
_request = request ?? throw new ArgumentNullException(nameof(request));
6175
_config = config ?? throw new ArgumentNullException(nameof(config));
76+
_sharedHttpThrottler = sharedHttpThrottler; // Can be null for single file downloads
6277
}
6378

6479
/// <summary>

sdk/src/Services/S3/Custom/Transfer/Internal/MultipartDownloadManager.cs

Lines changed: 73 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ internal class MultipartDownloadManager : IDownloadManager
4444
private readonly DownloadManagerConfiguration _config;
4545
private readonly IPartDataHandler _dataHandler;
4646
private readonly SemaphoreSlim _httpConcurrencySlots;
47+
private readonly bool _ownsHttpThrottler;
4748
private readonly RequestEventHandler _requestEventHandler;
4849

4950
private Exception _downloadException;
@@ -79,23 +80,82 @@ private Logger Logger
7980
public Task DownloadCompletionTask => _downloadCompletionTask ?? Task.CompletedTask;
8081

8182
/// <summary>
82-
/// Initializes a new instance of the <see cref="MultipartDownloadManager"/> class.
83+
/// Initializes a new instance of the <see cref="MultipartDownloadManager"/> for single file downloads.
84+
/// This constructor creates and owns its own HTTP concurrency throttler based on the configuration.
8385
/// </summary>
84-
/// <param name="s3Client">The <see cref="IAmazonS3"/> client for making S3 requests.</param>
85-
/// <param name="request">The <see cref="BaseDownloadRequest"/> containing download parameters.</param>
86-
/// <param name="config">The <see cref="DownloadManagerConfiguration"/> with download settings.</param>
87-
/// <param name="dataHandler">The <see cref="IPartDataHandler"/> for processing downloaded parts.</param>
88-
/// <param name="requestEventHandler">Optional <see cref="RequestEventHandler"/> for user agent tracking.</param>
89-
/// <exception cref="ArgumentNullException">Thrown when any required parameter is null.</exception>
86+
/// <param name="s3Client">The <see cref="IAmazonS3"/> client used to make GetObject requests to S3.</param>
87+
/// <param name="request">The <see cref="BaseDownloadRequest"/> containing bucket, key, version, and download strategy configuration.</param>
88+
/// <param name="config">The <see cref="DownloadManagerConfiguration"/> specifying concurrency limits and part size settings.</param>
89+
/// <param name="dataHandler">The <see cref="IPartDataHandler"/> responsible for buffering and processing downloaded part data.</param>
90+
/// <param name="requestEventHandler">Optional request event handler for adding custom headers or tracking requests. May be null.</param>
91+
/// <exception cref="ArgumentNullException">
92+
/// Thrown when <paramref name="s3Client"/>, <paramref name="request"/>, <paramref name="config"/>, or <paramref name="dataHandler"/> is null.
93+
/// </exception>
94+
/// <remarks>
95+
/// This constructor is used for single file downloads where each download manages its own HTTP concurrency.
96+
/// The created <see cref="SemaphoreSlim"/> throttler will be disposed when this instance is disposed.
97+
/// For directory downloads with shared concurrency management, use the overload that accepts a shared throttler.
98+
/// </remarks>
99+
/// <seealso cref="DownloadManagerConfiguration"/>
100+
/// <seealso cref="IPartDataHandler"/>
101+
/// <seealso cref="MultipartDownloadType"/>
90102
public MultipartDownloadManager(IAmazonS3 s3Client, BaseDownloadRequest request, DownloadManagerConfiguration config, IPartDataHandler dataHandler, RequestEventHandler requestEventHandler = null)
103+
: this(s3Client, request, config, dataHandler, requestEventHandler, null)
104+
{
105+
}
106+
107+
/// <summary>
108+
/// Initializes a new instance of the <see cref="MultipartDownloadManager"/> for directory downloads or scenarios requiring shared concurrency control.
109+
/// This constructor allows using a shared HTTP concurrency throttler across multiple concurrent file downloads.
110+
/// </summary>
111+
/// <param name="s3Client">The <see cref="IAmazonS3"/> client used to make GetObject requests to S3.</param>
112+
/// <param name="request">The <see cref="BaseDownloadRequest"/> containing bucket, key, version, and download strategy configuration.</param>
113+
/// <param name="config">The <see cref="DownloadManagerConfiguration"/> specifying concurrency limits and part size settings.</param>
114+
/// <param name="dataHandler">The <see cref="IPartDataHandler"/> responsible for buffering and processing downloaded part data.</param>
115+
/// <param name="requestEventHandler">Optional request event handler for adding custom headers or tracking requests. May be null.</param>
116+
/// <param name="sharedHttpThrottler">
117+
/// Optional shared <see cref="SemaphoreSlim"/> for coordinating HTTP concurrency across multiple downloads.
118+
/// If null, a new throttler will be created and owned by this instance.
119+
/// If provided, the caller retains ownership and responsibility for disposal.
120+
/// </param>
121+
/// <exception cref="ArgumentNullException">
122+
/// Thrown when <paramref name="s3Client"/>, <paramref name="request"/>, <paramref name="config"/>, or <paramref name="dataHandler"/> is null.
123+
/// </exception>
124+
/// <remarks>
125+
/// <para>
126+
/// This constructor is typically used by directory download operations where multiple files are being downloaded
127+
/// concurrently and need to share a global HTTP concurrency limit.
128+
/// </para>
129+
/// <para>
130+
/// <strong>Resource Ownership:</strong>
131+
/// If <paramref name="sharedHttpThrottler"/> is provided, this instance does NOT take ownership and will NOT dispose it.
132+
/// If <paramref name="sharedHttpThrottler"/> is null, this instance creates and owns the throttler and will dispose it.
133+
/// </para>
134+
/// </remarks>
135+
/// <seealso cref="DownloadManagerConfiguration"/>
136+
/// <seealso cref="IPartDataHandler"/>
137+
/// <seealso cref="MultipartDownloadType"/>
138+
/// <seealso cref="DiscoverDownloadStrategyAsync"/>
139+
/// <seealso cref="StartDownloadsAsync"/>
140+
public MultipartDownloadManager(IAmazonS3 s3Client, BaseDownloadRequest request, DownloadManagerConfiguration config, IPartDataHandler dataHandler, RequestEventHandler requestEventHandler, SemaphoreSlim sharedHttpThrottler)
91141
{
92142
_s3Client = s3Client ?? throw new ArgumentNullException(nameof(s3Client));
93143
_request = request ?? throw new ArgumentNullException(nameof(request));
94144
_config = config ?? throw new ArgumentNullException(nameof(config));
95145
_dataHandler = dataHandler ?? throw new ArgumentNullException(nameof(dataHandler));
96146
_requestEventHandler = requestEventHandler;
97147

98-
_httpConcurrencySlots = new SemaphoreSlim(_config.ConcurrentServiceRequests);
148+
// Use shared throttler if provided, otherwise create our own
149+
if (sharedHttpThrottler != null)
150+
{
151+
_httpConcurrencySlots = sharedHttpThrottler;
152+
_ownsHttpThrottler = false; // Don't dispose - directory command owns it
153+
}
154+
else
155+
{
156+
_httpConcurrencySlots = new SemaphoreSlim(_config.ConcurrentServiceRequests);
157+
_ownsHttpThrottler = true; // We own it, so we dispose it
158+
}
99159
}
100160

101161
/// <inheritdoc/>
@@ -654,7 +714,11 @@ public void Dispose()
654714
{
655715
try
656716
{
657-
_httpConcurrencySlots?.Dispose();
717+
// Only dispose HTTP throttler if we own it
718+
if (_ownsHttpThrottler)
719+
{
720+
_httpConcurrencySlots?.Dispose();
721+
}
658722
_dataHandler?.Dispose();
659723
}
660724
catch (Exception)

sdk/src/Services/S3/Custom/Transfer/Internal/_async/MultipartDownloadCommand.async.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,14 @@ public override async Task<TransferUtilityDownloadResponse> ExecuteAsync(Cancell
5050
using (var dataHandler = new FilePartDataHandler(config))
5151
{
5252
// Create coordinator to manage the download process
53+
// Pass shared HTTP throttler to control concurrency across files
5354
using (var coordinator = new MultipartDownloadManager(
5455
_s3Client,
5556
_request,
5657
config,
5758
dataHandler,
58-
RequestEventHandler))
59+
RequestEventHandler,
60+
_sharedHttpThrottler))
5961
{
6062
long totalBytes = -1;
6163
try

sdk/src/Services/S3/Custom/Transfer/Internal/_bcl+netstandard/DownloadDirectoryCommand.cs

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,23 +63,43 @@ public override async Task<TransferUtilityDownloadDirectoryResponse> ExecuteAsyn
6363

6464
this._totalNumberOfFilesToDownload = objs.Count;
6565

66-
SemaphoreSlim asyncThrottler = null;
66+
// Two-level throttling architecture:
67+
// 1. File-level throttler: Controls how many files are downloaded concurrently
68+
// 2. HTTP-level throttler: Controls total HTTP requests across ALL file downloads
69+
//
70+
// Example with ConcurrentServiceRequests = 10:
71+
// - fileOperationThrottler = 10: Up to 10 files can download simultaneously
72+
// - sharedHttpRequestThrottler = 10: All 10 files share 10 total HTTP request slots
73+
// - Without HTTP throttler: Would result in 10 files × 10 parts = 100 concurrent HTTP requests
74+
// - With HTTP throttler: Enforces 10 total concurrent HTTP requests across all files
75+
//
76+
// This prevents resource exhaustion when downloading many large files with multipart downloads.
77+
SemaphoreSlim fileOperationThrottler = null;
78+
SemaphoreSlim sharedHttpRequestThrottler = null;
6779
CancellationTokenSource internalCts = null;
6880

6981
try
7082
{
71-
asyncThrottler = DownloadFilesConcurrently ?
83+
// File-level throttler: Controls concurrent file operations
84+
fileOperationThrottler = DownloadFilesConcurrently ?
7285
new SemaphoreSlim(this._config.ConcurrentServiceRequests) :
7386
new SemaphoreSlim(1);
7487

88+
// HTTP-level throttler: Shared across all downloads to control total HTTP concurrency
89+
// Only needed for multipart downloads where each file makes multiple HTTP requests
90+
if (this._useMultipartDownload)
91+
{
92+
sharedHttpRequestThrottler = new SemaphoreSlim(this._config.ConcurrentServiceRequests);
93+
}
94+
7595
internalCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
7696
var pendingTasks = new List<Task>();
7797
foreach (S3Object s3o in objs)
7898
{
7999
if (s3o.Key.EndsWith("/", StringComparison.Ordinal))
80100
continue;
81101

82-
await asyncThrottler.WaitAsync(cancellationToken)
102+
await fileOperationThrottler.WaitAsync(cancellationToken)
83103
.ConfigureAwait(continueOnCapturedContext: false);
84104

85105
cancellationToken.ThrowIfCancellationRequested();
@@ -105,9 +125,18 @@ await asyncThrottler.WaitAsync(cancellationToken)
105125
this._currentFile = s3o.Key.Substring(prefixLength);
106126

107127
var downloadRequest = ConstructTransferUtilityDownloadRequest(s3o, prefixLength);
108-
var command = new DownloadCommand(this._s3Client, downloadRequest);
128+
129+
BaseCommand<TransferUtilityDownloadResponse> command;
130+
if (this._useMultipartDownload)
131+
{
132+
command = new MultipartDownloadCommand(this._s3Client, downloadRequest, this._config, sharedHttpRequestThrottler);
133+
}
134+
else
135+
{
136+
command = new DownloadCommand(this._s3Client, downloadRequest);
137+
}
109138

110-
var task = ExecuteCommandAsync(command, internalCts, asyncThrottler);
139+
var task = ExecuteCommandAsync(command, internalCts, fileOperationThrottler);
111140

112141
pendingTasks.Add(task);
113142
}
@@ -122,7 +151,8 @@ await TaskHelpers.WhenAllOrFirstExceptionAsync(pendingTasks, cancellationToken)
122151
finally
123152
{
124153
internalCts.Dispose();
125-
asyncThrottler.Dispose();
154+
fileOperationThrottler.Dispose();
155+
sharedHttpRequestThrottler?.Dispose();
126156
}
127157
}
128158

0 commit comments

Comments
 (0)