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 a424b00  Ensure that directories are named uniquely and removed 
correctly
a424b00 is described below

commit a424b008abdceb5f0d37aa978f242db3dcdddc78
Author: Sean B. Palmer <[email protected]>
AuthorDate: Fri Jul 11 15:28:56 2025 +0100

    Ensure that directories are named uniquely and removed correctly
---
 atr/revision.py | 104 +++++++++++++++++++++++++++++++++++---------------------
 1 file changed, 66 insertions(+), 38 deletions(-)

diff --git a/atr/revision.py b/atr/revision.py
index eb849fb..f9af9a5 100644
--- a/atr/revision.py
+++ b/atr/revision.py
@@ -20,6 +20,7 @@ import contextlib
 import dataclasses
 import datetime
 import pathlib
+import secrets
 import tempfile
 from collections.abc import AsyncGenerator
 
@@ -45,6 +46,24 @@ class Creating:
     failed: FailedError | None = None
 
 
+class SafeSession:
+    def __init__(self, temp_dir: str):
+        self._stack = contextlib.AsyncExitStack()
+        self._manager = db.session()
+        self._temp_dir = temp_dir
+
+    async def __aenter__(self) -> db.Session:
+        try:
+            return await self._stack.enter_async_context(self._manager)
+        except Exception:
+            await aioshutil.rmtree(self._temp_dir)  # type: ignore[call-arg]
+            raise
+
+    async def __aexit__(self, _exc_type, _exc, _tb):
+        await self._stack.aclose()
+        return False
+
+
 # NOTE: The create_directory parameter is not used anymore
 # The temporary directory will always be created
 @contextlib.asynccontextmanager
@@ -66,7 +85,8 @@ async def create_and_manage(
     # Create a temporary directory
     # We ensure, below, that it's removed on any exception
     # Use the tmp subdirectory of state, to ensure that it is on the same 
filesystem
-    temp_dir: str = await asyncio.to_thread(tempfile.mkdtemp, 
dir=util.get_tmp_dir())
+    prefix_token = secrets.token_hex(16)
+    temp_dir: str = await asyncio.to_thread(tempfile.mkdtemp, 
prefix=prefix_token, dir=util.get_tmp_dir())
     temp_dir_path = pathlib.Path(temp_dir)
     creating = Creating(old=old_revision, interim_path=temp_dir_path, 
new=None, failed=None)
     try:
@@ -86,47 +106,55 @@ async def create_and_manage(
         raise
 
     # Ensure that the permissions of every directory are 755
-    await asyncio.to_thread(util.chmod_directories, temp_dir_path)
-
-    # Create a revision row, holding the write lock
-    async with db.session() as data:
-        # This is the only place where models.Revision is constructed
-        # That makes models.populate_revision_sequence_and_name safe against 
races
-        # Because that event is called when data.add is called below
-        # And we have a write lock at that point through the use of 
data.begin_immediate
-        new_revision = models.Revision(
-            release_name=release_name,
-            release=release,
-            asfuid=asf_uid,
-            created=datetime.datetime.now(datetime.UTC),
-            phase=release.phase,
-            description=description,
-        )
-
-        # Acquire the write lock and add the row
-        # We need this write lock for moving the directory below atomically
-        # But it also helps to make models.populate_revision_sequence_and_name 
safe against races
-        await data.begin_immediate()
-        data.add(new_revision)
-
-        # Flush but do not commit the row to get its name and number
-        # The row will still be invisible to other sessions after flushing
-        await data.flush()
-        # Give the caller details about the new revision
-        creating.new = new_revision
-
-        # Rename the directory to the new revision number
-        await data.refresh(release)
-        new_revision_dir = util.release_directory(release)
-
-        # Ensure that the parent directory exists
-        await aiofiles.os.makedirs(new_revision_dir.parent, exist_ok=True)
+    try:
+        await asyncio.to_thread(util.chmod_directories, temp_dir_path)
+    except Exception:
+        await aioshutil.rmtree(temp_dir)  # type: ignore[call-arg]
+        raise
 
-        # Rename the temporary interim directory to the new revision number
-        await aiofiles.os.rename(temp_dir, new_revision_dir)
+    async with SafeSession(temp_dir) as data:
+        try:
+            # This is the only place where models.Revision is constructed
+            # That makes models.populate_revision_sequence_and_name safe 
against races
+            # Because that event is called when data.add is called below
+            # And we have a write lock at that point through the use of 
data.begin_immediate
+            new_revision = models.Revision(
+                release_name=release_name,
+                release=release,
+                asfuid=asf_uid,
+                created=datetime.datetime.now(datetime.UTC),
+                phase=release.phase,
+                description=description,
+            )
+
+            # Acquire the write lock and add the row
+            # We need this write lock for moving the directory below atomically
+            # But it also helps to make 
models.populate_revision_sequence_and_name safe against races
+            await data.begin_immediate()
+            data.add(new_revision)
+
+            # Flush but do not commit the new revision row to get its name and 
number
+            # The row will still be invisible to other sessions after flushing
+            await data.flush()
+            # Give the caller details about the new revision
+            creating.new = new_revision
+
+            # Rename the directory to the new revision number
+            await data.refresh(release)
+            new_revision_dir = util.release_directory(release)
+
+            # Ensure that the parent directory exists
+            await aiofiles.os.makedirs(new_revision_dir.parent, exist_ok=True)
+
+            # Rename the temporary interim directory to the new revision number
+            await aiofiles.os.rename(temp_dir, new_revision_dir)
+        except Exception:
+            await aioshutil.rmtree(temp_dir)  # type: ignore[call-arg]
+            raise
 
         # Commit to end the transaction started by data.begin_immediate
         # We must commit the revision before starting the checks
+        # This also releases the write lock
         await data.commit()
 
         async with data.begin():


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

Reply via email to