Skip to content

Commit 2b8e96b

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
1 parent 818c2cc commit 2b8e96b

File tree

4 files changed

+112
-31
lines changed

4 files changed

+112
-31
lines changed

awscli/customizations/s3/subcommands.py

Lines changed: 14 additions & 3 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,17 @@ 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+
k: v for k, v in self.parameters.items() if k != 'no_overwrite'
1521+
}
1522+
return self.parameters
1523+
15131524

15141525
# TODO: This class is fairly quirky in the sense that it is both a builder
15151526
# and a data object. In the future we should make the following refactorings

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):

0 commit comments

Comments
 (0)