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 c1ea4d1 Move the code to upload files into the release writer
c1ea4d1 is described below
commit c1ea4d145b9cff894f0a6bdbfb3234763b0f0f08
Author: Sean B. Palmer <[email protected]>
AuthorDate: Thu Sep 11 20:37:50 2025 +0100
Move the code to upload files into the release writer
---
atr/models/policy.py | 2 +-
atr/routes/upload.py | 56 +++---------------------------------------
atr/storage/writers/keys.py | 2 +-
atr/storage/writers/release.py | 41 ++++++++++++++++++++++++++++++-
atr/tarzip.py | 4 +--
5 files changed, 48 insertions(+), 57 deletions(-)
diff --git a/atr/models/policy.py b/atr/models/policy.py
index 8abb337..ecc88ac 100644
--- a/atr/models/policy.py
+++ b/atr/models/policy.py
@@ -19,7 +19,7 @@ from typing import Any
import pydantic
-from atr.models import schema
+from . import schema
# TODO: Maybe it's easier to use quart_schema for all our forms
diff --git a/atr/routes/upload.py b/atr/routes/upload.py
index e326fd9..21ad2d3 100644
--- a/atr/routes/upload.py
+++ b/atr/routes/upload.py
@@ -15,22 +15,18 @@
# specific language governing permissions and limitations
# under the License.
-import asyncio
import pathlib
-from collections.abc import Sequence
-import aiofiles
import quart
-import werkzeug.datastructures as datastructures
import werkzeug.wrappers.response as response
import wtforms
import atr.db as db
import atr.forms as forms
import atr.log as log
-import atr.revision as revision
import atr.routes as routes
import atr.routes.compose as compose
+import atr.storage as storage
import atr.template as template
@@ -78,7 +74,9 @@ async def selected(session: routes.CommitterSession,
project_name: str, version_
file_name = pathlib.Path(form.file_name.data)
file_data = form.file_data.data
- number_of_files = await _upload_files(project_name, version_name,
session.uid, file_name, file_data)
+ async with storage.write(session.uid) as write:
+ wacp = await
write.as_project_committee_participant(project_name)
+ number_of_files = await
wacp.release.upload_files(project_name, version_name, file_name, file_data)
return await session.redirect(
compose.selected,
success=f"{number_of_files} file{'' if number_of_files == 1
else 's'} added successfully",
@@ -107,49 +105,3 @@ async def selected(session: routes.CommitterSession,
project_name: str, version_
svn_form=svn_form,
user_ssh_keys=user_ssh_keys,
)
-
-
-async def _save_file(file: datastructures.FileStorage, target_path:
pathlib.Path) -> None:
- # TODO: Move to the storage interface
- async with aiofiles.open(target_path, "wb") as f:
- while chunk := await asyncio.to_thread(file.stream.read, 8192):
- await f.write(chunk)
-
-
-async def _upload_files(
- project_name: str,
- version_name: str,
- asf_uid: str,
- file_name: pathlib.Path | None,
- files: Sequence[datastructures.FileStorage],
-) -> int:
- """Process and save the uploaded files into a new draft revision."""
- number_of_files = len(files)
- description = f"Upload of {number_of_files} file{'' if number_of_files ==
1 else 's'} through web interface"
- async with revision.create_and_manage(project_name, version_name, asf_uid,
description=description) as creating:
-
- def get_target_path(file: datastructures.FileStorage) -> pathlib.Path:
- # Determine the target path within the new revision directory
- relative_file_path: pathlib.Path
- if not file_name:
- if not file.filename:
- raise routes.FlashError("No filename provided")
- # Use the original name
- relative_file_path = pathlib.Path(file.filename)
- else:
- # Use the provided name, relative to its anchor
- # In other words, ignore the leading "/"
- relative_file_path = file_name.relative_to(file_name.anchor)
-
- # Construct path inside the new revision directory
- target_path = creating.interim_path / relative_file_path
- return target_path
-
- # Save each uploaded file to the new revision directory
- for file in files:
- target_path = get_target_path(file)
- # Ensure parent directories exist within the new revision
- target_path.parent.mkdir(parents=True, exist_ok=True)
- await _save_file(file, target_path)
-
- return len(files)
diff --git a/atr/storage/writers/keys.py b/atr/storage/writers/keys.py
index a4c4497..a21033d 100644
--- a/atr/storage/writers/keys.py
+++ b/atr/storage/writers/keys.py
@@ -428,7 +428,7 @@ class CommitteeParticipant(FoundationCommitter):
return outcome.Error(e)
try:
- await asyncio.to_thread(committee_keys_dir.mkdir, parents=True,
exist_ok=True)
+ await aiofiles.os.makedirs(committee_keys_dir, exist_ok=True)
await asyncio.to_thread(util.chmod_directories,
committee_keys_dir, permissions=0o755)
await asyncio.to_thread(committee_keys_path.write_text,
full_keys_file_content, encoding="utf-8")
except OSError as e:
diff --git a/atr/storage/writers/release.py b/atr/storage/writers/release.py
index e508dfe..238981f 100644
--- a/atr/storage/writers/release.py
+++ b/atr/storage/writers/release.py
@@ -18,6 +18,7 @@
# Removing this will cause circular imports
from __future__ import annotations
+import asyncio
import base64
import contextlib
import datetime
@@ -38,8 +39,9 @@ import atr.storage as storage
import atr.util as util
if TYPE_CHECKING:
- from collections.abc import AsyncGenerator
+ from collections.abc import AsyncGenerator, Sequence
+ import werkzeug.datastructures as datastructures
SPECIAL_SUFFIXES: Final[frozenset[str]] = frozenset({".asc", ".sha256",
".sha512"})
@@ -298,6 +300,38 @@ class CommitteeParticipant(FoundationCommitter):
number=creating.new.number,
).demand(storage.AccessError("Revision not found"))
+ async def upload_files(
+ self,
+ project_name: str,
+ version_name: str,
+ file_name: pathlib.Path | None,
+ files: Sequence[datastructures.FileStorage],
+ ) -> int:
+ """Process and save the uploaded files into a new draft revision."""
+ number_of_files = len(files)
+ description = f"Upload of {number_of_files} file{'' if number_of_files
== 1 else 's'} through web interface"
+ async with self.create_and_manage_revision(project_name, version_name,
description) as creating:
+ # Save each uploaded file to the new revision directory
+ for file in files:
+ # Determine the target path within the new revision directory
+ relative_file_path: pathlib.Path
+ if not file_name:
+ if not file.filename:
+ raise storage.AccessError("No filename provided")
+ # Use the original name
+ relative_file_path = pathlib.Path(file.filename)
+ else:
+ # Use the provided name, relative to its anchor
+ # In other words, ignore the leading "/"
+ relative_file_path =
file_name.relative_to(file_name.anchor)
+
+ # Construct path inside the new revision directory
+ target_path = creating.interim_path / relative_file_path
+ # Ensure parent directories exist within the new revision
+ await aiofiles.os.makedirs(target_path.parent, exist_ok=True)
+ await self.__save_file(file, target_path)
+ return len(files)
+
async def __current_paths(self, creating: revision.Creating) ->
list[pathlib.Path]:
all_current_paths_interim: list[pathlib.Path] = []
async for p_rel_interim in
util.paths_recursive_all(creating.interim_path):
@@ -389,6 +423,11 @@ class CommitteeParticipant(FoundationCommitter):
return True, renamed_count_local
return False, renamed_count_local
+ async def __save_file(self, file: datastructures.FileStorage, target_path:
pathlib.Path) -> None:
+ async with aiofiles.open(target_path, "wb") as f:
+ while chunk := await asyncio.to_thread(file.stream.read, 8192):
+ await f.write(chunk)
+
async def __setup_revision(
self,
source_files_rel: list[pathlib.Path],
diff --git a/atr/tarzip.py b/atr/tarzip.py
index 414f37a..13f6cd8 100644
--- a/atr/tarzip.py
+++ b/atr/tarzip.py
@@ -15,10 +15,10 @@
# specific language governing permissions and limitations
# under the License.
+import contextlib
import tarfile
import zipfile
from collections.abc import Generator, Iterator
-from contextlib import contextmanager
from typing import IO, TypeVar
from typing import Protocol as TypingProtocol
@@ -134,7 +134,7 @@ ZipArchive = ArchiveContext[zipfile.ZipFile]
Archive = TarArchive | ZipArchive
-@contextmanager
[email protected]
def open_archive(archive_path: str) -> Generator[Archive]:
archive_file: tarfile.TarFile | zipfile.ZipFile | None = None
try:
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]