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

sbp pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-release.git


The following commit(s) were added to refs/heads/main by this push:
     new 7ab7261  Add functions for cloning a directory with hard links and 
setting a symlink atomically
7ab7261 is described below

commit 7ab7261182418c47a42f5e388141a48ab9666365
Author: Sean B. Palmer <[email protected]>
AuthorDate: Mon Apr 7 19:14:35 2025 +0100

    Add functions for cloning a directory with hard links and setting a symlink 
atomically
---
 atr/util.py | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 58 insertions(+)

diff --git a/atr/util.py b/atr/util.py
index 007fee3..9a87374 100644
--- a/atr/util.py
+++ b/atr/util.py
@@ -20,9 +20,11 @@ import binascii
 import contextlib
 import dataclasses
 import hashlib
+import logging
 import pathlib
 import shutil
 import tempfile
+import uuid
 from collections.abc import AsyncGenerator, Callable, Mapping, Sequence
 from typing import Annotated, Any, TypeVar
 
@@ -41,6 +43,8 @@ import atr.db.models as models
 F = TypeVar("F", bound="QuartFormTyped")
 T = TypeVar("T")
 
+_LOGGER = logging.getLogger(__name__)
+
 
 async def get_asf_id_or_die() -> str:
     web_session = await session.read()
@@ -143,6 +147,34 @@ async def content_list(phase_subdir: pathlib.Path, 
project_name: str, version_na
         )
 
 
+async def create_hard_link_clone(source_dir: pathlib.Path, dest_dir: 
pathlib.Path) -> None:
+    """Recursively create a clone of source_dir in dest_dir using hard links 
for files."""
+    # Ensure source exists and is a directory
+    if not await aiofiles.os.path.isdir(source_dir):
+        raise ValueError(f"Source path is not a directory or does not exist: 
{source_dir}")
+
+    # Create destination directory
+    await aiofiles.os.makedirs(dest_dir, exist_ok=False)
+
+    async def _clone_recursive(current_source: pathlib.Path, current_dest: 
pathlib.Path) -> None:
+        for entry in await aiofiles.os.scandir(current_source):
+            source_entry_path = current_source / entry.name
+            dest_entry_path = current_dest / entry.name
+
+            try:
+                if entry.is_dir():
+                    await aiofiles.os.makedirs(dest_entry_path, exist_ok=True)
+                    await _clone_recursive(source_entry_path, dest_entry_path)
+                elif entry.is_file():
+                    await aiofiles.os.link(source_entry_path, dest_entry_path)
+                # Ignore other types like symlinks for now
+            except OSError as e:
+                _LOGGER.error(f"Error cloning {source_entry_path} to 
{dest_entry_path}: {e}")
+                raise
+
+    await _clone_recursive(source_dir, dest_dir)
+
+
 async def file_sha3(path: str) -> str:
     """Compute SHA3-256 hash of a file."""
     sha3 = hashlib.sha3_256()
@@ -238,6 +270,32 @@ def unwrap(value: T | None, error_message: str = 
"unexpected None when unwrappin
         return value
 
 
+async def update_atomic_symlink(link_path: pathlib.Path, target_path: 
pathlib.Path | str) -> None:
+    """Atomically update or create a symbolic link at link_path pointing to 
target_path."""
+    target_str = str(target_path)
+
+    # Generate a temporary path name for the new link
+    link_dir = link_path.parent
+    temp_link_path = link_dir / f".{link_path.name}.{uuid.uuid4()}.tmp"
+
+    try:
+        await aiofiles.os.symlink(target_str, temp_link_path)
+        # Atomically rename the temporary link to the final link path
+        # This overwrites link_path if it exists
+        await aiofiles.os.rename(temp_link_path, link_path)
+        _LOGGER.info(f"Atomically updated symlink {link_path} -> {target_str}")
+    except Exception as e:
+        # Don't bother with _LOGGER.exception here
+        _LOGGER.error(f"Failed to update atomic symlink {link_path} -> 
{target_str}: {e}")
+        # Clean up temporary link if rename failed
+        try:
+            await aiofiles.os.remove(temp_link_path)
+        except FileNotFoundError:
+            # TODO: Use with contextlib.suppress(FileNotFoundError) for these 
sorts of blocks?
+            pass
+        raise
+
+
 def user_releases(asf_uid: str, releases: Sequence[models.Release]) -> 
list[models.Release]:
     """Return a list of releases for which the user is a committee member or 
committer."""
     # TODO: This should probably be a session method instead


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to