11using System . Diagnostics . CodeAnalysis ;
2- using System . Runtime . ExceptionServices ;
32
43using Amazon . S3 ;
54using Amazon . S3 . Model ;
@@ -23,7 +22,7 @@ internal sealed class S3UploadStream : Stream
2322 private readonly FileStream _stream ;
2423 private readonly List < PartETag > _partETags ;
2524
26- private bool _disposed ;
25+ private volatile int _disposed ;
2726
2827 /// <inheritdoc />
2928 public override bool CanRead => false ;
@@ -163,13 +162,11 @@ protected override void Dispose(bool disposing)
163162 /// <inheritdoc />
164163 public override async ValueTask DisposeAsync ( )
165164 {
166- if ( _disposed )
165+ if ( Interlocked . Exchange ( ref _disposed , 1 ) != 0 )
167166 return ;
168167
169168 try
170169 {
171- _disposed = true ;
172-
173170 await UploadPartAsync ( CancellationToken . None ) . ConfigureAwait ( false ) ;
174171
175172 var request = new CompleteMultipartUploadRequest
@@ -184,16 +181,24 @@ await _client
184181 . CompleteMultipartUploadAsync ( request )
185182 . ConfigureAwait ( false ) ;
186183 }
187- catch ( Exception exception )
184+ catch
188185 {
189186 await AbortAsync ( CancellationToken . None ) . ConfigureAwait ( false ) ;
190- ExceptionDispatchInfo . Throw ( exception ) ;
187+ throw ;
191188 }
192189 finally
193190 {
194- await _stream
195- . DisposeAsync ( )
196- . ConfigureAwait ( false ) ;
191+ try
192+ {
193+ await _stream . DisposeAsync ( ) . ConfigureAwait ( false ) ;
194+ }
195+ catch
196+ {
197+ // Ignore:
198+ // Errors when disposing the temporary buffer are not significant here, because:
199+ // 1) If the upload succeeded, the job is done; a cleanup error can be ignored.
200+ // 2) If the upload failed, preserving the original exception is more important for us.
201+ }
197202 }
198203 }
199204
@@ -216,49 +221,35 @@ private void UploadPart()
216221 /// </returns>
217222 private async ValueTask UploadPartAsync ( CancellationToken cancellationToken )
218223 {
219- // Upload an empty part if nothing has been uploaded yet,
220- // since we must specify at least one part.
221-
222- if ( _stream . Length != 0 || _partETags . Count == 0 )
224+ _stream . Position = 0 ;
225+
226+ // https://docs.aws.amazon.com/AmazonS3/latest/userguide/qfacts.html
227+ // The maximum allowed part size is 5 GiB.
228+ // -----------------------------------------------------------------------------------
229+ // We don't need to worry about S3's 5 GiB part limit because:
230+ // 1. All Write/WriteAsync methods are inherently limited by Array.MaxLength (~2 GiB).
231+ // 2. The upload starts as soon as the buffer reaches MinPartSize (5 MiB).
232+ // Even if a single write matches Array.MaxLength, the data is
233+ // uploaded immediately, staying within AWS limits.
234+
235+ var request = new UploadPartRequest
223236 {
224- try
225- {
226- _stream . Position = 0 ;
227-
228- // https://docs.aws.amazon.com/AmazonS3/latest/userguide/qfacts.html
229- // The maximum allowed part size is 5 GiB.
230- // -----------------------------------------------------------------------------------
231- // We don't need to worry about S3's 5 GiB part limit because:
232- // 1. All Write/WriteAsync methods are inherently limited by Array.MaxLength (~2 GiB).
233- // 2. The upload starts as soon as the buffer reaches MinPartSize (5 MiB).
234- // Even if a single write matches Array.MaxLength, the data is
235- // uploaded immediately, staying within AWS limits.
236-
237- var request = new UploadPartRequest
238- {
239- BucketName = _bucketName ,
240- Key = _key ,
241- UploadId = _uploadId ,
242- PartNumber = _partETags . Count + 1 ,
243- InputStream = _stream ,
244- PartSize = _stream . Length
245- } ;
237+ BucketName = _bucketName ,
238+ Key = _key ,
239+ UploadId = _uploadId ,
240+ PartNumber = _partETags . Count + 1 ,
241+ InputStream = _stream ,
242+ PartSize = _stream . Length
243+ } ;
246244
247- var response = await _client
248- . UploadPartAsync ( request , cancellationToken )
249- . ConfigureAwait ( false ) ;
245+ var response = await _client
246+ . UploadPartAsync ( request , cancellationToken )
247+ . ConfigureAwait ( false ) ;
250248
251- _partETags . Add ( new PartETag ( response ) ) ;
249+ _partETags . Add ( new PartETag ( response ) ) ;
252250
253- _stream . Position = 0 ;
254- _stream . SetLength ( 0 ) ;
255- }
256- catch
257- {
258- await AbortAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
259- throw ;
260- }
261- }
251+ _stream . Position = 0 ;
252+ _stream . SetLength ( 0 ) ;
262253 }
263254
264255 /// <summary>
@@ -289,23 +280,31 @@ private void Abort()
289280 /// </remarks>
290281 private async ValueTask AbortAsync ( CancellationToken cancellationToken )
291282 {
292- var request = new AbortMultipartUploadRequest
293- {
294- BucketName = _bucketName ,
295- Key = _key ,
296- UploadId = _uploadId
297- } ;
298-
299- await _client
300- . AbortMultipartUploadAsync ( request , cancellationToken )
301- . ConfigureAwait ( false ) ;
283+ if ( Interlocked . Exchange ( ref _disposed , 1 ) != 0 )
284+ return ;
302285
303- _disposed = true ;
286+ try
287+ {
288+ await using ( _stream . ConfigureAwait ( false ) )
289+ {
290+ var request = new AbortMultipartUploadRequest
291+ {
292+ BucketName = _bucketName ,
293+ Key = _key ,
294+ UploadId = _uploadId
295+ } ;
304296
305- // Prevent subsequent writes to the stream.
306- await _stream
307- . DisposeAsync ( )
308- . ConfigureAwait ( false ) ;
297+ await _client
298+ . AbortMultipartUploadAsync ( request , cancellationToken )
299+ . ConfigureAwait ( false ) ;
300+ }
301+ }
302+ catch
303+ {
304+ // IGNORE:
305+ // Suppressing the exception during abort to preserve
306+ // the original exception that triggered the abort.
307+ }
309308 }
310309
311310 /// <summary>
0 commit comments