|
42 | 42 | from urllib3._collections import HTTPHeaderDict |
43 | 43 |
|
44 | 44 | from minio import Minio |
| 45 | +from minio.checksum import Algorithm |
45 | 46 | from minio.commonconfig import ENABLED, REPLACE, CopySource, SnowballObject |
46 | 47 | from minio.datatypes import PostPolicy |
47 | 48 | from minio.deleteobjects import DeleteObject |
@@ -908,6 +909,111 @@ def test_negative_put_object_with_path_segment( # pylint: disable=invalid-name |
908 | 909 | _client.remove_bucket(bucket_name=bucket_name) |
909 | 910 |
|
910 | 911 |
|
| 912 | +def test_put_object_multipart_with_checksum( # pylint: disable=invalid-name |
| 913 | + log_entry): |
| 914 | + """Test put_object() multipart upload with checksum validation. |
| 915 | +
|
| 916 | + This test validates the AWS S3 compliant checksum implementation for |
| 917 | + multipart uploads: |
| 918 | + - CreateMultipartUpload receives algorithm header only (not values) |
| 919 | + - UploadPart includes checksum value headers |
| 920 | + - CompleteMultipartUpload includes checksums in XML body |
| 921 | + """ |
| 922 | + |
| 923 | + # Get a unique bucket_name and object_name |
| 924 | + bucket_name = _gen_bucket_name() |
| 925 | + object_name = f"{uuid4()}-checksum" |
| 926 | + object_name_sha256 = None # Initialize for cleanup |
| 927 | + # Use 6 MB to trigger multipart upload (> 5 MB threshold) |
| 928 | + length = 6 * MB |
| 929 | + |
| 930 | + log_entry["args"] = { |
| 931 | + "bucket_name": bucket_name, |
| 932 | + "object_name": object_name, |
| 933 | + "length": length, |
| 934 | + "data": "LimitedRandomReader(6 * MB)", |
| 935 | + "checksum": "Algorithm.CRC32C", |
| 936 | + } |
| 937 | + |
| 938 | + try: |
| 939 | + _client.make_bucket(bucket_name=bucket_name) |
| 940 | + |
| 941 | + # Upload with CRC32C checksum - triggers multipart upload |
| 942 | + reader = LimitedRandomReader(length) |
| 943 | + result = _client.put_object( |
| 944 | + bucket_name=bucket_name, |
| 945 | + object_name=object_name, |
| 946 | + data=reader, |
| 947 | + length=length, |
| 948 | + checksum=Algorithm.CRC32C, |
| 949 | + ) |
| 950 | + |
| 951 | + # Verify upload succeeded and returned valid result |
| 952 | + if not result.etag: |
| 953 | + raise ValueError("Upload did not return valid ETag") |
| 954 | + |
| 955 | + # Verify ETag indicates multipart upload (contains dash and part count) |
| 956 | + if '-' not in result.etag: |
| 957 | + raise ValueError( |
| 958 | + f"Expected multipart ETag (with dash), got: {result.etag}") |
| 959 | + |
| 960 | + # Stat the object to verify it exists and has correct size |
| 961 | + st_obj = _client.stat_object( |
| 962 | + bucket_name=bucket_name, |
| 963 | + object_name=object_name, |
| 964 | + ) |
| 965 | + |
| 966 | + if st_obj.size != length: |
| 967 | + raise ValueError( |
| 968 | + f"Size mismatch: expected {length}, got {st_obj.size}") |
| 969 | + |
| 970 | + # Test with SHA256 checksum algorithm |
| 971 | + object_name_sha256 = f"{uuid4()}-checksum-sha256" |
| 972 | + log_entry["args"]["object_name"] = object_name_sha256 |
| 973 | + log_entry["args"]["checksum"] = "Algorithm.SHA256" |
| 974 | + |
| 975 | + reader = LimitedRandomReader(length) |
| 976 | + result = _client.put_object( |
| 977 | + bucket_name=bucket_name, |
| 978 | + object_name=object_name_sha256, |
| 979 | + data=reader, |
| 980 | + length=length, |
| 981 | + checksum=Algorithm.SHA256, |
| 982 | + ) |
| 983 | + |
| 984 | + if not result.etag: |
| 985 | + raise ValueError("Upload with SHA256 did not return valid ETag") |
| 986 | + |
| 987 | + if '-' not in result.etag: |
| 988 | + raise ValueError( |
| 989 | + f"Expected multipart ETag for SHA256, got: {result.etag}") |
| 990 | + |
| 991 | + st_obj = _client.stat_object( |
| 992 | + bucket_name=bucket_name, |
| 993 | + object_name=object_name_sha256, |
| 994 | + ) |
| 995 | + |
| 996 | + if st_obj.size != length: |
| 997 | + raise ValueError( |
| 998 | + f"Size mismatch: expected {length}, got {st_obj.size}") |
| 999 | + |
| 1000 | + finally: |
| 1001 | + try: |
| 1002 | + _client.remove_object(bucket_name=bucket_name, object_name=object_name) |
| 1003 | + except: # pylint: disable=bare-except |
| 1004 | + pass |
| 1005 | + if object_name_sha256: |
| 1006 | + try: |
| 1007 | + _client.remove_object( |
| 1008 | + bucket_name=bucket_name, object_name=object_name_sha256) |
| 1009 | + except: # pylint: disable=bare-except |
| 1010 | + pass |
| 1011 | + try: |
| 1012 | + _client.remove_bucket(bucket_name=bucket_name) |
| 1013 | + except: # pylint: disable=bare-except |
| 1014 | + pass |
| 1015 | + |
| 1016 | + |
911 | 1017 | def _test_stat_object(log_entry, sse=None, version_check=False): |
912 | 1018 | """Test stat_object().""" |
913 | 1019 |
|
@@ -2393,6 +2499,7 @@ def main(): |
2393 | 2499 | test_copy_object_unmodified_since: None, |
2394 | 2500 | test_put_object: {"sse": ssec} if ssec else None, |
2395 | 2501 | test_negative_put_object_with_path_segment: None, |
| 2502 | + test_put_object_multipart_with_checksum: None, |
2396 | 2503 | test_stat_object: {"sse": ssec} if ssec else None, |
2397 | 2504 | test_stat_object_version: {"sse": ssec} if ssec else None, |
2398 | 2505 | test_get_object: {"sse": ssec} if ssec else None, |
|
0 commit comments