diff --git a/awscli/customizations/s3/s3handler.py b/awscli/customizations/s3/s3handler.py index 3cd6aa6a533c..6c41a019cf06 100644 --- a/awscli/customizations/s3/s3handler.py +++ b/awscli/customizations/s3/s3handler.py @@ -517,43 +517,8 @@ def _submit_transfer_request(self, fileinfo, extra_args, subscribers): def _get_warning_handlers(self): return [ self._warn_glacier, - self._warn_if_zero_byte_file_exists_with_no_overwrite, ] - def _warn_if_zero_byte_file_exists_with_no_overwrite(self, fileinfo): - """ - Warning handler to skip zero-byte files when no_overwrite is set and file exists. - - This method handles the transfer of zero-byte objects when the no-overwrite parameter is specified. - To prevent overwrite, it uses head_object to verify if the object exists at the destination: - If the object is present at destination: skip the file (return True) - If the object is not present at destination: allow transfer (return False) - - :type fileinfo: FileInfo - :param fileinfo: The FileInfo object containing transfer details - - :rtype: bool - :return: True if file should be skipped, False if transfer should proceed - """ - if not self._cli_params.get('no_overwrite') or ( - getattr(fileinfo, 'size') and fileinfo.size > 0 - ): - return False - - bucket, key = find_bucket_key(fileinfo.dest) - client = fileinfo.source_client - try: - client.head_object(Bucket=bucket, Key=key) - LOGGER.debug( - f"warning: skipping {fileinfo.src} -> {fileinfo.dest}, file exists at destination" - ) - return True - except ClientError as e: - if e.response['Error']['Code'] == '404': - return False - else: - raise - def _format_src_dest(self, fileinfo): src = self._format_s3_path(fileinfo.src) dest = self._format_s3_path(fileinfo.dest) diff --git a/awscli/s3transfer/copies.py b/awscli/s3transfer/copies.py index 92ee2ea2e015..9f2c75a6e60e 100644 --- a/awscli/s3transfer/copies.py +++ b/awscli/s3transfer/copies.py @@ -81,9 +81,6 @@ class CopySubmissionTask(SubmissionTask): 'IfNoneMatch', ] - COPY_OBJECT_ARGS_BLOCKLIST = [ - 'IfNoneMatch', - ] def _submit( self, client, config, osutil, request_executor, transfer_future @@ -141,24 +138,9 @@ def _submit( # during a multipart copy. transfer_future.meta.provide_object_etag(response.get('ETag')) - # Check for ifNoneMatch is enabled and file has content - # Special handling for 0-byte files: Since multipart copy works with object size - # and divides the object into smaller chunks, there's an edge case when the object - # size is zero. This would result in 0 parts being calculated, and the - # CompleteMultipartUpload operation throws a MalformedXML error when transferring - # 0 parts because the XML does not validate against the published schema. - # Therefore, 0-byte files are always handled via single copy request regardless - # of the multipart threshold setting. - should_overwrite = ( - call_args.extra_args.get("IfNoneMatch") - and transfer_future.meta.size != 0 - ) - # If it is less than threshold and ifNoneMatch is not in parameters - # do a regular copy else do multipart copy. - if ( - transfer_future.meta.size < config.multipart_threshold - and not should_overwrite - ): + # If it is greater than threshold do a multipart copy, otherwise + # do a regular copy object. + if transfer_future.meta.size < config.multipart_threshold: self._submit_copy_request( client, config, osutil, request_executor, transfer_future ) @@ -175,13 +157,6 @@ def _submit_copy_request( # Get the needed progress callbacks for the task progress_callbacks = get_callbacks(transfer_future, 'progress') - # Submit the request of a single copy and make sure it - # does not include any blocked arguments. - copy_object_extra_args = { - param: val - for param, val in call_args.extra_args.items() - if param not in self.COPY_OBJECT_ARGS_BLOCKLIST - } self._transfer_coordinator.submit( request_executor, CopyObjectTask( @@ -191,7 +166,7 @@ def _submit_copy_request( "copy_source": call_args.copy_source, "bucket": call_args.bucket, "key": call_args.key, - "extra_args": copy_object_extra_args, + "extra_args": call_args.extra_args, "callbacks": progress_callbacks, "size": transfer_future.meta.size, }, diff --git a/tests/functional/s3/test_cp_command.py b/tests/functional/s3/test_cp_command.py index e176fd086bed..36a4d7c7df79 100644 --- a/tests/functional/s3/test_cp_command.py +++ b/tests/functional/s3/test_cp_command.py @@ -387,90 +387,32 @@ def test_no_overwrite_flag_multipart_upload_when_object_exists_on_target( def test_no_overwrite_flag_on_copy_when_small_object_does_not_exist_on_target( self, ): - cmdline = f'{self.prefix} s3://bucket1/key.txt s3://bucket/key1.txt --no-overwrite' - # Set up responses for multipart copy (since no-overwrite always uses multipart) + cmdline = f'{self.prefix} s3://bucket1/key.txt s3://bucket/key.txt --no-overwrite' self.parsed_responses = [ - self.head_object_response(), # HeadObject to get source metadata - self.create_mpu_response('foo'), # CreateMultipartUpload response - self.upload_part_copy_response(), # UploadPartCopy response - {}, # CompleteMultipartUpload response + self.head_object_response(ContentLength=5), + self.copy_object_response(), ] self.run_cmd(cmdline, expected_rc=0) - # Verify all multipart operations were called - self.assertEqual(len(self.operations_called), 4) - self.assertEqual(self.operations_called[0][0].name, 'HeadObject') - self.assertEqual( - self.operations_called[1][0].name, 'CreateMultipartUpload' - ) - self.assertEqual(self.operations_called[2][0].name, 'UploadPartCopy') self.assertEqual( - self.operations_called[3][0].name, 'CompleteMultipartUpload' + len(self.operations_called), 2, self.operations_called ) - # Verify the IfNoneMatch condition was set in the CompleteMultipartUpload request - self.assertEqual(self.operations_called[3][1]['IfNoneMatch'], '*') + self.assertEqual(self.operations_called[0][0].name, 'HeadObject') + self.assertEqual(self.operations_called[1][0].name, 'CopyObject') + self.assertEqual(self.operations_called[1][1]['IfNoneMatch'], '*') def test_no_overwrite_flag_on_copy_when_small_object_exists_on_target( self, ): cmdline = f'{self.prefix} s3://bucket1/key.txt s3://bucket/key.txt --no-overwrite' - # Set up responses for multipart copy (since no-overwrite always uses multipart) self.parsed_responses = [ - self.head_object_response(), # HeadObject to get source metadata - self.create_mpu_response('foo'), # CreateMultipartUpload response - self.upload_part_copy_response(), # UploadPartCopy response - self.precondition_failed_error_response(), # CompleteMultipartUpload - {}, # AbortMultipartUpload response - ] - self.run_cmd(cmdline, expected_rc=0) - # Verify all multipart operations were called - self.assertEqual(len(self.operations_called), 5) - self.assertEqual(self.operations_called[0][0].name, 'HeadObject') - self.assertEqual( - self.operations_called[1][0].name, 'CreateMultipartUpload' - ) - self.assertEqual(self.operations_called[2][0].name, 'UploadPartCopy') - self.assertEqual( - self.operations_called[3][0].name, 'CompleteMultipartUpload' - ) - self.assertEqual( - self.operations_called[4][0].name, 'AbortMultipartUpload' - ) - # Verify the IfNoneMatch condition was set in the CompleteMultipartUpload request - self.assertEqual(self.operations_called[3][1]['IfNoneMatch'], '*') - - def test_no_overwrite_flag_on_copy_when_zero_size_object_exists_at_destination( - self, - ): - cmdline = f'{self.prefix} s3://bucket1/file.txt s3://bucket2/file.txt --no-overwrite' - self.parsed_responses = [ - self.head_object_response( - ContentLength=0 - ), # Source object (zero size) - self.head_object_response(), # Checking the object at destination + self.head_object_response(ContentLength=5), + self.precondition_failed_error_response(), ] self.run_cmd(cmdline, expected_rc=0) self.assertEqual(len(self.operations_called), 2) self.assertEqual(self.operations_called[0][0].name, 'HeadObject') - self.assertEqual(self.operations_called[1][0].name, 'HeadObject') - - def test_no_overwrite_flag_on_copy_when_zero_size_object_not_exists_at_destination( - self, - ): - cmdline = f'{self.prefix} s3://bucket1/file.txt s3://bucket2/file1.txt --no-overwrite' - self.parsed_responses = [ - self.head_object_response( - ContentLength=0 - ), # Source object (zero size) - { - 'Error': {'Code': '404', 'Message': 'Not Found'} - }, # At destination object does not exists - self.copy_object_response(), # Copy Request when object does not exists - ] - self.run_cmd(cmdline, expected_rc=0) - self.assertEqual(len(self.operations_called), 3) - self.assertEqual(self.operations_called[0][0].name, 'HeadObject') - self.assertEqual(self.operations_called[1][0].name, 'HeadObject') - self.assertEqual(self.operations_called[2][0].name, 'CopyObject') + self.assertEqual(self.operations_called[1][0].name, 'CopyObject') + self.assertEqual(self.operations_called[1][1]['IfNoneMatch'], '*') def test_no_overwrite_flag_on_copy_when_large_object_exists_on_target( self, diff --git a/tests/functional/s3/test_mv_command.py b/tests/functional/s3/test_mv_command.py index 2935531e0f3e..0017db89a265 100644 --- a/tests/functional/s3/test_mv_command.py +++ b/tests/functional/s3/test_mv_command.py @@ -423,26 +423,21 @@ def test_mv_no_overwrite_flag_on_copy_when_small_object_does_not_exist_on_target # Set up responses for multipart copy (since no-overwrite always uses multipart) self.parsed_responses = [ self.head_object_response(), # HeadObject to get source metadata - self.create_mpu_response('foo'), # CreateMultipartUpload response - self.upload_part_copy_response(), # UploadPartCopy response - {}, # CompleteMultipartUpload response - self.delete_object_response(), # DeleteObject (for move operation) + self.copy_object_response(), + self.delete_object_response(), ] self.run_cmd(cmdline, expected_rc=0) # Verify all multipart copy operations were called - self.assertEqual(len(self.operations_called), 5) - self.assertEqual(len(self.operations_called), 5) + self.assertEqual(len(self.operations_called), 3) self.assertEqual(self.operations_called[0][0].name, 'HeadObject') self.assertEqual( - self.operations_called[1][0].name, 'CreateMultipartUpload' + self.operations_called[1][0].name, 'CopyObject' ) - self.assertEqual(self.operations_called[2][0].name, 'UploadPartCopy') + self.assertEqual(self.operations_called[1][1]['IfNoneMatch'], '*') + self.assertEqual( - self.operations_called[3][0].name, 'CompleteMultipartUpload' + self.operations_called[2][0].name, 'DeleteObject' ) - self.assertEqual(self.operations_called[4][0].name, 'DeleteObject') - # Verify the IfNoneMatch condition was set in the CompleteMultipartUpload request - self.assertEqual(self.operations_called[3][1]['IfNoneMatch'], '*') def test_mv_no_overwrite_flag_on_copy_when_small_object_exists_on_target( self, @@ -451,65 +446,19 @@ def test_mv_no_overwrite_flag_on_copy_when_small_object_exists_on_target( # Set up responses for multipart copy (since no-overwrite always uses multipart) self.parsed_responses = [ self.head_object_response(), # HeadObject to get source metadata - self.create_mpu_response('foo'), # CreateMultipartUpload response - self.upload_part_copy_response(), # UploadPartCopy response - self.precondition_failed_error_response(), # CompleteMultipartUpload response - {}, # AbortMultipart + self.precondition_failed_error_response(), # CopyObject response ] self.run_cmd(cmdline, expected_rc=0) # Set up the response to simulate a PreconditionFailed error self.http_response.status_code = 412 - # Verify all multipart copy operations were called - self.assertEqual(len(self.operations_called), 5) + # Verify all copy operations were called + self.assertEqual(len(self.operations_called), 2) self.assertEqual(self.operations_called[0][0].name, 'HeadObject') self.assertEqual( - self.operations_called[1][0].name, 'CreateMultipartUpload' - ) - self.assertEqual(self.operations_called[2][0].name, 'UploadPartCopy') - self.assertEqual( - self.operations_called[3][0].name, 'CompleteMultipartUpload' - ) - self.assertEqual( - self.operations_called[4][0].name, 'AbortMultipartUpload' + self.operations_called[1][0].name, 'CopyObject' ) - # Verify the IfNoneMatch condition was set in the CompleteMultipartUpload request - self.assertEqual(self.operations_called[3][1]['IfNoneMatch'], '*') - - def test_no_overwrite_flag_on_copy_when_zero_size_object_exists_at_destination( - self, - ): - cmdline = f'{self.prefix} s3://bucket1/file.txt s3://bucket2/file.txt --no-overwrite' - self.parsed_responses = [ - self.head_object_response( - ContentLength=0 - ), # Source object (zero size) - self.head_object_response(), # Checking the object at destination - ] - self.run_cmd(cmdline, expected_rc=0) - self.assertEqual(len(self.operations_called), 2) - self.assertEqual(self.operations_called[0][0].name, 'HeadObject') - self.assertEqual(self.operations_called[1][0].name, 'HeadObject') - - def test_no_overwrite_flag_on_copy_when_zero_size_object_not_exists_at_destination( - self, - ): - cmdline = f'{self.prefix} s3://bucket1/file.txt s3://bucket2/file1.txt --no-overwrite' - self.parsed_responses = [ - self.head_object_response( - ContentLength=0 - ), # Source object (zero size) - { - 'Error': {'Code': '404', 'Message': 'Not Found'} - }, # At destination object does not exists - self.copy_object_response(), # Copy Request when object does not exists - self.delete_object_response(), # Delete Request for move object - ] - self.run_cmd(cmdline, expected_rc=0) - self.assertEqual(len(self.operations_called), 4) - self.assertEqual(self.operations_called[0][0].name, 'HeadObject') - self.assertEqual(self.operations_called[1][0].name, 'HeadObject') - self.assertEqual(self.operations_called[2][0].name, 'CopyObject') - self.assertEqual(self.operations_called[3][0].name, 'DeleteObject') + # Verify the IfNoneMatch condition was set in the CopyObject request + self.assertEqual(self.operations_called[1][1]['IfNoneMatch'], '*') def test_mv_no_overwrite_flag_when_large_object_exists_on_target(self): cmdline = f'{self.prefix} s3://bucket1/key1.txt s3://bucket/key1.txt --no-overwrite' diff --git a/tests/functional/s3transfer/test_copy.py b/tests/functional/s3transfer/test_copy.py index 378b9eefe8f5..1935e7cf1764 100644 --- a/tests/functional/s3transfer/test_copy.py +++ b/tests/functional/s3transfer/test_copy.py @@ -255,6 +255,69 @@ def test_copy_with_extra_args(self): future.result() self.stubber.assert_no_pending_responses() + def test_copy_with_ifnonematch_when_object_not_exists_at_target(self): + self.extra_args['IfNoneMatch'] = '*' + + expected_head_params = { + 'Bucket': 'mysourcebucket', + 'Key': 'mysourcekey', + } + + expected_copy_object = { + 'Bucket': 'mybucket', + 'Key': 'mykey', + 'CopySource': { + 'Bucket': 'mysourcebucket', + 'Key': 'mysourcekey', + }, + "IfNoneMatch": "*" + } + + self.add_head_object_response(expected_params=expected_head_params) + self.add_successful_copy_responses( + expected_copy_params=expected_copy_object + ) + + call_kwargs = self.create_call_kwargs() + call_kwargs['extra_args'] = self.extra_args + future = self.manager.copy(**call_kwargs) + future.result() + self.stubber.assert_no_pending_responses() + + def test_copy_with_ifnonematch_when_object_exists_at_target(self): + self.extra_args['IfNoneMatch'] = '*' + + expected_head_params = { + 'Bucket': 'mysourcebucket', + 'Key': 'mysourcekey', + } + + self.add_head_object_response(expected_params=expected_head_params) + + # Mock a PreconditionFailed error for copy_object + self.stubber.add_client_error( + method='copy_object', + service_error_code='PreconditionFailed', + service_message='The condition specified in the conditional header(s) was not met', + http_status_code=412, + expected_params={ + 'Bucket': self.bucket, + 'Key': self.key, + 'CopySource': self.copy_source, + 'IfNoneMatch': '*', + }, + ) + + call_kwargs = self.create_call_kwargs() + call_kwargs['extra_args'] = self.extra_args + future = self.manager.copy(**call_kwargs) + with self.assertRaises(ClientError) as context: + future.result() + self.assertEqual( + context.exception.response['Error']['Code'], 'PreconditionFailed' + ) + self.stubber.assert_no_pending_responses() + def test_copy_maps_extra_args_to_head_object(self): self.extra_args['CopySourceSSECustomerAlgorithm'] = 'AES256' @@ -283,13 +346,8 @@ def test_copy_maps_extra_args_to_head_object(self): def test_allowed_copy_params_are_valid(self): op_model = self.client.meta.service_model.operation_model('CopyObject') - allowed_copy_arg = [ - arg - for arg in self._manager.ALLOWED_COPY_ARGS - if arg not in CopySubmissionTask.COPY_OBJECT_ARGS_BLOCKLIST - ] - for allowed_upload_arg in allowed_copy_arg: - self.assertIn(allowed_upload_arg, op_model.input_shape.members) + for allowed_copy_arg in self._manager.ALLOWED_COPY_ARGS: + self.assertIn(allowed_copy_arg, op_model.input_shape.members) def test_copy_with_tagging(self): extra_args = {'Tagging': 'tag1=val1', 'TaggingDirective': 'REPLACE'} @@ -719,136 +777,7 @@ def test_mp_copy_with_tagging_directive(self): future.result() self.stubber.assert_no_pending_responses() - def test_copy_with_no_overwrite_flag_when_small_object_exists_at_target( - self, - ): - # Set up IfNoneMatch in extra_args - self.extra_args['IfNoneMatch'] = '*' - # Setting up the size of object - small_content_size = 5 - self.content = b'0' * small_content_size - # Add head object response with small content size - head_response = self.create_stubbed_responses()[0] - head_response['service_response'] = { - 'ContentLength': small_content_size - } - self.stubber.add_response(**head_response) - # Should use multipart upload - # Add create_multipart_upload response - self.stubber.add_response( - 'create_multipart_upload', - service_response={'UploadId': self.multipart_id}, - expected_params={ - 'Bucket': self.bucket, - 'Key': self.key, - }, - ) - # Add upload_part_copy response - self.stubber.add_response( - 'upload_part_copy', - {'CopyPartResult': {'ETag': 'etag-1'}}, - { - 'Bucket': self.bucket, - 'Key': self.key, - 'CopySource': self.copy_source, - 'UploadId': self.multipart_id, - 'PartNumber': 1, - 'CopySourceRange': f'bytes=0-{small_content_size-1}', - }, - ) - # Mock a PreconditionFailed error for complete_multipart_upload - self.stubber.add_client_error( - method='complete_multipart_upload', - service_error_code='PreconditionFailed', - service_message='The condition specified in the conditional header(s) was not met', - http_status_code=412, - expected_params={ - 'Bucket': self.bucket, - 'Key': self.key, - 'UploadId': self.multipart_id, - 'MultipartUpload': { - 'Parts': [{'ETag': 'etag-1', 'PartNumber': 1}] - }, - 'IfNoneMatch': '*', - }, - ) - # Add abort_multipart_upload response - self.stubber.add_response( - 'abort_multipart_upload', - service_response={}, - expected_params={ - 'Bucket': self.bucket, - 'Key': self.key, - 'UploadId': self.multipart_id, - }, - ) - call_kwargs = self.create_call_kwargs() - call_kwargs['extra_args'] = self.extra_args - future = self.manager.copy(**call_kwargs) - with self.assertRaises(ClientError) as context: - future.result() - self.assertEqual( - context.exception.response['Error']['Code'], 'PreconditionFailed' - ) - self.stubber.assert_no_pending_responses() - - def test_copy_with_no_overwrite_flag_when_small_object_not_exists_at_target( - self, - ): - # Set up IfNoneMatch in extra_args - self.extra_args['IfNoneMatch'] = '*' - # Setting up the size of object - small_content_size = 5 - self.content = b'0' * small_content_size - # Add head object response with small content size - head_response = self.create_stubbed_responses()[0] - head_response['service_response'] = { - 'ContentLength': small_content_size - } - self.stubber.add_response(**head_response) - # Should use multipart copy - # Add create_multipart_upload response - self.stubber.add_response( - 'create_multipart_upload', - service_response={'UploadId': self.multipart_id}, - expected_params={ - 'Bucket': self.bucket, - 'Key': self.key, - }, - ) - # Add upload_part_copy response - self.stubber.add_response( - 'upload_part_copy', - {'CopyPartResult': {'ETag': 'etag-1'}}, - { - 'Bucket': self.bucket, - 'Key': self.key, - 'CopySource': self.copy_source, - 'UploadId': self.multipart_id, - 'PartNumber': 1, - 'CopySourceRange': f'bytes=0-{small_content_size-1}', - }, - ) - self.stubber.add_response( - 'complete_multipart_upload', - service_response={}, - expected_params={ - 'Bucket': self.bucket, - 'Key': self.key, - 'UploadId': self.multipart_id, - 'MultipartUpload': { - 'Parts': [{'ETag': 'etag-1', 'PartNumber': 1}] - }, - 'IfNoneMatch': '*', - }, - ) - call_kwargs = self.create_call_kwargs() - call_kwargs['extra_args'] = self.extra_args - future = self.manager.copy(**call_kwargs) - future.result() - self.stubber.assert_no_pending_responses() - - def test_copy_with_no_overwrite_flag_when_large_object_exists_at_target( + def test_copy_with_ifnonematch_when_large_object_exists_at_target( self, ): # Set up IfNoneMatch in extra_args @@ -898,7 +827,7 @@ def test_copy_with_no_overwrite_flag_when_large_object_exists_at_target( ) self.stubber.assert_no_pending_responses() - def test_copy_with_no_overwrite_flag_when_large_object_not_exists_at_target( + def test_copy_with_ifnonematch_when_large_object_not_exists_at_target( self, ): # Set up IfNoneMatch in extra_args @@ -931,6 +860,7 @@ def test_copy_with_no_overwrite_flag_when_large_object_not_exists_at_target( future = self.manager.copy(**call_kwargs) future.result() self.stubber.assert_no_pending_responses() + def test_copy_fails_if_etag_validation_fails(self): expected_params = { 'Bucket': 'mybucket', diff --git a/tests/unit/customizations/s3/test_s3handler.py b/tests/unit/customizations/s3/test_s3handler.py index 6f08536d8845..464b46c6000d 100644 --- a/tests/unit/customizations/s3/test_s3handler.py +++ b/tests/unit/customizations/s3/test_s3handler.py @@ -957,59 +957,6 @@ def test_submit_move_adds_delete_source_subscriber(self): for i, actual_subscriber in enumerate(actual_subscribers): self.assertIsInstance(actual_subscriber, ref_subscribers[i]) - def test_skip_copy_with_no_overwrite_and_zero_byte_file_exists(self): - self.cli_params['no_overwrite'] = True - fileinfo = FileInfo( - src=self.source_bucket + "/" + self.source_key, - dest=self.bucket + "/" + self.key, - operation_name='copy', - size=0, - source_client=mock.Mock(), - ) - fileinfo.source_client.head_object.return_value = {} - future = self.transfer_request_submitter.submit(fileinfo) - # The transfer should be skipped, so future should be None - self.assertIsNone(future) - # Result Queue should be empty because it was specified to ignore no-overwrite warnings. - self.assertTrue(self.result_queue.empty()) - self.assertEqual(len(self.transfer_manager.copy.call_args_list), 0) - - def test_proceed_copy_with_no_overwrite_and_zero_byte_file_does_not_exist( - self, - ): - self.cli_params['no_overwrite'] = True - fileinfo = FileInfo( - src=self.source_bucket + "/" + self.source_key, - dest=self.bucket + "/" + self.key, - operation_name='copy', - size=0, - source_client=mock.Mock(), - ) - fileinfo.source_client.head_object.side_effect = ClientError( - {'Error': {'Code': '404', 'Message': 'Not Found'}}, - 'HeadObject', - ) - future = self.transfer_request_submitter.submit(fileinfo) - # The transfer should proceed, so future should be the transfer manager's return value - self.assertIs(self.transfer_manager.copy.return_value, future) - self.assertEqual(len(self.transfer_manager.copy.call_args_list), 1) - - def test_proceed_copy_with_no_overwrite_for_non_zero_byte_file(self): - self.cli_params['no_overwrite'] = True - fileinfo = FileInfo( - src=self.source_bucket + "/" + self.source_key, - dest=self.bucket + "/" + self.key, - operation_name='copy', - size=100, - source_client=mock.Mock(), - ) - future = self.transfer_request_submitter.submit(fileinfo) - # The transfer should proceed, so future should be the transfer manager's return value - self.assertIs(self.transfer_manager.copy.return_value, future) - self.assertEqual(len(self.transfer_manager.copy.call_args_list), 1) - # Head should not be called when no_overwrite is false - fileinfo.source_client.head_object.assert_not_called() - def test_file_exists_without_no_overwrite(self): self.cli_params['no_overwrite'] = False fileinfo = FileInfo(