|
14 | 14 | import logging |
15 | 15 | import threading |
16 | 16 |
|
| 17 | +from botocore.exceptions import ClientError |
17 | 18 | from s3transfer.compat import seekable |
18 | | -from s3transfer.exceptions import RetriesExceededError |
| 19 | +from s3transfer.exceptions import RetriesExceededError, S3DownloadFailedError |
19 | 20 | from s3transfer.futures import IN_MEMORY_DOWNLOAD_TAG |
20 | 21 | from s3transfer.tasks import SubmissionTask, Task |
21 | 22 | from s3transfer.utils import ( |
@@ -346,17 +347,23 @@ def _submit( |
346 | 347 | :param bandwidth_limiter: The bandwidth limiter to use when |
347 | 348 | downloading streams |
348 | 349 | """ |
349 | | - if transfer_future.meta.size is None: |
350 | | - # If a size was not provided figure out the size for the |
351 | | - # user. |
| 350 | + if ( |
| 351 | + transfer_future.meta.size is None |
| 352 | + or transfer_future.meta.etag is None |
| 353 | + ): |
352 | 354 | response = client.head_object( |
353 | 355 | Bucket=transfer_future.meta.call_args.bucket, |
354 | 356 | Key=transfer_future.meta.call_args.key, |
355 | 357 | **transfer_future.meta.call_args.extra_args, |
356 | 358 | ) |
| 359 | + # If a size was not provided figure out the size for the |
| 360 | + # user. |
357 | 361 | transfer_future.meta.provide_transfer_size( |
358 | 362 | response['ContentLength'] |
359 | 363 | ) |
| 364 | + # Provide an etag to ensure a stored object is not modified |
| 365 | + # during a multipart download. |
| 366 | + transfer_future.meta.provide_object_etag(response.get('ETag')) |
360 | 367 |
|
361 | 368 | download_output_manager = self._get_download_output_manager_cls( |
362 | 369 | transfer_future, osutil |
@@ -479,9 +486,12 @@ def _submit_ranged_download_request( |
479 | 486 | part_size, i, num_parts |
480 | 487 | ) |
481 | 488 |
|
482 | | - # Inject the Range parameter to the parameters to be passed in |
483 | | - # as extra args |
484 | | - extra_args = {'Range': range_parameter} |
| 489 | + # Inject extra parameters to be passed in as extra args |
| 490 | + extra_args = { |
| 491 | + 'Range': range_parameter, |
| 492 | + } |
| 493 | + if transfer_future.meta.etag is not None: |
| 494 | + extra_args['IfMatch'] = transfer_future.meta.etag |
485 | 495 | extra_args.update(call_args.extra_args) |
486 | 496 | finalize_download_invoker.increment() |
487 | 497 | # Submit the ranged downloads |
@@ -593,6 +603,15 @@ def _main( |
593 | 603 | else: |
594 | 604 | return |
595 | 605 | return |
| 606 | + except ClientError as e: |
| 607 | + error_code = e.response.get('Error', {}).get('Code') |
| 608 | + if error_code == "PreconditionFailed": |
| 609 | + raise S3DownloadFailedError( |
| 610 | + f'Contents of stored object "{key}" in bucket ' |
| 611 | + f'"{bucket}" did not match expected ETag.' |
| 612 | + ) |
| 613 | + else: |
| 614 | + raise |
596 | 615 | except S3_RETRYABLE_DOWNLOAD_ERRORS as e: |
597 | 616 | logger.debug( |
598 | 617 | "Retrying exception caught (%s), " |
|
0 commit comments