Skip to content

Commit 68fb87d

Browse files
author
Rakshil Modi
committed
Implemented No-overwrite for uploads using cp and mv command
Customizing error message test cases for no_overwrite for upload operations using cp command Code cleanup Test cases for move command Removed unwanted code changes code quality improvement
1 parent df9bac5 commit 68fb87d

File tree

7 files changed

+159
-4
lines changed

7 files changed

+159
-4
lines changed

awscli/customizations/s3/results.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from awscli.customizations.s3.subscribers import OnDoneFilteredSubscriber
2424
from awscli.customizations.s3.utils import WarningResult, human_readable_size
2525
from awscli.customizations.utils import uni_print
26+
from awscli.customizations.s3.utils import create_warning
2627

2728
LOGGER = logging.getLogger(__name__)
2829

@@ -123,7 +124,12 @@ def _on_failure(self, future, e):
123124
if isinstance(e, FatalError):
124125
error_result_cls = ErrorResult
125126
self._result_queue.put(error_result_cls(exception=e))
126-
else:
127+
elif self._is_precondition_failed(e):
128+
error_message = f"as it already exists on {self._dest}"
129+
warning = create_warning(self._src, error_message, True)
130+
self._result_queue.put(warning)
131+
return
132+
else :
127133
self._result_queue.put(
128134
FailureResult(
129135
transfer_type=self._transfer_type,
@@ -132,6 +138,11 @@ def _on_failure(self, future, e):
132138
exception=e,
133139
)
134140
)
141+
142+
def _is_precondition_failed(self, exception):
143+
"""Check if this is a PreconditionFailed error"""
144+
return (hasattr(exception, 'response') and
145+
exception.response.get('Error', {}).get('Code') == 'PreconditionFailed')
135146

136147

137148
class BaseResultHandler:

awscli/customizations/s3/subcommands.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -642,6 +642,16 @@
642642
),
643643
}
644644

645+
NO_OVERWRITE = {
646+
'name': 'no-overwrite',
647+
'action': 'store_true',
648+
'help_text': (
649+
"This flag prevents overwriting of files at the destination. With this flag, "
650+
"only the files not present at the destination will be transferred "
651+
"and others will be skipped during the operation."
652+
),
653+
}
654+
645655
TRANSFER_ARGS = [
646656
DRYRUN,
647657
QUIET,
@@ -1057,7 +1067,7 @@ class CpCommand(S3TransferCommand):
10571067
}
10581068
]
10591069
+ TRANSFER_ARGS
1060-
+ [METADATA, COPY_PROPS, METADATA_DIRECTIVE, EXPECTED_SIZE, RECURSIVE]
1070+
+ [METADATA, COPY_PROPS, METADATA_DIRECTIVE, EXPECTED_SIZE, RECURSIVE, NO_OVERWRITE]
10611071
)
10621072

10631073

@@ -1081,6 +1091,7 @@ class MvCommand(S3TransferCommand):
10811091
METADATA_DIRECTIVE,
10821092
RECURSIVE,
10831093
VALIDATE_SAME_S3_PATHS,
1094+
NO_OVERWRITE,
10841095
]
10851096
)
10861097

awscli/customizations/s3/utils.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,7 @@ def map_put_object_params(cls, request_params, cli_params):
489489
cls._set_sse_c_request_params(request_params, cli_params)
490490
cls._set_request_payer_param(request_params, cli_params)
491491
cls._set_checksum_algorithm_param(request_params, cli_params)
492+
cls._set_no_overwrite_param(request_params, cli_params)
492493

493494
@classmethod
494495
def map_get_object_params(cls, request_params, cli_params):
@@ -557,6 +558,12 @@ def map_delete_object_params(cls, request_params, cli_params):
557558
@classmethod
558559
def map_list_objects_v2_params(cls, request_params, cli_params):
559560
cls._set_request_payer_param(request_params, cli_params)
561+
562+
@classmethod
563+
def _set_no_overwrite_param(cls, request_params, cli_params):
564+
"""Mapping No overwrite header with IfNoneMatch"""
565+
if cli_params.get('no_overwrite'):
566+
request_params['IfNoneMatch'] = "*"
560567

561568
@classmethod
562569
def _set_request_payer_param(cls, request_params, cli_params):

awscli/s3transfer/manager.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ class TransferManager:
195195
+ [
196196
'ChecksumType',
197197
'MpuObjectSize',
198+
'IfNoneMatch',
198199
]
199200
+ FULL_OBJECT_CHECKSUM_ARGS
200201
)

awscli/s3transfer/upload.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -515,7 +515,7 @@ class UploadSubmissionTask(SubmissionTask):
515515

516516
PUT_OBJECT_BLOCKLIST = ["ChecksumType", "MpuObjectSize"]
517517

518-
CREATE_MULTIPART_BLOCKLIST = FULL_OBJECT_CHECKSUM_ARGS + ["MpuObjectSize"]
518+
CREATE_MULTIPART_BLOCKLIST = FULL_OBJECT_CHECKSUM_ARGS + ["MpuObjectSize", "IfNoneMatch"]
519519

520520
UPLOAD_PART_ARGS = [
521521
'ChecksumAlgorithm',
@@ -534,6 +534,7 @@ class UploadSubmissionTask(SubmissionTask):
534534
'ExpectedBucketOwner',
535535
'ChecksumType',
536536
'MpuObjectSize',
537+
'IfNoneMatch',
537538
] + FULL_OBJECT_CHECKSUM_ARGS
538539

539540
def _get_upload_input_manager_cls(self, transfer_future):

tests/functional/s3/test_cp_command.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,68 @@ def test_operations_used_in_recursive_download(self):
295295
len(self.operations_called), 1, self.operations_called
296296
)
297297
self.assertEqual(self.operations_called[0][0].name, 'ListObjectsV2')
298+
299+
def test_for_no_overwrite_flag_when_object_not_exists_on_target(self):
300+
"""Testing when object with different key is successsfully uploaded using no-overwrite"""
301+
full_path = self.files.create_file('foo.txt', 'mycontent')
302+
cmdline = '%s %s s3://bucket --no-overwrite' % ( self.prefix,full_path)
303+
self.parsed_responses = [
304+
{'ETag': '"c8afdb36c52cf4727836669019e69222"'}
305+
]
306+
self.run_cmd(cmdline, expected_rc=0)
307+
#Verify putObject was called
308+
self.assertEqual(
309+
len(self.operations_called), 1, self.operations_called
310+
)
311+
self.assertEqual(self.operations_called[0][0].name, 'PutObject')
312+
# Verify the IfNoneMatch condition was set in the request
313+
self.assertEqual(self.operations_called[0][1]['IfNoneMatch'], '*')
314+
315+
def test_for_no_overwrite_flag_when_object_exists_on_target(self):
316+
"""Testing when object already exists using no-overwrite"""
317+
full_path = self.files.create_file('foo.txt', 'mycontent')
318+
cmdline = '%s %s s3://bucket --no-overwrite' % ( self.prefix,full_path)
319+
# Set up the response to simulate a PreconditionFailed error
320+
self.http_response.status_code = 412
321+
self.parsed_responses = [
322+
{
323+
'Error': {
324+
'Code': 'PreconditionFailed',
325+
'Message': 'At least one of the pre-conditions you specified did not hold'
326+
}
327+
}
328+
]
329+
stdout, stderr, rc = self.run_cmd(cmdline, expected_rc=2) #Checking for warning
330+
# Verify PutObject was attempted with IfNoneMatch
331+
self.assertEqual(len(self.operations_called), 1)
332+
self.assertEqual(self.operations_called[0][0].name, 'PutObject')
333+
self.assertEqual(self.operations_called[0][1]['IfNoneMatch'], '*')
334+
335+
def test_for_no_overwrite_flag_multipart_upload_when_object_not_exists_on_target(self):
336+
"""Testing multipart upload with no-overwrite flag when object doesn't exist"""
337+
# Create a large file that will trigger multipart upload
338+
full_path = self.files.create_file('foo.txt', 'a' * 10 * (1024**2))
339+
cmdline = '%s %s s3://bucket --no-overwrite' % (self.prefix, full_path)
340+
341+
# Set up responses for multipart upload
342+
self.parsed_responses = [
343+
{'UploadId': 'foo'}, # CreateMultipartUpload response
344+
{'ETag': '"foo-1"'}, # UploadPart response
345+
{'ETag': '"foo-2"'}, # UploadPart response
346+
{} # CompleteMultipartUpload response
347+
]
348+
349+
self.run_cmd(cmdline, expected_rc=0)
350+
351+
# Verify all multipart operations were called
352+
self.assertEqual(len(self.operations_called), 4)
353+
self.assertEqual(self.operations_called[0][0].name, 'CreateMultipartUpload')
354+
self.assertEqual(self.operations_called[1][0].name, 'UploadPart')
355+
self.assertEqual(self.operations_called[2][0].name, 'UploadPart')
356+
self.assertEqual(self.operations_called[3][0].name, 'CompleteMultipartUpload')
357+
358+
# Verify the IfNoneMatch condition was set in the CompleteMultipartUpload request
359+
self.assertEqual(self.operations_called[3][1]['IfNoneMatch'], '*')
298360

299361
def test_dryrun_download(self):
300362
self.parsed_responses = [self.head_object_response()]

tests/functional/s3/test_mv_command.py

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,69 @@ def test_download_with_checksum_mode_crc32(self):
315315
self.assertEqual(
316316
self.operations_called[1][1]['ChecksumMode'], 'ENABLED'
317317
)
318-
318+
319+
def test_mv_no_overwrite_flag_when_object_not_exists_on_target(self):
320+
"""Testing mv when object doesnt exist using no-overwrite"""
321+
full_path = self.files.create_file('foo.txt', 'contents')
322+
cmdline = f'{self.prefix} {full_path} s3://bucket --no-overwrite'
323+
self.run_cmd(cmdline, expected_rc=0)
324+
# Verify putObject was called
325+
self.assertEqual(len(self.operations_called),1)
326+
self.assertEqual(self.operations_called[0][0].name, 'PutObject')
327+
# Verify the IfNoneMatch condition was set in the request
328+
self.assertEqual(self.operations_called[0][1]['IfNoneMatch'], '*')
329+
# Verify source file was deleted (move operation)
330+
self.assertFalse(os.path.exists(full_path))
331+
332+
def test_mv_no_overwrite_flag_when_object_exists_on_target(self):
333+
"""Testing mv when object already exists using no-overwrite"""
334+
full_path = self.files.create_file('foo.txt', 'mycontent')
335+
cmdline = '%s %s s3://bucket/foo.txt --no-overwrite' % (self.prefix, full_path)
336+
# Set up the response to simulate a PreconditionFailed error
337+
self.http_response.status_code = 412
338+
self.parsed_responses = [
339+
{
340+
'Error': {
341+
'Code': 'PreconditionFailed',
342+
'Message': 'At least one of the pre-conditions you specified did not hold'
343+
}
344+
}
345+
]
346+
stdout, stderr, rc = self.run_cmd(cmdline, expected_rc=2)
347+
# Verify PutObject was attempted with IfNoneMatch
348+
self.assertEqual(len(self.operations_called), 1)
349+
self.assertEqual(self.operations_called[0][0].name, 'PutObject')
350+
self.assertEqual(self.operations_called[0][1]['IfNoneMatch'], '*')
351+
# Verify source file was not deleted
352+
self.assertTrue(os.path.exists(full_path))
353+
354+
def test_mv_no_overwrite_flag_multipart_upload_when_object_not_exists_on_target(self):
355+
"""Testing mv multipart upload with no-overwrite flag when object doesn't exist"""
356+
# Create a large file that will trigger multipart upload
357+
full_path = self.files.create_file('foo.txt', 'a' * 10 * (1024**2))
358+
cmdline = '%s %s s3://bucket/foo.txt --no-overwrite' % (self.prefix, full_path)
359+
360+
# Set up responses for multipart upload
361+
self.parsed_responses = [
362+
{'UploadId': 'foo'}, # CreateMultipartUpload response
363+
{'ETag': '"foo-1"'}, # UploadPart response
364+
{'ETag': '"foo-2"'}, # UploadPart response
365+
{} # CompleteMultipartUpload response
366+
]
367+
368+
self.run_cmd(cmdline, expected_rc=0)
369+
370+
# Verify all multipart operations were called
371+
self.assertEqual(len(self.operations_called), 4)
372+
self.assertEqual(self.operations_called[0][0].name, 'CreateMultipartUpload')
373+
self.assertEqual(self.operations_called[1][0].name, 'UploadPart')
374+
self.assertEqual(self.operations_called[2][0].name, 'UploadPart')
375+
self.assertEqual(self.operations_called[3][0].name, 'CompleteMultipartUpload')
376+
377+
# Verify the IfNoneMatch condition was set in the CompleteMultipartUpload request
378+
self.assertEqual(self.operations_called[3][1]['IfNoneMatch'], '*')
379+
# Verify source file was deleted (successful move operation)
380+
self.assertFalse(os.path.exists(full_path))
319381

320382
class TestMvWithCRTClient(BaseCRTTransferClientTest):
321383
def test_upload_move_using_crt_client(self):

0 commit comments

Comments
 (0)