From eff29d46306a0c1f82aaba06d4ce30c6f60e8bd8 Mon Sep 17 00:00:00 2001 From: Kenneth Daily Date: Mon, 22 Jan 2024 16:50:06 -0800 Subject: [PATCH] Disable S3 Express support for s3 sync command --- .../next-release/bugfix-s3sync-98772.json | 5 +++ awscli/customizations/s3/subcommands.py | 16 +++++++++ tests/functional/s3/test_sync_command.py | 36 ++++++++++++++++++- 3 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 .changes/next-release/bugfix-s3sync-98772.json diff --git a/.changes/next-release/bugfix-s3sync-98772.json b/.changes/next-release/bugfix-s3sync-98772.json new file mode 100644 index 000000000000..cd1747b15af6 --- /dev/null +++ b/.changes/next-release/bugfix-s3sync-98772.json @@ -0,0 +1,5 @@ +{ + "type": "bugfix", + "category": "``s3 sync``", + "description": "Disable S3 Express support for s3 sync command" +} diff --git a/awscli/customizations/s3/subcommands.py b/awscli/customizations/s3/subcommands.py index 11aa463cd72b..3996e5307c07 100644 --- a/awscli/customizations/s3/subcommands.py +++ b/awscli/customizations/s3/subcommands.py @@ -15,6 +15,7 @@ import sys from botocore.client import Config +from botocore.utils import is_s3express_bucket from dateutil.parser import parse from dateutil.tz import tzlocal @@ -1199,6 +1200,21 @@ def add_paths(self, paths): self._validate_streaming_paths() self._validate_path_args() self._validate_sse_c_args() + self._validate_not_s3_express_bucket_for_sync() + + def _validate_not_s3_express_bucket_for_sync(self): + if self.cmd == 'sync' and \ + (self._is_s3express_path(self.parameters['src']) or + self._is_s3express_path(self.parameters['dest'])): + raise ValueError( + "Cannot use sync command with a directory bucket." + ) + + def _is_s3express_path(self, path): + if path.startswith("s3://"): + bucket = split_s3_bucket_key(path)[0] + return is_s3express_bucket(bucket) + return False def _validate_streaming_paths(self): self.parameters['is_stream'] = False diff --git a/tests/functional/s3/test_sync_command.py b/tests/functional/s3/test_sync_command.py index 2f4d3c3242a1..6bfd5498634a 100644 --- a/tests/functional/s3/test_sync_command.py +++ b/tests/functional/s3/test_sync_command.py @@ -17,7 +17,7 @@ from awscli.compat import six from awscli.customizations.s3.utils import relative_path -from awscli.testutils import mock +from awscli.testutils import mock, cd from tests.functional.s3 import ( BaseS3TransferCommandTest, BaseS3CLIRunnerTest, BaseCRTTransferClientTest ) @@ -467,3 +467,37 @@ def test_does_not_use_crt_client_for_copy_syncs(self): self.run_command(cmdline) self.assertEqual(self.get_crt_make_request_calls(), []) self.assert_no_remaining_botocore_responses() + +class TestSyncCommandWithS3Express(BaseS3TransferCommandTest): + prefix = 's3 sync ' + + def test_incompatible_with_sync_upload(self): + cmdline = '%s localdirectory/ s3://testdirectorybucket--usw2-az1--x-s3/' % self.prefix + stderr = self.run_cmd(cmdline, expected_rc=255)[1] + self.assertIn('Cannot use sync command with a directory bucket.', stderr) + + def test_incompatible_with_sync_download(self): + cmdline = '%s s3://testdirectorybucket--usw2-az1--x-s3/ localdirectory/' % self.prefix + stderr = self.run_cmd(cmdline, expected_rc=255)[1] + self.assertIn('Cannot use sync command with a directory bucket.', stderr) + + def test_incompatible_with_sync_copy(self): + cmdline = '%s s3://bucket/ s3://testdirectorybucket--usw2-az1--x-s3/' % self.prefix + stderr = self.run_cmd(cmdline, expected_rc=255)[1] + self.assertIn('Cannot use sync command with a directory bucket.', stderr) + + def test_incompatible_with_sync_with_delete(self): + cmdline = '%s s3://bucket/ s3://testdirectorybucket--usw2-az1--x-s3/ --delete' % self.prefix + stderr = self.run_cmd(cmdline, expected_rc=255)[1] + self.assertIn('Cannot use sync command with a directory bucket.', stderr) + + def test_compatible_with_sync_with_local_directory_like_directory_bucket(self): + self.parsed_responses = [ + {'Contents': []} + ] + cmdline = '%s s3://bucket/ testdirectorybucket--usw2-az1--x-s3/' % self.prefix + with cd(self.files.rootdir): + _, stderr, _ = self.run_cmd(cmdline) + # Just asserting that command validated and made an API call + self.assertEqual(len(self.operations_called), 1) + self.assertEqual(self.operations_called[0][0].name, 'ListObjectsV2')