Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-s3transfer for 
openSUSE:Factory checked in at 2025-11-24 14:14:26
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-s3transfer (Old)
 and      /work/SRC/openSUSE:Factory/.python-s3transfer.new.14147 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-s3transfer"

Mon Nov 24 14:14:26 2025 rev:41 rq:1319689 version:0.15.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-s3transfer/python-s3transfer.changes      
2025-09-18 21:08:01.788778227 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-s3transfer.new.14147/python-s3transfer.changes
   2025-11-24 14:16:58.956993191 +0100
@@ -1,0 +2,6 @@
+Mon Nov 24 09:05:47 UTC 2025 - John Paul Adrian Glaubitz 
<[email protected]>
+
+- Update to version 0.15.0
+  * feature:``CopyPartTask``: Validate ETag of stored object during multipart 
copies
+
+-------------------------------------------------------------------

Old:
----
  s3transfer-0.14.0.tar.gz

New:
----
  s3transfer-0.15.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-s3transfer.spec ++++++
--- /var/tmp/diff_new_pack.VzwGw4/_old  2025-11-24 14:16:59.557018400 +0100
+++ /var/tmp/diff_new_pack.VzwGw4/_new  2025-11-24 14:16:59.557018400 +0100
@@ -18,7 +18,7 @@
 
 %{?sle15_python_module_pythons}
 Name:           python-s3transfer
-Version:        0.14.0
+Version:        0.15.0
 Release:        0
 Summary:        Python S3 transfer manager
 License:        Apache-2.0

++++++ s3transfer-0.14.0.tar.gz -> s3transfer-0.15.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/s3transfer-0.14.0/PKG-INFO 
new/s3transfer-0.15.0/PKG-INFO
--- old/s3transfer-0.14.0/PKG-INFO      2025-09-09 20:09:31.783774000 +0200
+++ new/s3transfer-0.15.0/PKG-INFO      2025-11-20 20:13:52.397613000 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: s3transfer
-Version: 0.14.0
+Version: 0.15.0
 Summary: An Amazon S3 Transfer Manager
 Home-page: https://github.com/boto/s3transfer
 Author: Amazon Web Services
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/s3transfer-0.14.0/s3transfer/__init__.py 
new/s3transfer-0.15.0/s3transfer/__init__.py
--- old/s3transfer-0.14.0/s3transfer/__init__.py        2025-09-09 
20:09:31.000000000 +0200
+++ new/s3transfer-0.15.0/s3transfer/__init__.py        2025-11-20 
20:13:52.000000000 +0100
@@ -146,7 +146,7 @@
 from s3transfer.exceptions import RetriesExceededError, S3UploadFailedError
 
 __author__ = 'Amazon Web Services'
-__version__ = '0.14.0'
+__version__ = '0.15.0'
 
 
 logger = logging.getLogger(__name__)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/s3transfer-0.14.0/s3transfer/copies.py 
new/s3transfer-0.15.0/s3transfer/copies.py
--- old/s3transfer-0.14.0/s3transfer/copies.py  2025-09-09 20:04:48.000000000 
+0200
+++ new/s3transfer-0.15.0/s3transfer/copies.py  2025-11-20 20:13:52.000000000 
+0100
@@ -13,6 +13,9 @@
 import copy
 import math
 
+from botocore.exceptions import ClientError
+
+from s3transfer.exceptions import S3CopyFailedError
 from s3transfer.tasks import (
     CompleteMultipartUploadTask,
     CreateMultipartUploadTask,
@@ -98,8 +101,10 @@
         :param transfer_future: The transfer future associated with the
             transfer request that tasks are being submitted for
         """
-        # Determine the size if it was not provided
-        if transfer_future.meta.size is None:
+        if (
+            transfer_future.meta.size is None
+            or transfer_future.meta.etag is None
+        ):
             # If a size was not provided figure out the size for the
             # user. Note that we will only use the client provided to
             # the TransferManager. If the object is outside of the region
@@ -127,6 +132,9 @@
             transfer_future.meta.provide_transfer_size(
                 response['ContentLength']
             )
+            # Provide an etag to ensure a stored object is not modified
+            # during a multipart copy.
+            transfer_future.meta.provide_object_etag(response.get('ETag'))
 
         # If it is greater than threshold do a multipart copy, otherwise
         # do a regular copy object.
@@ -218,6 +226,10 @@
                 num_parts,
                 transfer_future.meta.size,
             )
+            if transfer_future.meta.etag is not None:
+                extra_part_args['CopySourceIfMatch'] = (
+                    transfer_future.meta.etag
+                )
             # Get the size of the part copy as well for the progress
             # callbacks.
             size = self._get_transfer_size(
@@ -367,14 +379,27 @@
             the multipart upload. If a checksum is in the response,
             it will also be included.
         """
-        response = client.upload_part_copy(
-            CopySource=copy_source,
-            Bucket=bucket,
-            Key=key,
-            UploadId=upload_id,
-            PartNumber=part_number,
-            **extra_args,
-        )
+        try:
+            response = client.upload_part_copy(
+                CopySource=copy_source,
+                Bucket=bucket,
+                Key=key,
+                UploadId=upload_id,
+                PartNumber=part_number,
+                **extra_args,
+            )
+        except ClientError as e:
+            error_code = e.response.get('Error', {}).get('Code')
+            src_key = copy_source['Key']
+            src_bucket = copy_source['Bucket']
+            if error_code == "PreconditionFailed":
+                raise S3CopyFailedError(
+                    f'Contents of stored object "{src_key}" '
+                    f'in bucket "{src_bucket}" did not match '
+                    'expected ETag.'
+                )
+            else:
+                raise
         for callback in callbacks:
             callback(bytes_transferred=size)
         etag = response['CopyPartResult']['ETag']
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/s3transfer-0.14.0/s3transfer/exceptions.py 
new/s3transfer-0.15.0/s3transfer/exceptions.py
--- old/s3transfer-0.14.0/s3transfer/exceptions.py      2025-09-09 
20:09:31.000000000 +0200
+++ new/s3transfer-0.15.0/s3transfer/exceptions.py      2025-11-20 
20:13:52.000000000 +0100
@@ -27,6 +27,10 @@
     pass
 
 
+class S3CopyFailedError(Exception):
+    pass
+
+
 class InvalidSubscriberMethodError(Exception):
     pass
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/s3transfer-0.14.0/s3transfer.egg-info/PKG-INFO 
new/s3transfer-0.15.0/s3transfer.egg-info/PKG-INFO
--- old/s3transfer-0.14.0/s3transfer.egg-info/PKG-INFO  2025-09-09 
20:09:31.000000000 +0200
+++ new/s3transfer-0.15.0/s3transfer.egg-info/PKG-INFO  2025-11-20 
20:13:52.000000000 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: s3transfer
-Version: 0.14.0
+Version: 0.15.0
 Summary: An Amazon S3 Transfer Manager
 Home-page: https://github.com/boto/s3transfer
 Author: Amazon Web Services
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/s3transfer-0.14.0/tests/functional/test_copy.py 
new/s3transfer-0.15.0/tests/functional/test_copy.py
--- old/s3transfer-0.14.0/tests/functional/test_copy.py 2025-09-09 
20:04:48.000000000 +0200
+++ new/s3transfer-0.15.0/tests/functional/test_copy.py 2025-11-20 
20:13:52.000000000 +0100
@@ -10,12 +10,15 @@
 # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
 # ANY KIND, either express or implied. See the License for the specific
 # language governing permissions and limitations under the License.
+import copy
+
 from botocore.exceptions import ClientError
 from botocore.stub import Stubber
 
+from s3transfer.exceptions import S3CopyFailedError
 from s3transfer.manager import TransferConfig, TransferManager
 from s3transfer.utils import MIN_UPLOAD_CHUNKSIZE
-from tests import BaseGeneralInterfaceTest, FileSizeProvider
+from tests import BaseGeneralInterfaceTest, ETagProvider, FileSizeProvider
 
 
 class BaseCopyTest(BaseGeneralInterfaceTest):
@@ -31,6 +34,7 @@
         # Initialize some default arguments
         self.bucket = 'mybucket'
         self.key = 'mykey'
+        self.etag = 'myetag'
         self.copy_source = {'Bucket': 'mysourcebucket', 'Key': 'mysourcekey'}
         self.extra_args = {}
         self.subscribers = []
@@ -122,7 +126,10 @@
         self.add_successful_copy_responses()
 
         call_kwargs = self.create_call_kwargs()
-        call_kwargs['subscribers'] = [FileSizeProvider(len(self.content))]
+        call_kwargs['subscribers'] = [
+            FileSizeProvider(len(self.content)),
+            ETagProvider(self.etag),
+        ]
 
         future = self.manager.copy(**call_kwargs)
         future.result()
@@ -330,7 +337,10 @@
         return [
             {
                 'method': 'head_object',
-                'service_response': {'ContentLength': len(self.content)},
+                'service_response': {
+                    'ContentLength': len(self.content),
+                    'ETag': self.etag,
+                },
             },
             {
                 'method': 'create_multipart_upload',
@@ -392,6 +402,7 @@
                 'UploadId': self.multipart_id,
                 'PartNumber': i + 1,
                 'CopySourceRange': range_val,
+                'CopySourceIfMatch': self.etag,
             }
             if extra_expected_params:
                 if 'ChecksumAlgorithm' in extra_expected_params:
@@ -470,6 +481,7 @@
                     'UploadId': self.multipart_id,
                     'PartNumber': i + 1,
                     'CopySourceRange': range_val,
+                    'CopySourceIfMatch': self.etag,
                 }
             )
 
@@ -700,3 +712,41 @@
         )
         future.result()
         self.stubber.assert_no_pending_responses()
+
+    def test_copy_fails_if_etag_validation_fails(self):
+        expected_params = {
+            'Bucket': 'mybucket',
+            'Key': 'mykey',
+            'CopySource': {'Bucket': 'mysourcebucket', 'Key': 'mysourcekey'},
+            'CopySourceIfMatch': self.etag,
+            'UploadId': self.multipart_id,
+        }
+        self.add_get_head_response_with_default_expected_params()
+        self.add_create_multipart_response_with_default_expected_params()
+        expected_ranges = ['bytes=0-5242879', 'bytes=5242880-10485759']
+        for i, stubbed_response in enumerate(
+            self.create_stubbed_responses()[2:4]
+        ):
+            stubbed_response['expected_params'] = copy.deepcopy(
+                expected_params
+            )
+            stubbed_response['expected_params']['CopySourceRange'] = (
+                expected_ranges[i]
+            )
+            stubbed_response['expected_params']['PartNumber'] = i + 1
+            self.stubber.add_response(**stubbed_response)
+        # Simulate ETag validation failure by adding a
+        # client error for the last UploadCopyPart request.
+        self.stubber.add_client_error(
+            method='upload_part_copy',
+            service_error_code='PreconditionFailed',
+            service_message=(
+                'At least one of the pre-conditions you specified did not hold'
+            ),
+            http_status_code=412,
+        )
+
+        future = self.manager.copy(**self.create_call_kwargs())
+        with self.assertRaises(S3CopyFailedError) as e:
+            future.result()
+        self.assertIn('did not match expected ETag', str(e.exception))

Reply via email to