Skip to content

Commit a4b7187

Browse files
authored
S3: Allow in place copies when bucket versioning is enabled (#7303)
1 parent 7eb3c57 commit a4b7187

File tree

2 files changed

+55
-2
lines changed

2 files changed

+55
-2
lines changed

moto/s3/models.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ def __init__(
108108
storage: Optional[str] = "STANDARD",
109109
etag: Optional[str] = None,
110110
is_versioned: bool = False,
111-
version_id: str = "null",
111+
version_id: Optional[str] = None,
112112
max_buffer_size: Optional[int] = None,
113113
multipart: Optional["FakeMultipart"] = None,
114114
bucket_name: Optional[str] = None,
@@ -170,7 +170,7 @@ def safe_name(self, encoding_type: Optional[str] = None) -> str:
170170

171171
@property
172172
def version_id(self) -> str:
173-
return self._version_id
173+
return self._version_id or "null"
174174

175175
@property
176176
def value(self) -> bytes:
@@ -2674,6 +2674,7 @@ def copy_object(
26742674
mdirective == "REPLACE",
26752675
website_redirect_location,
26762676
bucket.encryption, # S3 will allow copy in place if the bucket has encryption configured
2677+
src_key._version_id and bucket.is_versioned,
26772678
)
26782679
):
26792680
raise CopyObjectMustChangeSomething

tests/test_s3/test_s3_copyobject.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -783,6 +783,58 @@ def test_copy_object_in_place_with_bucket_encryption():
783783
assert response["ServerSideEncryption"] == "AES256"
784784

785785

786+
@mock_aws
787+
def test_copy_object_in_place_with_versioning():
788+
# If a bucket has versioning enabled, it will allow copy in place
789+
client = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
790+
bucket_name = "testbucket"
791+
client.create_bucket(Bucket=bucket_name)
792+
key = "source-key"
793+
794+
response = client.put_object(
795+
Body=b"",
796+
Bucket=bucket_name,
797+
Key=key,
798+
)
799+
800+
response = client.put_bucket_versioning(
801+
Bucket=bucket_name,
802+
VersioningConfiguration={
803+
"MFADelete": "Disabled",
804+
"Status": "Enabled",
805+
},
806+
)
807+
assert response["ResponseMetadata"]["HTTPStatusCode"] == 200
808+
809+
response = client.put_object(
810+
Body=b"",
811+
Bucket=bucket_name,
812+
Key=key,
813+
)
814+
version_id = response["ResponseMetadata"]["HTTPHeaders"]["x-amz-version-id"]
815+
assert version_id and version_id != "null"
816+
817+
response = client.copy_object(
818+
Bucket=bucket_name,
819+
CopySource={"Bucket": bucket_name, "Key": key, "VersionId": version_id},
820+
Key=key,
821+
)
822+
assert response["ResponseMetadata"]["HTTPStatusCode"] == 200
823+
824+
response = client.copy_object(
825+
Bucket=bucket_name,
826+
CopySource={"Bucket": bucket_name, "Key": key, "VersionId": "null"},
827+
Key=key,
828+
)
829+
assert response["ResponseMetadata"]["HTTPStatusCode"] == 200
830+
831+
response = client.list_object_versions(
832+
Bucket=bucket_name,
833+
Prefix=key,
834+
)
835+
assert len(response["Versions"]) == 4
836+
837+
786838
@mock_aws
787839
@pytest.mark.parametrize(
788840
"algorithm",

0 commit comments

Comments
 (0)