dabla commented on code in PR #67016:
URL: https://github.com/apache/airflow/pull/67016#discussion_r3957848833
##########
providers/microsoft/azure/src/airflow/providers/microsoft/azure/hooks/wasb.py:
##########
@@ -463,6 +475,98 @@ def download(
# TODO: rework the interface as it might also return Awaitable
return blob_client.download_blob(offset=offset, length=length,
**kwargs) # type: ignore[return-value]
+ def _sync_to_local_dir_delete_stale_local_files(
+ self, current_wasb_objects: list[Path], local_dir: Path
+ ) -> None:
+ current_wasb_keys = {key.resolve() for key in current_wasb_objects}
+
+ for item in local_dir.rglob("*"):
+ if item.is_file() and item.resolve() not in current_wasb_keys:
+ self.log.debug("Deleting stale local file: %s", item)
+ item.unlink()
+ for root, dirs, _ in os.walk(local_dir, topdown=False):
+ for d in dirs:
+ dir_path = os.path.join(root, d)
+ if not os.listdir(dir_path):
+ self.log.debug("Deleting stale empty directory: %s",
dir_path)
+ os.rmdir(dir_path)
+
+ def _sync_to_local_dir_if_changed(
+ self, container_name: str, blob: BlobProperties, local_target_path:
Path
+ ) -> None:
+ should_download = False
+ download_logs: list[str] = []
+ download_log_params: list[Any] = []
+
+ if not local_target_path.exists():
+ should_download = True
+ download_logs.append("Local file %s does not exist.")
+ download_log_params.append(local_target_path)
+ else:
+ local_stats = local_target_path.stat()
+ if blob.size != local_stats.st_size:
+ should_download = True
+ download_logs.append("Blob size (%s) and local file size (%s)
differ.")
+ download_log_params.extend([blob.size, local_stats.st_size])
+
+ blob_last_modified = blob.last_modified
+ if blob_last_modified and local_stats.st_mtime <
blob_last_modified.timestamp():
+ should_download = True
+ download_logs.append("Blob last modified (%s) and local file
last modified (%s) differ.")
+ download_log_params.extend([blob_last_modified.timestamp(),
local_stats.st_mtime])
+
+ if should_download:
+ self.get_file(
+ file_path=str(local_target_path),
+ container_name=container_name,
+ blob_name=blob.name,
+ )
+ download_logs.append("Downloaded %s to %s")
+ download_log_params.extend([blob.name,
local_target_path.as_posix()])
+ self.log.debug(" ".join(download_logs), *download_log_params)
+ else:
+ self.log.debug(
+ "Local file %s is up-to-date with blob %s. Skipping download.",
+ local_target_path.as_posix(),
+ blob.name,
+ )
+
+ def sync_to_local_dir(
+ self,
+ container_name: str,
+ local_dir: Path,
+ prefix: str | None = None,
+ delete_stale: bool = True,
+ ) -> None:
+ """Download files from an Azure Blob Storage container to a local
directory."""
+ self.log.debug("Downloading data from wasb://%s/%s to %s",
container_name, prefix, local_dir)
+
+ local_wasb_objects: list[Path] = []
+ container = self._get_container_client(container_name)
+ self.check_for_variable_type("container", container, ContainerClient)
+ container = cast("ContainerClient", container)
+
+ for blob in container.list_blobs(name_starts_with=prefix):
+ if blob.name.endswith("/"):
+ continue
+ blob_path = Path(blob.name)
+ if prefix:
+ local_target_path =
local_dir.joinpath(blob_path.relative_to(prefix))
Review Comment:
**[warning]** `list_blobs(name_starts_with=prefix)` is a raw string-prefix
match, not folder-aware, so it can pick up blobs outside the intended prefix
and crash the sync.
```python
for blob in container.list_blobs(name_starts_with=prefix):
...
if prefix:
local_target_path = local_dir.joinpath(blob_path.relative_to(prefix))
```
Azure's `name_starts_with` matches on the literal string, not a path segment
boundary. If `prefix="project1/dags"` and the container also has blobs under a
sibling path such as `project1/dags-archive/old_dag.py`, that blob's name also
starts with the string `"project1/dags"` and will be included in the listing.
`Path("project1/dags-archive/old_dag.py").relative_to("project1/dags")` then
raises `ValueError` (not a subpath), which will propagate out of
`sync_to_local_dir` and fail the whole bundle refresh — even though the
offending blob was never meant to be part of this bundle.
The bundle's own `check_for_prefix` validation (in
`WasbDagBundle._initialize`) passes `delimiter="/"` for exactly this reason,
but `sync_to_local_dir`'s own listing call does not. Consider normalizing the
prefix to end with `/` before listing (e.g. `prefix = prefix.rstrip("/") + "/"
if prefix else prefix`), or catching the mismatch defensively, so a same-prefix
sibling blob can't crash the whole sync.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]