Skip to content

Commit 232883a

Browse files
Add DownloadDirectoryWithResponse (#4141)
1 parent 18ad664 commit 232883a

16 files changed

+2146
-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
@@ -42,16 +42,24 @@ internal partial class DownloadDirectoryCommand : BaseCommand<TransferUtilityDow
4242
private readonly IAmazonS3 _s3Client;
4343
private readonly TransferUtilityDownloadDirectoryRequest _request;
4444
private readonly bool _skipEncryptionInstructionFiles;
45+
private readonly bool _useMultipartDownload;
4546
int _totalNumberOfFilesToDownload;
4647
int _numberOfFilesDownloaded;
4748
long _totalBytes;
4849
long _transferredBytes;
4950
string _currentFile;
5051

5152
internal DownloadDirectoryCommand(IAmazonS3 s3Client, TransferUtilityDownloadDirectoryRequest request)
53+
: this(s3Client, request, useMultipartDownload: false)
54+
{
55+
}
56+
57+
internal DownloadDirectoryCommand(IAmazonS3 s3Client, TransferUtilityDownloadDirectoryRequest request, bool useMultipartDownload)
5258
{
5359
if (s3Client == null)
54-
throw new ArgumentNullException("s3Client");
60+
throw new ArgumentNullException(nameof(s3Client));
61+
if (request == null)
62+
throw new ArgumentNullException(nameof(request));
5563

5664
this._s3Client = s3Client;
5765
this._request = request;
@@ -60,6 +68,13 @@ internal DownloadDirectoryCommand(IAmazonS3 s3Client, TransferUtilityDownloadDir
6068
request.FailurePolicy == FailurePolicy.AbortOnFailure
6169
? new AbortOnFailurePolicy()
6270
: new ContinueOnFailurePolicy(_errors);
71+
this._useMultipartDownload = useMultipartDownload;
72+
}
73+
74+
internal DownloadDirectoryCommand(IAmazonS3 s3Client, TransferUtilityDownloadDirectoryRequest request, TransferUtilityConfig config, bool useMultipartDownload)
75+
: this(s3Client, request, useMultipartDownload)
76+
{
77+
this._config = config;
6378
}
6479

6580
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: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,23 +66,43 @@ public override async Task<TransferUtilityDownloadDirectoryResponse> ExecuteAsyn
6666

6767
this._totalNumberOfFilesToDownload = objs.Count;
6868

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

7284
try
7385
{
74-
asyncThrottler = DownloadFilesConcurrently ?
86+
// File-level throttler: Controls concurrent file operations
87+
fileOperationThrottler = DownloadFilesConcurrently ?
7588
new SemaphoreSlim(this._config.ConcurrentServiceRequests) :
7689
new SemaphoreSlim(1);
7790

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

85-
await asyncThrottler.WaitAsync(cancellationToken)
105+
await fileOperationThrottler.WaitAsync(cancellationToken)
86106
.ConfigureAwait(continueOnCapturedContext: false);
87107

88108
try
@@ -137,7 +157,15 @@ await asyncThrottler.WaitAsync(cancellationToken)
137157

138158
var task = _failurePolicy.ExecuteAsync(
139159
async () => {
140-
var command = new DownloadCommand(this._s3Client, downloadRequest);
160+
BaseCommand<TransferUtilityDownloadResponse> command;
161+
if (this._useMultipartDownload)
162+
{
163+
command = new MultipartDownloadCommand(this._s3Client, downloadRequest, this._config, sharedHttpRequestThrottler);
164+
}
165+
else
166+
{
167+
command = new DownloadCommand(this._s3Client, downloadRequest);
168+
}
141169
await command.ExecuteAsync(internalCts.Token)
142170
.ConfigureAwait(false);
143171
},
@@ -149,7 +177,7 @@ await command.ExecuteAsync(internalCts.Token)
149177
}
150178
finally
151179
{
152-
asyncThrottler.Release();
180+
fileOperationThrottler.Release();
153181
}
154182
}
155183
await TaskHelpers.WhenAllOrFirstExceptionAsync(pendingTasks, cancellationToken)
@@ -170,7 +198,8 @@ await TaskHelpers.WhenAllOrFirstExceptionAsync(pendingTasks, cancellationToken)
170198
finally
171199
{
172200
internalCts.Dispose();
173-
asyncThrottler.Dispose();
201+
fileOperationThrottler.Dispose();
202+
sharedHttpRequestThrottler?.Dispose();
174203
}
175204
}
176205

0 commit comments

Comments
 (0)