Skip to content

Commit eb56a58

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 Resolved requested changes NIT change
1 parent 17098c9 commit eb56a58

File tree

7 files changed

+218
-140
lines changed

7 files changed

+218
-140
lines changed

awscli/customizations/s3/s3handler.py

Lines changed: 4 additions & 8 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),
@@ -464,9 +464,7 @@ def _warn_if_file_exists_with_no_overwrite(self, fileinfo):
464464
fileout = self._get_fileout(fileinfo)
465465
if os.path.exists(fileout):
466466
LOGGER.debug(
467-
"Warning: Skipping file %s as it already exists on %s",
468-
fileinfo.src,
469-
fileinfo.dest,
467+
f"warning: skipping {fileinfo.src} -> {fileinfo.dest}, file exists at destination"
470468
)
471469
return True
472470
return False
@@ -543,9 +541,7 @@ def _warn_if_zero_byte_file_exists_with_no_overwrite(self, fileinfo):
543541
try:
544542
client.head_object(Bucket=bucket, Key=key)
545543
LOGGER.debug(
546-
"Warning: Skipping file %s as it already exists on %s",
547-
fileinfo.src,
548-
fileinfo.dest,
544+
f"warning: skipping {fileinfo.src} -> {fileinfo.dest}, file exists at destination"
549545
)
550546
return True
551547
except ClientError as e:

awscli/customizations/s3/subcommands.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1143,7 +1143,7 @@ class SyncCommand(S3TransferCommand):
11431143
}
11441144
]
11451145
+ TRANSFER_ARGS
1146-
+ [METADATA, COPY_PROPS, METADATA_DIRECTIVE]
1146+
+ [METADATA, COPY_PROPS, METADATA_DIRECTIVE, NO_OVERWRITE]
11471147
)
11481148

11491149

@@ -1400,7 +1400,8 @@ def run(self):
14001400
self._client, self._source_client, self.parameters
14011401
)
14021402

1403-
s3_transfer_handler = S3TransferHandlerFactory(self.parameters)(
1403+
params = self._get_s3_handler_params()
1404+
s3_transfer_handler = S3TransferHandlerFactory(params)(
14041405
self._transfer_manager, result_queue
14051406
)
14061407

@@ -1509,6 +1510,16 @@ def _map_sse_c_params(self, request_parameters, paths_type):
15091510
},
15101511
)
15111512

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+
params = self.parameters.copy()
1519+
if self.cmd == 'sync':
1520+
params.pop('no_overwrite', None)
1521+
return params
1522+
15121523

15131524
# TODO: This class is fairly quirky in the sense that it is both a builder
15141525
# and a data object. In the future we should make the following refactorings
@@ -1572,6 +1583,7 @@ def add_paths(self, paths):
15721583
elif len(paths) == 1:
15731584
self.parameters['dest'] = paths[0]
15741585
self._validate_streaming_paths()
1586+
self._validate_no_overwrite_for_download_streaming()
15751587
self._validate_path_args()
15761588
self._validate_sse_c_args()
15771589
self._validate_not_s3_express_bucket_for_sync()
@@ -1602,7 +1614,6 @@ def _validate_streaming_paths(self):
16021614
self.parameters['is_stream'] = True
16031615
self.parameters['dir_op'] = False
16041616
self.parameters['only_show_errors'] = True
1605-
self._validate_streaming_no_overwrite_for_download_parameter()
16061617

16071618
def _validate_path_args(self):
16081619
# If we're using a mv command, you can't copy the object onto itself.
@@ -1825,15 +1836,18 @@ def _validate_sse_c_copy_source_for_paths(self):
18251836
'copy operations.'
18261837
)
18271838

1828-
def _validate_streaming_no_overwrite_for_download_parameter(self):
1839+
def _validate_no_overwrite_for_download_streaming(self):
18291840
"""
18301841
Validates that no-overwrite parameter is not used with streaming downloads.
18311842
18321843
Raises:
18331844
ParamValidationError: If no-overwrite is specified with a streaming download.
18341845
"""
1835-
params = self.parameters
1836-
if params['paths_type'] == 's3local' and params.get('no_overwrite'):
1846+
if (
1847+
self.parameters['is_stream']
1848+
and self.parameters.get('no_overwrite')
1849+
and self.parameters['dest'] == '-'
1850+
):
18371851
raise ParamValidationError(
18381852
"--no-overwrite parameter is not supported for "
18391853
"streaming downloads"

tests/functional/s3/test_cp_command.py

Lines changed: 13 additions & 50 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
@@ -550,7 +538,7 @@ def test_no_overwrite_flag_on_copy_when_large_object_does_not_exist_on_target(
550538
# Verify the IfNoneMatch condition was set in the CompleteMultipartUpload request
551539
self.assertEqual(self.operations_called[5][1]['IfNoneMatch'], '*')
552540

553-
def test_no_overwrite_flag_on_download_when_small_object_already_exists_at_target(
541+
def test_no_overwrite_flag_on_download_when_single_object_already_exists_at_target(
554542
self,
555543
):
556544
full_path = self.files.create_file('foo.txt', 'existing content')
@@ -566,7 +554,7 @@ def test_no_overwrite_flag_on_download_when_small_object_already_exists_at_targe
566554
with open(full_path) as f:
567555
self.assertEqual(f.read(), 'existing content')
568556

569-
def test_no_overwrite_flag_on_download_when_small_object_does_not_exist_at_target(
557+
def test_no_overwrite_flag_on_download_when_single_object_does_not_exist_at_target(
570558
self,
571559
):
572560
full_path = self.files.full_path('foo.txt')
@@ -584,40 +572,6 @@ def test_no_overwrite_flag_on_download_when_small_object_does_not_exist_at_targe
584572
with open(full_path) as f:
585573
self.assertEqual(f.read(), 'foo')
586574

587-
def test_no_overwrite_flag_on_download_when_large_object_exists_at_target(
588-
self,
589-
):
590-
full_path = self.files.create_file('foo.txt', 'existing content')
591-
cmdline = (
592-
f'{self.prefix} s3://bucket/foo.txt {full_path} --no-overwrite'
593-
)
594-
self.parsed_responses = [
595-
self.head_object_response(ContentLength=10 * (1024**2)),
596-
]
597-
self.run_cmd(cmdline, expected_rc=0)
598-
self.assertEqual(len(self.operations_called), 1)
599-
self.assertEqual(self.operations_called[0][0].name, 'HeadObject')
600-
with open(full_path) as f:
601-
self.assertEqual(f.read(), 'existing content')
602-
603-
def test_no_overwrite_flag_on_download_when_large_object_does_not_exist_at_target(
604-
self,
605-
):
606-
full_path = self.files.full_path('foo.txt')
607-
cmdline = (
608-
f'{self.prefix} s3://bucket/foo.txt {full_path} --no-overwrite'
609-
)
610-
self.parsed_responses = [
611-
self.head_object_response(ContentLength=10 * (1024**2)),
612-
self.get_object_response(),
613-
self.get_object_response(),
614-
]
615-
self.run_cmd(cmdline, expected_rc=0)
616-
self.assertEqual(len(self.operations_called), 3)
617-
self.assertEqual(self.operations_called[0][0].name, 'HeadObject')
618-
self.assertEqual(self.operations_called[1][0].name, 'GetObject')
619-
self.assertEqual(self.operations_called[2][0].name, 'GetObject')
620-
621575
def test_dryrun_download(self):
622576
self.parsed_responses = [self.head_object_response()]
623577
target = self.files.full_path('file.txt')
@@ -1496,6 +1450,15 @@ def test_streaming_download_error(self):
14961450
)
14971451
self.assertIn(error_message, stderr)
14981452

1453+
def test_no_overwrite_cannot_be_used_with_streaming_download(self):
1454+
command = "s3 cp s3://bucket/streaming.txt - --no-overwrite"
1455+
_, stderr, _ = self.run_cmd(command, expected_rc=252)
1456+
error_message = (
1457+
"--no-overwrite parameter is not supported for "
1458+
"streaming downloads"
1459+
)
1460+
self.assertIn(error_message, stderr)
1461+
14991462

15001463
class TestCpCommandWithRequesterPayer(BaseCPCommandTest):
15011464
def setUp(self):

tests/functional/s3/test_mv_command.py

Lines changed: 4 additions & 52 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)
@@ -575,7 +563,7 @@ def test_mv_no_overwrite_flag_when_large_object_does_not_exist_on_target(
575563
)
576564
self.assertEqual(self.operations_called[6][0].name, 'DeleteObject')
577565

578-
def test_no_overwrite_flag_on_mv_download_when_small_object_exists_at_target(
566+
def test_no_overwrite_flag_on_mv_download_when_single_object_exists_at_target(
579567
self,
580568
):
581569
full_path = self.files.create_file('foo.txt', 'existing content')
@@ -591,7 +579,7 @@ def test_no_overwrite_flag_on_mv_download_when_small_object_exists_at_target(
591579
with open(full_path) as f:
592580
self.assertEqual(f.read(), 'existing content')
593581

594-
def test_no_overwrite_flag_on_mv_download_when_small_object_does_not_exist_at_target(
582+
def test_no_overwrite_flag_on_mv_download_when_single_object_does_not_exist_at_target(
595583
self,
596584
):
597585
full_path = self.files.full_path('foo.txt')
@@ -611,42 +599,6 @@ def test_no_overwrite_flag_on_mv_download_when_small_object_does_not_exist_at_ta
611599
with open(full_path) as f:
612600
self.assertEqual(f.read(), 'foo')
613601

614-
def test_no_overwrite_flag_on_mv_download_when_large_object_exists_at_target(
615-
self,
616-
):
617-
full_path = self.files.create_file('foo.txt', 'existing content')
618-
cmdline = (
619-
f'{self.prefix} s3://bucket/foo.txt {full_path} --no-overwrite'
620-
)
621-
self.parsed_responses = [
622-
self.head_object_response(ContentLength=10 * (1024**2)),
623-
]
624-
self.run_cmd(cmdline, expected_rc=0)
625-
self.assertEqual(len(self.operations_called), 1)
626-
self.assertEqual(self.operations_called[0][0].name, 'HeadObject')
627-
with open(full_path) as f:
628-
self.assertEqual(f.read(), 'existing content')
629-
630-
def test_no_overwrite_flag_on_mv_download_when_large_object_does_not_exist_at_target(
631-
self,
632-
):
633-
full_path = self.files.full_path('foo.txt')
634-
cmdline = (
635-
f'{self.prefix} s3://bucket/foo.txt {full_path} --no-overwrite'
636-
)
637-
self.parsed_responses = [
638-
self.head_object_response(ContentLength=10 * (1024**2)),
639-
self.get_object_response(),
640-
self.get_object_response(),
641-
self.delete_object_response(),
642-
]
643-
self.run_cmd(cmdline, expected_rc=0)
644-
self.assertEqual(len(self.operations_called), 4)
645-
self.assertEqual(self.operations_called[0][0].name, 'HeadObject')
646-
self.assertEqual(self.operations_called[1][0].name, 'GetObject')
647-
self.assertEqual(self.operations_called[2][0].name, 'GetObject')
648-
self.assertEqual(self.operations_called[3][0].name, 'DeleteObject')
649-
650602

651603
class TestMvWithCRTClient(BaseCRTTransferClientTest):
652604
def test_upload_move_using_crt_client(self):

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+
{'ETag': '"c8afdb36c52cf4727836669019e69222"'},
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)