Skip to content

Commit ee59a30

Browse files
committed
Add DownloadInitiated, Failed and Completed events
stack-info: PR: #4079, branch: GarrettBeatty/stacked/10
1 parent a3dfaec commit ee59a30

File tree

6 files changed

+515
-2
lines changed

6 files changed

+515
-2
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+
"Added UploadInitiatedEvent, UploadCompletedEvent, and UploadFailedEvent for downloads."
8+
]
9+
}
10+
]
11+
}

sdk/src/Services/S3/Custom/Model/GetObjectResponse.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1042,5 +1042,10 @@ internal WriteObjectProgressArgs(string bucketName, string key, string filePath,
10421042
/// True if writing is complete
10431043
/// </summary>
10441044
public bool IsCompleted { get; private set; }
1045+
1046+
/// <summary>
1047+
/// The original TransferUtilityDownloadRequest created by the user.
1048+
/// </summary>
1049+
public Transfer.TransferUtilityDownloadRequest Request { get; internal set; }
10451050
}
10461051
}

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,34 @@ static Logger Logger
6262

6363
IAmazonS3 _s3Client;
6464
TransferUtilityDownloadRequest _request;
65+
long _totalTransferredBytes;
66+
67+
#region Event Firing Methods
68+
69+
private void FireTransferInitiatedEvent()
70+
{
71+
var transferInitiatedEventArgs = new DownloadInitiatedEventArgs(_request, _request.FilePath);
72+
_request.OnRaiseTransferInitiatedEvent(transferInitiatedEventArgs);
73+
}
74+
75+
private void FireTransferCompletedEvent(TransferUtilityDownloadResponse response, string filePath, long transferredBytes, long totalBytes)
76+
{
77+
var transferCompletedEventArgs = new DownloadCompletedEventArgs(
78+
_request,
79+
response,
80+
filePath,
81+
transferredBytes,
82+
totalBytes);
83+
_request.OnRaiseTransferCompletedEvent(transferCompletedEventArgs);
84+
}
85+
86+
private void FireTransferFailedEvent(string filePath, long transferredBytes, long totalBytes = -1)
87+
{
88+
var eventArgs = new DownloadFailedEventArgs(this._request, filePath, transferredBytes, totalBytes);
89+
this._request.OnRaiseTransferFailedEvent(eventArgs);
90+
}
91+
92+
#endregion
6593

6694
internal DownloadCommand(IAmazonS3 s3Client, TransferUtilityDownloadRequest request)
6795
{
@@ -89,6 +117,12 @@ private void ValidateRequest()
89117

90118
void OnWriteObjectProgressEvent(object sender, WriteObjectProgressArgs e)
91119
{
120+
// Keep track of the total transferred bytes so that we can also return this value in case of failure
121+
Interlocked.Add(ref _totalTransferredBytes, e.IncrementTransferred);
122+
123+
// Set the Request property to enable access to the original download request
124+
e.Request = this._request;
125+
92126
this._request.OnRaiseProgressEvent(e);
93127
}
94128

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

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,18 @@ internal partial class DownloadCommand : BaseCommand<TransferUtilityDownloadResp
3333
public override async Task<TransferUtilityDownloadResponse> ExecuteAsync(CancellationToken cancellationToken)
3434
{
3535
ValidateRequest();
36+
37+
FireTransferInitiatedEvent();
38+
3639
GetObjectRequest getRequest = ConvertToGetObjectRequest(this._request);
3740

3841
var maxRetries = _s3Client.Config.MaxErrorRetry;
3942
var retries = 0;
4043
bool shouldRetry = false;
4144
string mostRecentETag = null;
45+
GetObjectResponse lastSuccessfulResponse = null;
46+
long? totalBytesFromResponse = null; // Track total bytes once we have response headers
47+
4248
do
4349
{
4450
shouldRetry = false;
@@ -54,6 +60,9 @@ public override async Task<TransferUtilityDownloadResponse> ExecuteAsync(Cancell
5460
using (var response = await this._s3Client.GetObjectAsync(getRequest, cancellationToken)
5561
.ConfigureAwait(continueOnCapturedContext: false))
5662
{
63+
// Capture total bytes from response headers as soon as we get them
64+
totalBytesFromResponse = response.ContentLength;
65+
5766
if (!string.IsNullOrEmpty(mostRecentETag) && !string.Equals(mostRecentETag, response.ETag))
5867
{
5968
//if the eTag changed, we need to retry from the start of the file
@@ -101,6 +110,9 @@ await response.WriteResponseStreamToFileAsync(this._request.FilePath, false, can
101110
await response.WriteResponseStreamToFileAsync(this._request.FilePath, true, cancellationToken)
102111
.ConfigureAwait(continueOnCapturedContext: false);
103112
}
113+
114+
// Store the successful response for the completion event
115+
lastSuccessfulResponse = response;
104116
}
105117
}
106118
catch (Exception exception)
@@ -109,6 +121,9 @@ await response.WriteResponseStreamToFileAsync(this._request.FilePath, true, canc
109121
shouldRetry = HandleExceptionForHttpClient(exception, retries, maxRetries);
110122
if (!shouldRetry)
111123
{
124+
// Pass total bytes if we have them from response headers, otherwise -1 for unknown
125+
FireTransferFailedEvent(this._request.FilePath, Interlocked.Read(ref _totalTransferredBytes), totalBytesFromResponse ?? -1);
126+
112127
if (exception is IOException)
113128
{
114129
throw;
@@ -131,8 +146,14 @@ await response.WriteResponseStreamToFileAsync(this._request.FilePath, true, canc
131146
WaitBeforeRetry(retries);
132147
} while (shouldRetry);
133148

134-
// TODO map and return response
135-
return new TransferUtilityDownloadResponse();
149+
// Map the response once after successful download
150+
var mappedResponse = ResponseMapper.MapGetObjectResponse(lastSuccessfulResponse);
151+
152+
long totalTransferredBytes = Interlocked.Read(ref _totalTransferredBytes);
153+
long totalBytes = lastSuccessfulResponse.ContentLength;
154+
FireTransferCompletedEvent(mappedResponse, this._request.FilePath, totalTransferredBytes, totalBytes);
155+
156+
return mappedResponse;
136157
}
137158

138159
private static bool HandleExceptionForHttpClient(Exception exception, int retries, int maxRetries)

sdk/src/Services/S3/Custom/Transfer/TransferUtilityDownloadRequest.cs

Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,5 +90,254 @@ internal void OnRaiseProgressEvent(WriteObjectProgressArgs progressArgs)
9090
{
9191
AWSSDKUtils.InvokeInBackground(WriteObjectProgressEvent, progressArgs, this);
9292
}
93+
94+
/// <summary>
95+
/// The event for DownloadInitiatedEvent notifications. All
96+
/// subscribers will be notified when a download transfer operation
97+
/// starts.
98+
/// <para>
99+
/// The DownloadInitiatedEvent is fired exactly once when
100+
/// a download transfer operation begins. The delegates attached to the event
101+
/// will be passed information about the download request and
102+
/// file path, but no progress information.
103+
/// </para>
104+
/// </summary>
105+
/// <remarks>
106+
/// Subscribe to this event if you want to receive
107+
/// DownloadInitiatedEvent notifications. Here is how:<br />
108+
/// 1. Define a method with a signature similar to this one:
109+
/// <code>
110+
/// private void downloadStarted(object sender, DownloadInitiatedEventArgs args)
111+
/// {
112+
/// Console.WriteLine($"Download started: {args.FilePath}");
113+
/// Console.WriteLine($"Bucket: {args.Request.BucketName}");
114+
/// Console.WriteLine($"Key: {args.Request.Key}");
115+
/// }
116+
/// </code>
117+
/// 2. Add this method to the DownloadInitiatedEvent delegate's invocation list
118+
/// <code>
119+
/// TransferUtilityDownloadRequest request = new TransferUtilityDownloadRequest();
120+
/// request.DownloadInitiatedEvent += downloadStarted;
121+
/// </code>
122+
/// </remarks>
123+
public event EventHandler<DownloadInitiatedEventArgs> DownloadInitiatedEvent;
124+
125+
/// <summary>
126+
/// The event for DownloadCompletedEvent notifications. All
127+
/// subscribers will be notified when a download transfer operation
128+
/// completes successfully.
129+
/// <para>
130+
/// The DownloadCompletedEvent is fired exactly once when
131+
/// a download transfer operation completes successfully. The delegates attached to the event
132+
/// will be passed information about the completed download including
133+
/// the final response from S3 with ETag, VersionId, and other metadata.
134+
/// </para>
135+
/// </summary>
136+
/// <remarks>
137+
/// Subscribe to this event if you want to receive
138+
/// DownloadCompletedEvent notifications. Here is how:<br />
139+
/// 1. Define a method with a signature similar to this one:
140+
/// <code>
141+
/// private void downloadCompleted(object sender, DownloadCompletedEventArgs args)
142+
/// {
143+
/// Console.WriteLine($"Download completed: {args.FilePath}");
144+
/// Console.WriteLine($"Transferred: {args.TransferredBytes} bytes");
145+
/// Console.WriteLine($"ETag: {args.Response.ETag}");
146+
/// Console.WriteLine($"S3 Key: {args.Response.Key}");
147+
/// Console.WriteLine($"Version ID: {args.Response.VersionId}");
148+
/// }
149+
/// </code>
150+
/// 2. Add this method to the DownloadCompletedEvent delegate's invocation list
151+
/// <code>
152+
/// TransferUtilityDownloadRequest request = new TransferUtilityDownloadRequest();
153+
/// request.DownloadCompletedEvent += downloadCompleted;
154+
/// </code>
155+
/// </remarks>
156+
public event EventHandler<DownloadCompletedEventArgs> DownloadCompletedEvent;
157+
158+
/// <summary>
159+
/// The event for DownloadFailedEvent notifications. All
160+
/// subscribers will be notified when a download transfer operation
161+
/// fails.
162+
/// <para>
163+
/// The DownloadFailedEvent is fired exactly once when
164+
/// a download transfer operation fails. The delegates attached to the event
165+
/// will be passed information about the failed download including
166+
/// partial progress information, but no response data since the download failed.
167+
/// </para>
168+
/// </summary>
169+
/// <remarks>
170+
/// Subscribe to this event if you want to receive
171+
/// DownloadFailedEvent notifications. Here is how:<br />
172+
/// 1. Define a method with a signature similar to this one:
173+
/// <code>
174+
/// private void downloadFailed(object sender, DownloadFailedEventArgs args)
175+
/// {
176+
/// Console.WriteLine($"Download failed: {args.FilePath}");
177+
/// Console.WriteLine($"Partial progress: {args.TransferredBytes} bytes");
178+
/// Console.WriteLine($"Bucket: {args.Request.BucketName}");
179+
/// Console.WriteLine($"Key: {args.Request.Key}");
180+
/// }
181+
/// </code>
182+
/// 2. Add this method to the DownloadFailedEvent delegate's invocation list
183+
/// <code>
184+
/// TransferUtilityDownloadRequest request = new TransferUtilityDownloadRequest();
185+
/// request.DownloadFailedEvent += downloadFailed;
186+
/// </code>
187+
/// </remarks>
188+
public event EventHandler<DownloadFailedEventArgs> DownloadFailedEvent;
189+
190+
/// <summary>
191+
/// Causes the DownloadInitiatedEvent event to be fired.
192+
/// </summary>
193+
/// <param name="args">DownloadInitiatedEventArgs args</param>
194+
internal void OnRaiseTransferInitiatedEvent(DownloadInitiatedEventArgs args)
195+
{
196+
AWSSDKUtils.InvokeInBackground(DownloadInitiatedEvent, args, this);
197+
}
198+
199+
/// <summary>
200+
/// Causes the DownloadCompletedEvent event to be fired.
201+
/// </summary>
202+
/// <param name="args">DownloadCompletedEventArgs args</param>
203+
internal void OnRaiseTransferCompletedEvent(DownloadCompletedEventArgs args)
204+
{
205+
AWSSDKUtils.InvokeInBackground(DownloadCompletedEvent, args, this);
206+
}
207+
208+
/// <summary>
209+
/// Causes the DownloadFailedEvent event to be fired.
210+
/// </summary>
211+
/// <param name="args">DownloadFailedEventArgs args</param>
212+
internal void OnRaiseTransferFailedEvent(DownloadFailedEventArgs args)
213+
{
214+
AWSSDKUtils.InvokeInBackground(DownloadFailedEvent, args, this);
215+
}
216+
}
217+
218+
/// <summary>
219+
/// Encapsulates the information needed when a download transfer operation is initiated.
220+
/// Provides access to the original request without progress or total byte information.
221+
/// </summary>
222+
public class DownloadInitiatedEventArgs : EventArgs
223+
{
224+
/// <summary>
225+
/// Initializes a new instance of the DownloadInitiatedEventArgs class.
226+
/// </summary>
227+
/// <param name="request">The original TransferUtilityDownloadRequest created by the user</param>
228+
/// <param name="filePath">The file being downloaded</param>
229+
internal DownloadInitiatedEventArgs(TransferUtilityDownloadRequest request, string filePath)
230+
{
231+
Request = request;
232+
FilePath = filePath;
233+
}
234+
235+
/// <summary>
236+
/// The original TransferUtilityDownloadRequest created by the user.
237+
/// Contains all the download parameters and configuration.
238+
/// </summary>
239+
public TransferUtilityDownloadRequest Request { get; private set; }
240+
241+
/// <summary>
242+
/// Gets the file being downloaded.
243+
/// </summary>
244+
public string FilePath { get; private set; }
245+
}
246+
247+
/// <summary>
248+
/// Encapsulates the information needed when a download transfer operation completes successfully.
249+
/// Provides access to the original request, final response, and completion details.
250+
/// </summary>
251+
public class DownloadCompletedEventArgs : EventArgs
252+
{
253+
/// <summary>
254+
/// Initializes a new instance of the DownloadCompletedEventArgs class.
255+
/// </summary>
256+
/// <param name="request">The original TransferUtilityDownloadRequest created by the user</param>
257+
/// <param name="response">The unified response from Transfer Utility</param>
258+
/// <param name="filePath">The file being downloaded</param>
259+
/// <param name="transferredBytes">The total number of bytes transferred</param>
260+
/// <param name="totalBytes">The total number of bytes for the complete file</param>
261+
internal DownloadCompletedEventArgs(TransferUtilityDownloadRequest request, TransferUtilityDownloadResponse response, string filePath, long transferredBytes, long totalBytes)
262+
{
263+
Request = request;
264+
Response = response;
265+
FilePath = filePath;
266+
TransferredBytes = transferredBytes;
267+
TotalBytes = totalBytes;
268+
}
269+
270+
/// <summary>
271+
/// The original TransferUtilityDownloadRequest created by the user.
272+
/// Contains all the download parameters and configuration.
273+
/// </summary>
274+
public TransferUtilityDownloadRequest Request { get; private set; }
275+
276+
/// <summary>
277+
/// The unified response from Transfer Utility after successful download completion.
278+
/// Contains mapped fields from GetObjectResponse.
279+
/// </summary>
280+
public TransferUtilityDownloadResponse Response { get; private set; }
281+
282+
/// <summary>
283+
/// Gets the file being downloaded.
284+
/// </summary>
285+
public string FilePath { get; private set; }
286+
287+
/// <summary>
288+
/// Gets the total number of bytes that were successfully transferred.
289+
/// </summary>
290+
public long TransferredBytes { get; private set; }
291+
292+
/// <summary>
293+
/// Gets the total number of bytes for the complete file.
294+
/// </summary>
295+
public long TotalBytes { get; private set; }
296+
}
297+
298+
/// <summary>
299+
/// Encapsulates the information needed when a download transfer operation fails.
300+
/// Provides access to the original request and partial progress information.
301+
/// </summary>
302+
public class DownloadFailedEventArgs : EventArgs
303+
{
304+
/// <summary>
305+
/// Initializes a new instance of the DownloadFailedEventArgs class.
306+
/// </summary>
307+
/// <param name="request">The original TransferUtilityDownloadRequest created by the user</param>
308+
/// <param name="filePath">The file being downloaded</param>
309+
/// <param name="transferredBytes">The number of bytes transferred before failure</param>
310+
/// <param name="totalBytes">The total number of bytes for the complete file, or -1 if unknown</param>
311+
internal DownloadFailedEventArgs(TransferUtilityDownloadRequest request, string filePath, long transferredBytes, long totalBytes)
312+
{
313+
Request = request;
314+
FilePath = filePath;
315+
TransferredBytes = transferredBytes;
316+
TotalBytes = totalBytes;
317+
}
318+
319+
/// <summary>
320+
/// The original TransferUtilityDownloadRequest created by the user.
321+
/// Contains all the download parameters and configuration.
322+
/// </summary>
323+
public TransferUtilityDownloadRequest Request { get; private set; }
324+
325+
/// <summary>
326+
/// Gets the file being downloaded.
327+
/// </summary>
328+
public string FilePath { get; private set; }
329+
330+
/// <summary>
331+
/// Gets the number of bytes that were transferred before the failure occurred.
332+
/// </summary>
333+
public long TransferredBytes { get; private set; }
334+
335+
/// <summary>
336+
/// Gets the total number of bytes for the complete file, or -1 if unknown.
337+
/// This will be -1 for failures that occur before receiving the GetObjectResponse
338+
/// (e.g., authentication errors, non-existent objects), and will contain the actual
339+
/// file size for failures that occur after receiving response headers (e.g., disk full).
340+
/// </summary>
341+
public long TotalBytes { get; private set; }
93342
}
94343
}

0 commit comments

Comments
 (0)