Skip to content

Commit db0a335

Browse files
author
Rakshil Modi
committed
Add no overwrite for sync operation
Added no overwrite for download operation handled error during download streaming streaming Improved upload test cases Updated test cases Added test cases
1 parent 19096f8 commit db0a335

File tree

7 files changed

+214
-59
lines changed

7 files changed

+214
-59
lines changed

awscli/customizations/s3/s3handler.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -452,8 +452,8 @@ def _warn_if_file_exists_with_no_overwrite(self, fileinfo):
452452
when the --no-overwrite flag is specified. It checks if the destination file already
453453
exists on the local filesystem and skips the download if found.
454454
455-
:type fileinfo: awscli.customizations.s3.fileinfo.FileInfo
456-
:param fileinfo: The FileInfo object containing source and destination details
455+
:type fileinfo: FileInfo
456+
:param fileinfo: The FileInfo object containing transfer details
457457
458458
:rtype: bool
459459
:returns: True if the file should be skipped (exists and no-overwrite is set),

awscli/customizations/s3/subcommands.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -684,6 +684,7 @@
684684
REQUEST_PAYER,
685685
CHECKSUM_MODE,
686686
CHECKSUM_ALGORITHM,
687+
NO_OVERWRITE,
687688
]
688689

689690

@@ -1072,7 +1073,6 @@ class CpCommand(S3TransferCommand):
10721073
METADATA_DIRECTIVE,
10731074
EXPECTED_SIZE,
10741075
RECURSIVE,
1075-
NO_OVERWRITE,
10761076
]
10771077
)
10781078

@@ -1097,7 +1097,6 @@ class MvCommand(S3TransferCommand):
10971097
METADATA_DIRECTIVE,
10981098
RECURSIVE,
10991099
VALIDATE_SAME_S3_PATHS,
1100-
NO_OVERWRITE,
11011100
]
11021101
)
11031102

@@ -1401,7 +1400,8 @@ def run(self):
14011400
self._client, self._source_client, self.parameters
14021401
)
14031402

1404-
s3_transfer_handler = S3TransferHandlerFactory(self.parameters)(
1403+
params = self._get_s3_handler_params()
1404+
s3_transfer_handler = S3TransferHandlerFactory(params)(
14051405
self._transfer_manager, result_queue
14061406
)
14071407

@@ -1510,6 +1510,19 @@ def _map_sse_c_params(self, request_parameters, paths_type):
15101510
},
15111511
)
15121512

1513+
def _get_s3_handler_params(self):
1514+
"""
1515+
Removing no-overwrite params from sync since file to
1516+
be synced are already separated out using sync strategy
1517+
"""
1518+
if self.cmd == 'sync':
1519+
return {
1520+
param: val
1521+
for param, val in self.parameters.items()
1522+
if param != 'no_overwrite'
1523+
}
1524+
return self.parameters
1525+
15131526

15141527
# TODO: This class is fairly quirky in the sense that it is both a builder
15151528
# and a data object. In the future we should make the following refactorings
@@ -1603,7 +1616,7 @@ def _validate_streaming_paths(self):
16031616
self.parameters['is_stream'] = True
16041617
self.parameters['dir_op'] = False
16051618
self.parameters['only_show_errors'] = True
1606-
self._validate_streaming_no_overwrite_for_download_parameter()
1619+
self._validate_no_overwrite_for_download_streaming()
16071620

16081621
def _validate_path_args(self):
16091622
# If we're using a mv command, you can't copy the object onto itself.
@@ -1826,7 +1839,7 @@ def _validate_sse_c_copy_source_for_paths(self):
18261839
'copy operations.'
18271840
)
18281841

1829-
def _validate_streaming_no_overwrite_for_download_parameter(self):
1842+
def _validate_no_overwrite_for_download_streaming(self):
18301843
"""
18311844
Validates that no-overwrite parameter is not used with streaming downloads.
18321845

tests/functional/s3/test_cp_command.py

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -315,13 +315,7 @@ def test_no_overwrite_flag_when_object_exists_on_target(self):
315315
# Set up the response to simulate a PreconditionFailed error
316316
self.http_response.status_code = 412
317317
self.parsed_responses = [
318-
{
319-
'Error': {
320-
'Code': 'PreconditionFailed',
321-
'Message': 'At least one of the pre-conditions you specified did not hold',
322-
'Condition': 'If-None-Match',
323-
}
324-
}
318+
self.precondition_failed_error_response(),
325319
]
326320
self.run_cmd(cmdline, expected_rc=0)
327321
# Verify PutObject was attempted with IfNoneMatch
@@ -367,13 +361,7 @@ def test_no_overwrite_flag_multipart_upload_when_object_exists_on_target(
367361
{'UploadId': 'foo'}, # CreateMultipartUpload response
368362
{'ETag': '"foo-1"'}, # UploadPart response
369363
{'ETag': '"foo-2"'}, # UploadPart response
370-
{
371-
'Error': {
372-
'Code': 'PreconditionFailed',
373-
'Message': 'At least one of the pre-conditions you specified did not hold',
374-
'Condition': 'If-None-Match',
375-
}
376-
}, # PreconditionFailed error for CompleteMultipart Upload
364+
self.precondition_failed_error_response(), # PreconditionFailed error for CompleteMultipart Upload
377365
{}, # AbortMultipartUpload response
378366
]
379367
# Checking for success as file is skipped
@@ -1496,6 +1484,15 @@ def test_streaming_download_error(self):
14961484
)
14971485
self.assertIn(error_message, stderr)
14981486

1487+
def test_no_overwrite_cannot_be_used_with_streaming_download(self):
1488+
command = "s3 cp s3://bucket/streaming.txt - --no-overwrite"
1489+
_, stderr, _ = self.run_cmd(command, expected_rc=252)
1490+
error_message = (
1491+
"--no-overwrite parameter is not supported for "
1492+
"streaming downloads"
1493+
)
1494+
self.assertIn(error_message, stderr)
1495+
14991496

15001497
class TestCpCommandWithRequesterPayer(BaseCPCommandTest):
15011498
def setUp(self):

tests/functional/s3/test_mv_command.py

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -336,13 +336,7 @@ def test_mv_no_overwrite_flag_when_object_exists_on_target(self):
336336
# Set up the response to simulate a PreconditionFailed error
337337
self.http_response.status_code = 412
338338
self.parsed_responses = [
339-
{
340-
'Error': {
341-
'Code': 'PreconditionFailed',
342-
'Message': 'At least one of the pre-conditions you specified did not hold',
343-
'Condition': 'If-None-Match',
344-
}
345-
}
339+
self.precondition_failed_error_response(),
346340
]
347341
self.run_cmd(cmdline, expected_rc=0)
348342
# Verify PutObject was attempted with IfNoneMatch
@@ -392,13 +386,7 @@ def test_mv_no_overwrite_flag_multipart_upload_when_object_exists_on_target(
392386
{'UploadId': 'foo'}, # CreateMultipartUpload response
393387
{'ETag': '"foo-1"'}, # UploadPart response
394388
{'ETag': '"foo-2"'}, # UploadPart response
395-
{
396-
'Error': {
397-
'Code': 'PreconditionFailed',
398-
'Message': 'At least one of the pre-conditions you specified did not hold',
399-
'Condition': 'If-None-Match',
400-
}
401-
}, # CompleteMultipartUpload response
389+
self.precondition_failed_error_response(), # CompleteMultipartUpload response
402390
{}, # Abort Multipart
403391
]
404392
self.run_cmd(cmdline, expected_rc=0)

tests/functional/s3/test_sync_command.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,91 @@ def test_download_with_checksum_mode_crc64nvme(self):
540540
('ChecksumMode', 'ENABLED'), self.operations_called[1][1].items()
541541
)
542542

543+
def test_sync_upload_with_no_overwrite_when_file_does_not_exist_at_destination(
544+
self,
545+
):
546+
self.files.create_file("new_file.txt", "mycontent")
547+
self.parsed_responses = [
548+
self.list_objects_response(['file.txt']),
549+
self.get_object_response(),
550+
]
551+
cmdline = (
552+
f'{self.prefix} {self.files.rootdir} s3://bucket --no-overwrite'
553+
)
554+
self.run_cmd(cmdline, expected_rc=0)
555+
self.assertEqual(len(self.operations_called), 2)
556+
self.assertEqual(self.operations_called[0][0].name, 'ListObjectsV2')
557+
self.assertEqual(self.operations_called[1][0].name, 'PutObject')
558+
self.assertEqual(self.operations_called[1][1]['Key'], 'new_file.txt')
559+
560+
def test_sync_upload_with_no_overwrite_when_file_exists_at_destination(
561+
self,
562+
):
563+
self.files.create_file("new_file.txt", "mycontent")
564+
self.parsed_responses = [
565+
self.list_objects_response(['new_file.txt']),
566+
]
567+
cmdline = (
568+
f'{self.prefix} {self.files.rootdir} s3://bucket --no-overwrite'
569+
)
570+
self.run_cmd(cmdline, expected_rc=0)
571+
self.assertEqual(len(self.operations_called), 1)
572+
self.assertEqual(self.operations_called[0][0].name, 'ListObjectsV2')
573+
574+
def test_sync_download_with_no_overwrite_file_not_exists_at_destination(
575+
self,
576+
):
577+
self.parsed_responses = [
578+
self.list_objects_response(['new_file.txt']),
579+
self.get_object_response(),
580+
]
581+
cmdline = (
582+
f'{self.prefix} s3://bucket/ {self.files.rootdir} --no-overwrite'
583+
)
584+
self.run_cmd(cmdline, expected_rc=0)
585+
self.assertEqual(len(self.operations_called), 2)
586+
self.assertEqual(self.operations_called[0][0].name, 'ListObjectsV2')
587+
self.assertEqual(self.operations_called[1][0].name, 'GetObject')
588+
local_file_path = os.path.join(self.files.rootdir, 'new_file.txt')
589+
self.assertTrue(os.path.exists(local_file_path))
590+
591+
def test_sync_download_with_no_overwrite_file_exists_at_destination(self):
592+
self.files.create_file('file.txt', 'My content')
593+
self.parsed_responses = [
594+
self.list_objects_response(['file.txt']),
595+
]
596+
cmdline = (
597+
f'{self.prefix} s3://bucket/ {self.files.rootdir} --no-overwrite'
598+
)
599+
self.run_cmd(cmdline, expected_rc=0)
600+
self.assertEqual(len(self.operations_called), 1)
601+
self.assertEqual(self.operations_called[0][0].name, 'ListObjectsV2')
602+
603+
def test_sync_copy_with_no_overwrite_file_not_exists_at_destination(self):
604+
self.parsed_responses = [
605+
self.list_objects_response(['new_file.txt']),
606+
self.list_objects_response(['file1.txt']),
607+
self.copy_object_response(),
608+
]
609+
cmdline = f'{self.prefix} s3://bucket/ s3://bucket2/ --no-overwrite'
610+
self.run_cmd(cmdline, expected_rc=0)
611+
self.assertEqual(len(self.operations_called), 3)
612+
self.assertEqual(self.operations_called[0][0].name, 'ListObjectsV2')
613+
self.assertEqual(self.operations_called[1][0].name, 'ListObjectsV2')
614+
self.assertEqual(self.operations_called[2][0].name, 'CopyObject')
615+
self.assertEqual(self.operations_called[2][1]['Key'], 'new_file.txt')
616+
617+
def test_sync_copy_with_no_overwrite_file_exists_at_destination(self):
618+
self.parsed_responses = [
619+
self.list_objects_response(['new_file.txt']),
620+
self.list_objects_response(['new_file.txt', 'file1.txt']),
621+
]
622+
cmdline = f'{self.prefix} s3://bucket/ s3://bucket2/ --no-overwrite'
623+
self.run_cmd(cmdline, expected_rc=0)
624+
self.assertEqual(len(self.operations_called), 2)
625+
self.assertEqual(self.operations_called[0][0].name, 'ListObjectsV2')
626+
self.assertEqual(self.operations_called[1][0].name, 'ListObjectsV2')
627+
543628

544629
class TestSyncSourceRegion(BaseS3CLIRunnerTest):
545630
def test_respects_source_region(self):

tests/unit/customizations/s3/test_s3handler.py

Lines changed: 96 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# language governing permissions and limitations under the License.
1313
import os
1414

15+
from botocore.exceptions import ClientError
1516
from s3transfer.manager import TransferManager
1617

1718
from awscli.compat import queue
@@ -679,38 +680,39 @@ def test_submit_move_adds_delete_source_subscriber(self):
679680
for i, actual_subscriber in enumerate(actual_subscribers):
680681
self.assertIsInstance(actual_subscriber, ref_subscribers[i])
681682

682-
def test_submit_with_no_overwrite_flag_when_file_exists_at_destination(
683-
self,
684-
):
685-
# Setting up the CLI params with no_overwrite flag as True
683+
def test_skip_download_when_no_overwrite_and_file_exists(self):
684+
"""Testing warn_if_file_exists_with_no_overwrite handler when file exists at destination"""
686685
self.cli_params['no_overwrite'] = True
687686
fileinfo = self.create_file_info(self.key)
688-
# Mocking os.path.exists to simulate that file already exists
689687
with mock.patch('os.path.exists', return_value=True):
690-
# Submitting download request
691688
future = self.transfer_request_submitter.submit(fileinfo)
692-
# Asserting that the future is None, as the file already exists
693-
self.assertIsNone(future)
694-
# Asserting that no download happened
695-
self.assert_no_downloads_happened()
696689

697-
def test_submit_with_no_overwrite_flag_when_file_does_not_exist_at_destination(
698-
self,
699-
):
700-
# Setting up the CLI params with no_overwrite flag as True
690+
# Result Queue should be empty because it was specified to ignore no-overwrite warnings.
691+
self.assertTrue(self.result_queue.empty())
692+
# The transfer should be skipped, so future should be None
693+
self.assertIsNone(future)
694+
self.assert_no_downloads_happened()
695+
696+
def test_proceed_download_when_no_overwrite_and_file_not_exists(self):
697+
"""Testing warn_if_file_exists_with_no_overwrite handler when file does not exist at destination"""
701698
self.cli_params['no_overwrite'] = True
702699
fileinfo = self.create_file_info(self.key)
703-
# Mocking os.path.exists to return False, to simulate that file does not exist
704700
with mock.patch('os.path.exists', return_value=False):
705-
# Submitting download request
706701
future = self.transfer_request_submitter.submit(fileinfo)
707-
# Asserting that the future is the same object returned by transfer_manager.download
708-
# This confirms that download was actually initiated
709-
self.assertIs(self.transfer_manager.download.return_value, future)
710-
# Asserting that download happened
711-
self.assertEqual(
712-
len(self.transfer_manager.download.call_args_list), 1
713-
)
702+
# The transfer should proceed, so future should be the transfer manager's return value
703+
self.assertIs(self.transfer_manager.download.return_value, future)
704+
# And download should have happened
705+
self.assertEqual(len(self.transfer_manager.download.call_args_list), 1)
706+
707+
def test_warn_if_file_exists_without_no_overwrite_flag(self):
708+
self.cli_params['no_overwrite'] = False
709+
fileinfo = self.create_file_info(self.key)
710+
with mock.patch('os.path.exists', return_value=True):
711+
future = self.transfer_request_submitter.submit(fileinfo)
712+
# The transfer should proceed, so future should be the transfer manager's return value
713+
self.assertIs(self.transfer_manager.download.return_value, future)
714+
# And download should have happened
715+
self.assertEqual(len(self.transfer_manager.download.call_args_list), 1)
714716

715717

716718
class TestCopyRequestSubmitter(BaseTransferRequestSubmitterTest):
@@ -957,6 +959,77 @@ def test_submit_move_adds_delete_source_subscriber(self):
957959
for i, actual_subscriber in enumerate(actual_subscribers):
958960
self.assertIsInstance(actual_subscriber, ref_subscribers[i])
959961

962+
def test_skip_copy_with_no_overwrite_and_zero_byte_file_exists(self):
963+
"""Testing warn_if_zero_byte_file_exists_with_no_overwrite handler when zero byte exists"""
964+
self.cli_params['no_overwrite'] = True
965+
fileinfo = FileInfo(
966+
src=self.source_bucket + "/" + self.source_key,
967+
dest=self.bucket + "/" + self.key,
968+
operation_name='copy',
969+
size=0,
970+
source_client=mock.Mock(),
971+
)
972+
fileinfo.source_client.head_object.return_value = {}
973+
future = self.transfer_request_submitter.submit(fileinfo)
974+
# The transfer should be skipped, so future should be None
975+
self.assertIsNone(future)
976+
# Result Queue should be empty because it was specified to ignore no-overwrite warnings.
977+
self.assertTrue(self.result_queue.empty())
978+
self.assertEqual(len(self.transfer_manager.copy.call_args_list), 0)
979+
980+
def test_proceed_copy_with_no_overwrite_and_zero_byte_file_does_not_exist(
981+
self,
982+
):
983+
"""Testing warn_if_zero_byte_file_exists_with_no_overwrite handler when zero byte does not exist"""
984+
self.cli_params['no_overwrite'] = True
985+
fileinfo = FileInfo(
986+
src=self.source_bucket + "/" + self.source_key,
987+
dest=self.bucket + "/" + self.key,
988+
operation_name='copy',
989+
size=0,
990+
source_client=mock.Mock(),
991+
)
992+
fileinfo.source_client.head_object.side_effect = ClientError(
993+
{'Error': {'Code': '404', 'Message': 'Not Found'}},
994+
'HeadObject',
995+
)
996+
future = self.transfer_request_submitter.submit(fileinfo)
997+
# The transfer should proceed, so future should be the transfer manager's return value
998+
self.assertIs(self.transfer_manager.copy.return_value, future)
999+
self.assertEqual(len(self.transfer_manager.copy.call_args_list), 1)
1000+
1001+
def test_proceed_copy_with_no_overwrite_for_non_zero_byte_file(self):
1002+
self.cli_params['no_overwrite'] = True
1003+
fileinfo = FileInfo(
1004+
src=self.source_bucket + "/" + self.source_key,
1005+
dest=self.bucket + "/" + self.key,
1006+
operation_name='copy',
1007+
size=100,
1008+
source_client=mock.Mock(),
1009+
)
1010+
future = self.transfer_request_submitter.submit(fileinfo)
1011+
# The transfer should proceed, so future should be the transfer manager's return value
1012+
self.assertIs(self.transfer_manager.copy.return_value, future)
1013+
self.assertEqual(len(self.transfer_manager.copy.call_args_list), 1)
1014+
# Head should not be called when no_overwrite is false
1015+
fileinfo.source_client.head_object.assert_not_called()
1016+
1017+
def test_file_exists_without_no_overwrite(self):
1018+
self.cli_params['no_overwrite'] = False
1019+
fileinfo = FileInfo(
1020+
src=self.source_bucket + "/" + self.source_key,
1021+
dest=self.bucket + "/" + self.key,
1022+
operation_name='copy',
1023+
size=100,
1024+
source_client=mock.Mock(),
1025+
)
1026+
future = self.transfer_request_submitter.submit(fileinfo)
1027+
# The transfer should proceed, so future should be the transfer manager's return value
1028+
self.assertIs(self.transfer_manager.copy.return_value, future)
1029+
self.assertEqual(len(self.transfer_manager.copy.call_args_list), 1)
1030+
# Head should not be called when no_overwrite is false
1031+
fileinfo.source_client.head_object.assert_not_called()
1032+
9601033

9611034
class TestUploadStreamRequestSubmitter(BaseTransferRequestSubmitterTest):
9621035
def setUp(self):

0 commit comments

Comments
 (0)