Skip to content

Commit c3ef8af

Browse files
committed
Fix silent overwrite of colliding blobs when replace=True
1 parent ff92efa commit c3ef8af

2 files changed

Lines changed: 45 additions & 19 deletions

File tree

providers/microsoft/azure/src/airflow/providers/microsoft/azure/transfers/gcs_to_wasb.py

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -188,33 +188,37 @@ def execute(self, context: Context) -> list[str]:
188188
if not self.keep_directory_structure and self.prefix and not self.flatten_structure:
189189
blob_prefix = os.path.join(blob_prefix, self.prefix)
190190

191+
existing_blobs_set: set[str] = set()
191192
if not self.replace:
192-
existing_blobs = wasb_hook.get_blobs_list_recursive(
193-
container_name=self.container_name,
194-
prefix=blob_prefix or None,
193+
existing_blobs = (
194+
wasb_hook.get_blobs_list_recursive(
195+
container_name=self.container_name,
196+
prefix=blob_prefix or None,
197+
)
198+
or []
195199
)
196-
existing_blobs = existing_blobs or []
197200
if blob_prefix:
198201
prefix_str = blob_prefix.rstrip("/") + "/"
199202
existing_blobs = [b.removeprefix(prefix_str) for b in existing_blobs]
200-
201203
existing_blobs_set = set(existing_blobs)
202-
filtered_files = []
203-
seen_transformed = set()
204204

205-
for file in gcs_files:
206-
transformed = self._transform_file_path(file)
207-
if transformed not in existing_blobs_set and transformed not in seen_transformed:
208-
filtered_files.append(file)
209-
seen_transformed.add(transformed)
210-
elif transformed in seen_transformed:
211-
self.log.warning(
212-
"Skipping duplicate file %s (transforms to %s)",
213-
file,
214-
transformed,
215-
)
205+
filtered_files: list[str] = []
206+
seen_transformed: set[str] = set()
207+
for file in gcs_files:
208+
transformed = self._transform_file_path(file)
209+
if transformed in existing_blobs_set:
210+
continue
211+
if transformed in seen_transformed:
212+
self.log.warning(
213+
"Skipping duplicate file %s (transforms to %s)",
214+
file,
215+
transformed,
216+
)
217+
continue
218+
filtered_files.append(file)
219+
seen_transformed.add(transformed)
216220

217-
gcs_files = filtered_files
221+
gcs_files = filtered_files
218222

219223
uploaded_blobs: list[str] = []
220224
if gcs_files:

providers/microsoft/azure/tests/unit/microsoft/azure/transfers/test_gcs_to_wasb.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,28 @@ def test_flatten_structure(self, mock_gcs_hook, mock_wasb_hook):
182182
assert str(kw["blob_name"]).replace("\\", "/") == _expected_blob_paths(BLOB_PREFIX, "a.txt")[0]
183183
assert kw["overwrite"] is True
184184

185+
@mock.patch("airflow.providers.microsoft.azure.transfers.gcs_to_wasb.WasbHook")
186+
@mock.patch("airflow.providers.microsoft.azure.transfers.gcs_to_wasb.GCSHook")
187+
def test_execute_dedups_collisions_when_replace_true(self, mock_gcs_hook, mock_wasb_hook):
188+
mock_gcs_hook.return_value.list.return_value = ["a/file.txt", "b/file.txt"]
189+
mock_file = mock.Mock()
190+
mock_file.name = "/tmp/local"
191+
mock_gcs_hook.return_value.provide_file.return_value.__enter__ = mock.Mock(return_value=mock_file)
192+
mock_gcs_hook.return_value.provide_file.return_value.__exit__ = mock.Mock(return_value=None)
193+
194+
op = GCSToAzureBlobStorageOperator(
195+
task_id=TASK_ID,
196+
gcs_bucket=GCS_BUCKET,
197+
container_name=CONTAINER,
198+
blob_prefix=BLOB_PREFIX,
199+
replace=True,
200+
flatten_structure=True,
201+
)
202+
result = op.execute(context=None)
203+
204+
assert _norm_paths(result) == _expected_blob_paths(BLOB_PREFIX, "file.txt")
205+
mock_wasb_hook.return_value.load_file.assert_called_once()
206+
185207
@mock.patch("airflow.providers.microsoft.azure.transfers.gcs_to_wasb.WasbHook")
186208
@mock.patch("airflow.providers.microsoft.azure.transfers.gcs_to_wasb.GCSHook")
187209
def test_execute_is_idempotent_on_retry(self, mock_gcs_hook, mock_wasb_hook):

0 commit comments

Comments
 (0)