This is an automated email from the ASF dual-hosted git repository.

shahar1 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new 565515c18c9 Fix GCSToGCSOperator ignoring replace=False for 
single-file copy (#66014)
565515c18c9 is described below

commit 565515c18c910a87b9efb3371f87e6e289e80ea0
Author: Taichi Nakashima (@taichi) <[email protected]>
AuthorDate: Wed May 6 01:45:37 2026 +0900

    Fix GCSToGCSOperator ignoring replace=False for single-file copy (#66014)
---
 .../providers/google/cloud/transfers/gcs_to_gcs.py | 19 +++++--
 .../unit/google/cloud/transfers/test_gcs_to_gcs.py | 62 ++++++++++++++++++++++
 2 files changed, 76 insertions(+), 5 deletions(-)

diff --git 
a/providers/google/src/airflow/providers/google/cloud/transfers/gcs_to_gcs.py 
b/providers/google/src/airflow/providers/google/cloud/transfers/gcs_to_gcs.py
index 823fef56021..12a75a5256c 100644
--- 
a/providers/google/src/airflow/providers/google/cloud/transfers/gcs_to_gcs.py
+++ 
b/providers/google/src/airflow/providers/google/cloud/transfers/gcs_to_gcs.py
@@ -416,11 +416,20 @@ class GCSToGCSOperator(BaseOperator):
         # and copy directly
         if len(objects) == 0 and prefix:
             if hook.exists(self.source_bucket, prefix):
-                uri = self._copy_single_object(
-                    hook=hook, source_object=prefix, 
destination_object=self.destination_object
-                )
-                if uri:
-                    result_uris.append(uri)
+                # `objects` may have been emptied by _ignore_existing_files; 
respect replace=False here too.
+                destination_object = self.destination_object or prefix
+                if not self.replace and hook.exists(self.destination_bucket, 
destination_object):
+                    self.log.info(
+                        "Skipped object %s; already exists in destination 
bucket %s.",
+                        destination_object,
+                        self.destination_bucket,
+                    )
+                else:
+                    uri = self._copy_single_object(
+                        hook=hook, source_object=prefix, 
destination_object=self.destination_object
+                    )
+                    if uri:
+                        result_uris.append(uri)
             elif self.source_object_required:
                 msg = f"{prefix} does not exist in bucket {self.source_bucket}"
                 self.log.warning(msg)
diff --git 
a/providers/google/tests/unit/google/cloud/transfers/test_gcs_to_gcs.py 
b/providers/google/tests/unit/google/cloud/transfers/test_gcs_to_gcs.py
index 45dcff4516b..1c1f4268a1b 100644
--- a/providers/google/tests/unit/google/cloud/transfers/test_gcs_to_gcs.py
+++ b/providers/google/tests/unit/google/cloud/transfers/test_gcs_to_gcs.py
@@ -677,6 +677,68 @@ class TestGoogleCloudStorageToCloudStorageOperator:
         ):
             operator.execute(None)
 
+    @mock.patch("airflow.providers.google.cloud.transfers.gcs_to_gcs.GCSHook")
+    def 
test_execute_replace_false_skips_single_file_when_destination_exists(self, 
mock_hook):
+        mock_hook.return_value.list.side_effect = [
+            [SOURCE_OBJECT_NO_WILDCARD],
+            [SOURCE_OBJECT_NO_WILDCARD],
+        ]
+        mock_hook.return_value.exists.return_value = True
+
+        operator = GCSToGCSOperator(
+            task_id=TASK_ID,
+            source_bucket=TEST_BUCKET,
+            source_object=SOURCE_OBJECT_NO_WILDCARD,
+            destination_bucket=DESTINATION_BUCKET,
+            destination_object=SOURCE_OBJECT_NO_WILDCARD,
+            replace=False,
+        )
+
+        operator.execute(None)
+
+        mock_hook.return_value.exists.assert_any_call(DESTINATION_BUCKET, 
SOURCE_OBJECT_NO_WILDCARD)
+        mock_hook.return_value.rewrite.assert_not_called()
+
+    @mock.patch("airflow.providers.google.cloud.transfers.gcs_to_gcs.GCSHook")
+    def 
test_execute_replace_false_copies_single_file_when_destination_missing(self, 
mock_hook):
+        mock_hook.return_value.list.side_effect = [[], []]
+        mock_hook.return_value.exists.side_effect = lambda bucket, _obj: 
bucket == TEST_BUCKET
+
+        operator = GCSToGCSOperator(
+            task_id=TASK_ID,
+            source_bucket=TEST_BUCKET,
+            source_object=SOURCE_OBJECT_NO_WILDCARD,
+            destination_bucket=DESTINATION_BUCKET,
+            destination_object=SOURCE_OBJECT_NO_WILDCARD,
+            replace=False,
+        )
+
+        operator.execute(None)
+
+        mock_hook.return_value.rewrite.assert_called_once_with(
+            TEST_BUCKET, SOURCE_OBJECT_NO_WILDCARD, DESTINATION_BUCKET, 
SOURCE_OBJECT_NO_WILDCARD
+        )
+
+    @mock.patch("airflow.providers.google.cloud.transfers.gcs_to_gcs.GCSHook")
+    def 
test_execute_replace_true_copies_single_file_even_when_destination_exists(self, 
mock_hook):
+        mock_hook.return_value.list.return_value = []
+        mock_hook.return_value.exists.return_value = True
+
+        operator = GCSToGCSOperator(
+            task_id=TASK_ID,
+            source_bucket=TEST_BUCKET,
+            source_object=SOURCE_OBJECT_NO_WILDCARD,
+            destination_bucket=DESTINATION_BUCKET,
+            destination_object=SOURCE_OBJECT_NO_WILDCARD,
+            replace=True,
+        )
+
+        operator.execute(None)
+
+        mock_hook.return_value.rewrite.assert_called_once_with(
+            TEST_BUCKET, SOURCE_OBJECT_NO_WILDCARD, DESTINATION_BUCKET, 
SOURCE_OBJECT_NO_WILDCARD
+        )
+
     @pytest.mark.parametrize(
         (
             "existing_objects",

Reply via email to