Skip to content

Commit 0bdf38a

Browse files
authored
Merge pull request #344 from boto/revert-343-check-etag
Revert "Validate multipart downloads using object ETags"
2 parents 8fdaa34 + efe9369 commit 0bdf38a

6 files changed

Lines changed: 17 additions & 140 deletions

File tree

.changes/next-release/bugfix-download-32668.json

Lines changed: 0 additions & 5 deletions
This file was deleted.

s3transfer/download.py

Lines changed: 9 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,8 @@
1414
import logging
1515
import threading
1616

17-
from botocore.exceptions import ClientError
18-
1917
from s3transfer.compat import seekable
20-
from s3transfer.exceptions import RetriesExceededError, S3DownloadFailedError
18+
from s3transfer.exceptions import RetriesExceededError
2119
from s3transfer.futures import IN_MEMORY_DOWNLOAD_TAG
2220
from s3transfer.tasks import SubmissionTask, Task
2321
from s3transfer.utils import (
@@ -348,18 +346,14 @@ def _submit(
348346
:param bandwidth_limiter: The bandwidth limiter to use when
349347
downloading streams
350348
"""
351-
response = client.head_object(
352-
Bucket=transfer_future.meta.call_args.bucket,
353-
Key=transfer_future.meta.call_args.key,
354-
**transfer_future.meta.call_args.extra_args,
355-
)
356-
# Provide an etag to ensure a stored object is not modified
357-
# during a multipart download.
358-
transfer_future.meta.provide_object_etag(response.get('ETag'))
359-
360349
if transfer_future.meta.size is None:
361350
# If a size was not provided figure out the size for the
362351
# user.
352+
response = client.head_object(
353+
Bucket=transfer_future.meta.call_args.bucket,
354+
Key=transfer_future.meta.call_args.key,
355+
**transfer_future.meta.call_args.extra_args,
356+
)
363357
transfer_future.meta.provide_transfer_size(
364358
response['ContentLength']
365359
)
@@ -485,12 +479,9 @@ def _submit_ranged_download_request(
485479
part_size, i, num_parts
486480
)
487481

488-
# Inject extra parameters to be passed in as extra args
489-
extra_args = {
490-
'Range': range_parameter,
491-
}
492-
if transfer_future.meta.etag is not None:
493-
extra_args['IfMatch'] = transfer_future.meta.etag
482+
# Inject the Range parameter to the parameters to be passed in
483+
# as extra args
484+
extra_args = {'Range': range_parameter}
494485
extra_args.update(call_args.extra_args)
495486
finalize_download_invoker.increment()
496487
# Submit the ranged downloads
@@ -602,15 +593,6 @@ def _main(
602593
else:
603594
return
604595
return
605-
except ClientError as e:
606-
error_code = e.response.get('Error', {}).get('Code')
607-
if error_code == "PreconditionFailed":
608-
raise S3DownloadFailedError(
609-
f'Contents of stored object "{key}" in bucket '
610-
f'"{bucket}" did not match expected ETag.'
611-
)
612-
else:
613-
raise
614596
except S3_RETRYABLE_DOWNLOAD_ERRORS as e:
615597
logger.debug(
616598
"Retrying exception caught (%s), "

s3transfer/exceptions.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,6 @@ class S3UploadFailedError(Exception):
2323
pass
2424

2525

26-
class S3DownloadFailedError(Exception):
27-
pass
28-
29-
3026
class InvalidSubscriberMethodError(Exception):
3127
pass
3228

s3transfer/futures.py

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,6 @@ def __init__(self, call_args=None, transfer_id=None):
134134
self._transfer_id = transfer_id
135135
self._size = None
136136
self._user_context = {}
137-
self._etag = None
138137

139138
@property
140139
def call_args(self):
@@ -156,11 +155,6 @@ def user_context(self):
156155
"""A dictionary that requesters can store data in"""
157156
return self._user_context
158157

159-
@property
160-
def etag(self):
161-
"""The etag of the stored object for validating multipart downloads"""
162-
return self._etag
163-
164158
def provide_transfer_size(self, size):
165159
"""A method to provide the size of a transfer request
166160
@@ -170,15 +164,6 @@ def provide_transfer_size(self, size):
170164
"""
171165
self._size = size
172166

173-
def provide_object_etag(self, etag):
174-
"""A method to provide the etag of a transfer request
175-
176-
By providing this value, the TransferManager will validate
177-
multipart downloads by supplying an IfMatch parameter with
178-
the etag as the value to GetObject requests.
179-
"""
180-
self._etag = etag
181-
182167

183168
class TransferCoordinator:
184169
"""A helper class for managing TransferFuture"""

tests/functional/test_download.py

Lines changed: 6 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from botocore.exceptions import ClientError
2222

2323
from s3transfer.compat import SOCKET_ERROR
24-
from s3transfer.exceptions import RetriesExceededError, S3DownloadFailedError
24+
from s3transfer.exceptions import RetriesExceededError
2525
from s3transfer.manager import TransferConfig, TransferManager
2626
from tests import (
2727
BaseGeneralInterfaceTest,
@@ -48,7 +48,6 @@ def setUp(self):
4848
# Initialize some default arguments
4949
self.bucket = 'mybucket'
5050
self.key = 'mykey'
51-
self.etag = 'myetag'
5251
self.extra_args = {}
5352
self.subscribers = []
5453

@@ -85,10 +84,7 @@ def create_stubbed_responses(self):
8584
return [
8685
{
8786
'method': 'head_object',
88-
'service_response': {
89-
'ContentLength': len(self.content),
90-
'ETag': self.etag,
91-
},
87+
'service_response': {'ContentLength': len(self.content)},
9288
},
9389
{
9490
'method': 'get_object',
@@ -295,7 +291,6 @@ def test_retry_rewinds_callbacks(self):
295291
self.assertEqual(-3, progress_byte_amts[1])
296292

297293
def test_can_provide_file_size(self):
298-
self.add_head_object_response()
299294
self.add_successful_get_object_responses()
300295

301296
call_kwargs = self.create_call_kwargs()
@@ -304,6 +299,8 @@ def test_can_provide_file_size(self):
304299
future = self.manager.download(**call_kwargs)
305300
future.result()
306301

302+
# The HeadObject should have not happened and should have been able
303+
# to successfully download the file.
307304
self.stubber.assert_no_pending_responses()
308305
with open(self.filename, 'rb') as f:
309306
self.assertEqual(self.content, f.read())
@@ -472,10 +469,7 @@ def create_stubbed_responses(self):
472469
return [
473470
{
474471
'method': 'head_object',
475-
'service_response': {
476-
'ContentLength': len(self.content),
477-
'ETag': self.etag,
478-
},
472+
'service_response': {'ContentLength': len(self.content)},
479473
},
480474
{
481475
'method': 'get_object',
@@ -508,7 +502,7 @@ def test_download(self):
508502
expected_ranges = ['bytes=0-3', 'bytes=4-7', 'bytes=8-']
509503
self.add_head_object_response(expected_params)
510504
self.add_successful_get_object_responses(
511-
{**expected_params, 'IfMatch': self.etag}, expected_ranges
505+
expected_params, expected_ranges
512506
)
513507

514508
future = self.manager.download(
@@ -529,76 +523,6 @@ def test_download_with_checksum_enabled(self):
529523
}
530524
expected_ranges = ['bytes=0-3', 'bytes=4-7', 'bytes=8-']
531525
self.add_head_object_response(expected_params)
532-
self.add_successful_get_object_responses(
533-
{**expected_params, 'IfMatch': self.etag}, expected_ranges
534-
)
535-
536-
future = self.manager.download(
537-
self.bucket, self.key, self.filename, self.extra_args
538-
)
539-
future.result()
540-
541-
# Ensure that the contents are correct
542-
with open(self.filename, 'rb') as f:
543-
self.assertEqual(self.content, f.read())
544-
545-
def test_download_raises_if_etag_validation_fails(self):
546-
expected_params = {
547-
'Bucket': self.bucket,
548-
'Key': self.key,
549-
}
550-
expected_ranges = ['bytes=0-3', 'bytes=4-7']
551-
self.add_head_object_response(expected_params)
552-
553-
# Add successful GetObject responses for the first 2 requests.
554-
for i, stubbed_response in enumerate(
555-
self.create_stubbed_responses()[1:3]
556-
):
557-
stubbed_response['expected_params'] = copy.deepcopy(
558-
{**expected_params, 'IfMatch': self.etag}
559-
)
560-
stubbed_response['expected_params']['Range'] = expected_ranges[i]
561-
self.stubber.add_response(**stubbed_response)
562-
563-
# Simulate ETag validation failure by adding a
564-
# client error for the last GetObject request.
565-
self.stubber.add_client_error(
566-
method='get_object',
567-
service_error_code='PreconditionFailed',
568-
service_message=(
569-
'At least one of the pre-conditions you specified did not hold'
570-
),
571-
http_status_code=412,
572-
)
573-
574-
future = self.manager.download(
575-
self.bucket, self.key, self.filename, self.extra_args
576-
)
577-
with self.assertRaises(S3DownloadFailedError) as e:
578-
future.result()
579-
self.assertIn('did not match expected ETag', str(e.exception))
580-
581-
# Ensure no data is written to disk.
582-
self.assertFalse(os.path.exists(self.filename))
583-
584-
def test_download_without_etag(self):
585-
expected_params = {
586-
'Bucket': self.bucket,
587-
'Key': self.key,
588-
}
589-
expected_ranges = ['bytes=0-3', 'bytes=4-7', 'bytes=8-']
590-
591-
# Stub HeadObject response with no ETag
592-
head_object_response = {
593-
'method': 'head_object',
594-
'service_response': {
595-
'ContentLength': len(self.content),
596-
},
597-
'expected_params': expected_params,
598-
}
599-
self.stubber.add_response(**head_object_response)
600-
601-
# This asserts that IfMatch isn't in the GetObject requests.
602526
self.add_successful_get_object_responses(
603527
expected_params, expected_ranges
604528
)

tests/unit/test_download.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -395,8 +395,7 @@ def setUp(self):
395395

396396
self.bucket = 'mybucket'
397397
self.key = 'mykey'
398-
self.etag = 'myetag'
399-
self.extra_args = {'IfMatch': self.etag}
398+
self.extra_args = {}
400399
self.subscribers = []
401400

402401
# Create a stream to read from
@@ -453,11 +452,7 @@ def assert_tag_for_get_object(self, tag_value):
453452

454453
def add_head_object_response(self):
455454
self.stubber.add_response(
456-
'head_object',
457-
{
458-
'ContentLength': len(self.content),
459-
'ETag': self.etag,
460-
},
455+
'head_object', {'ContentLength': len(self.content)}
461456
)
462457

463458
def add_get_responses(self):

0 commit comments

Comments
 (0)