2121from botocore .exceptions import ClientError
2222
2323from s3transfer .compat import SOCKET_ERROR
24- from s3transfer .exceptions import RetriesExceededError
24+ from s3transfer .exceptions import RetriesExceededError , S3DownloadFailedError
2525from s3transfer .manager import TransferConfig , TransferManager
2626from tests import (
2727 BaseGeneralInterfaceTest ,
28+ ETagProvider ,
2829 FileSizeProvider ,
2930 NonSeekableWriter ,
3031 RecordingOSUtils ,
@@ -48,6 +49,7 @@ def setUp(self):
4849 # Initialize some default arguments
4950 self .bucket = 'mybucket'
5051 self .key = 'mykey'
52+ self .etag = 'myetag'
5153 self .extra_args = {}
5254 self .subscribers = []
5355
@@ -84,7 +86,10 @@ def create_stubbed_responses(self):
8486 return [
8587 {
8688 'method' : 'head_object' ,
87- 'service_response' : {'ContentLength' : len (self .content )},
89+ 'service_response' : {
90+ 'ContentLength' : len (self .content ),
91+ 'ETag' : self .etag ,
92+ },
8893 },
8994 {
9095 'method' : 'get_object' ,
@@ -290,11 +295,14 @@ def test_retry_rewinds_callbacks(self):
290295 ]
291296 self .assertEqual (- 3 , progress_byte_amts [1 ])
292297
293- def test_can_provide_file_size (self ):
298+ def test_can_provide_file_size_and_etag (self ):
294299 self .add_successful_get_object_responses ()
295300
296301 call_kwargs = self .create_call_kwargs ()
297- call_kwargs ['subscribers' ] = [FileSizeProvider (len (self .content ))]
302+ call_kwargs ['subscribers' ] = [
303+ FileSizeProvider (len (self .content )),
304+ ETagProvider (self .etag ),
305+ ]
298306
299307 future = self .manager .download (** call_kwargs )
300308 future .result ()
@@ -469,7 +477,10 @@ def create_stubbed_responses(self):
469477 return [
470478 {
471479 'method' : 'head_object' ,
472- 'service_response' : {'ContentLength' : len (self .content )},
480+ 'service_response' : {
481+ 'ContentLength' : len (self .content ),
482+ 'ETag' : self .etag ,
483+ },
473484 },
474485 {
475486 'method' : 'get_object' ,
@@ -502,7 +513,7 @@ def test_download(self):
502513 expected_ranges = ['bytes=0-3' , 'bytes=4-7' , 'bytes=8-' ]
503514 self .add_head_object_response (expected_params )
504515 self .add_successful_get_object_responses (
505- expected_params , expected_ranges
516+ { ** expected_params , 'IfMatch' : self . etag } , expected_ranges
506517 )
507518
508519 future = self .manager .download (
@@ -523,6 +534,76 @@ def test_download_with_checksum_enabled(self):
523534 }
524535 expected_ranges = ['bytes=0-3' , 'bytes=4-7' , 'bytes=8-' ]
525536 self .add_head_object_response (expected_params )
537+ self .add_successful_get_object_responses (
538+ {** expected_params , 'IfMatch' : self .etag }, expected_ranges
539+ )
540+
541+ future = self .manager .download (
542+ self .bucket , self .key , self .filename , self .extra_args
543+ )
544+ future .result ()
545+
546+ # Ensure that the contents are correct
547+ with open (self .filename , 'rb' ) as f :
548+ self .assertEqual (self .content , f .read ())
549+
550+ def test_download_raises_if_etag_validation_fails (self ):
551+ expected_params = {
552+ 'Bucket' : self .bucket ,
553+ 'Key' : self .key ,
554+ }
555+ expected_ranges = ['bytes=0-3' , 'bytes=4-7' ]
556+ self .add_head_object_response (expected_params )
557+
558+ # Add successful GetObject responses for the first 2 requests.
559+ for i , stubbed_response in enumerate (
560+ self .create_stubbed_responses ()[1 :3 ]
561+ ):
562+ stubbed_response ['expected_params' ] = copy .deepcopy (
563+ {** expected_params , 'IfMatch' : self .etag }
564+ )
565+ stubbed_response ['expected_params' ]['Range' ] = expected_ranges [i ]
566+ self .stubber .add_response (** stubbed_response )
567+
568+ # Simulate ETag validation failure by adding a
569+ # client error for the last GetObject request.
570+ self .stubber .add_client_error (
571+ method = 'get_object' ,
572+ service_error_code = 'PreconditionFailed' ,
573+ service_message = (
574+ 'At least one of the pre-conditions you specified did not hold'
575+ ),
576+ http_status_code = 412 ,
577+ )
578+
579+ future = self .manager .download (
580+ self .bucket , self .key , self .filename , self .extra_args
581+ )
582+ with self .assertRaises (S3DownloadFailedError ) as e :
583+ future .result ()
584+ self .assertIn ('did not match expected ETag' , str (e .exception ))
585+
586+ # Ensure no data is written to disk.
587+ self .assertFalse (os .path .exists (self .filename ))
588+
589+ def test_download_without_etag (self ):
590+ expected_params = {
591+ 'Bucket' : self .bucket ,
592+ 'Key' : self .key ,
593+ }
594+ expected_ranges = ['bytes=0-3' , 'bytes=4-7' , 'bytes=8-' ]
595+
596+ # Stub HeadObject response with no ETag
597+ head_object_response = {
598+ 'method' : 'head_object' ,
599+ 'service_response' : {
600+ 'ContentLength' : len (self .content ),
601+ },
602+ 'expected_params' : expected_params ,
603+ }
604+ self .stubber .add_response (** head_object_response )
605+
606+ # This asserts that IfMatch isn't in the GetObject requests.
526607 self .add_successful_get_object_responses (
527608 expected_params , expected_ranges
528609 )
0 commit comments