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 22c1d06 Move draft.add_files to upload.release 22c1d06 is described below commit 22c1d06e63f3f31d8195c9c27825ee0b81b5bcbb Author: Sean B. Palmer <s...@miscoranda.com> AuthorDate: Fri Apr 25 16:55:19 2025 +0100 Move draft.add_files to upload.release --- atr/routes/draft.py | 133 ++------------------------------ atr/routes/modules.py | 1 + atr/routes/upload.py | 154 ++++++++++++++++++++++++++++++++++++++ atr/templates/draft-content.html | 2 +- atr/templates/draft-evaluate.html | 2 +- atr/templates/drafts.html | 2 +- 6 files changed, 163 insertions(+), 131 deletions(-) diff --git a/atr/routes/draft.py b/atr/routes/draft.py index 24bbda0..c269433 100644 --- a/atr/routes/draft.py +++ b/atr/routes/draft.py @@ -42,15 +42,15 @@ import atr.mail as mail import atr.revision as revision import atr.routes as routes import atr.routes.candidate as candidate +import atr.routes.upload as upload import atr.tasks.sbom as sbom import atr.tasks.vote as tasks_vote import atr.user as user import atr.util as util if TYPE_CHECKING: - from collections.abc import Callable, Sequence + from collections.abc import Callable - import werkzeug.datastructures as datastructures import werkzeug.wrappers.response as response # _CONFIG: Final = config.get() @@ -102,83 +102,6 @@ class StartReleaseForm(util.QuartFormTyped): submit = wtforms.SubmitField("Start new release") -class SvnImportForm(util.QuartFormTyped): - """Form for importing files from SVN into a draft.""" - - svn_url = wtforms.URLField( - "SVN URL", - validators=[ - wtforms.validators.InputRequired("SVN URL is required."), - wtforms.validators.URL(require_tld=False), - ], - description="The URL to the public SVN directory", - ) - revision = wtforms.StringField( - "Revision", - default="HEAD", - validators=[], - description="Specify an SVN revision number or leave as HEAD for the latest", - ) - target_subdirectory = wtforms.StringField( - "Target subdirectory", - validators=[], - description="Subdirectory to place imported files, defaulting to the root (optional)", - ) - submit = wtforms.SubmitField("Queue SVN import task") - - -# TODO: Rename to upload.release? -@routes.committer("/upload/<project_name>/<version_name>", methods=["GET", "POST"]) -async def add_files(session: routes.CommitterSession, project_name: str, version_name: str) -> response.Response | str: - """Show a page to allow the user to add files to a candidate draft.""" - await session.check_access(project_name) - - class AddFilesForm(util.QuartFormTyped): - """Form for adding files to a release candidate.""" - - file_name = wtforms.StringField("File name (optional)") - file_data = wtforms.MultipleFileField( - "Files", validators=[wtforms.validators.InputRequired("At least one file is required")] - ) - submit = wtforms.SubmitField("Add files") - - def validate_file_name(self, field: wtforms.Field) -> bool: - if field.data and len(self.file_data.data) > 1: - raise wtforms.validators.ValidationError("File name can only be used when uploading a single file") - return True - - form = await AddFilesForm.create_form() - if await form.validate_on_submit(): - try: - file_name = None - if isinstance(form.file_name.data, str) and form.file_name.data: - 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) - return await session.redirect( - compose, - success=f"{number_of_files} file{'' if number_of_files == 1 else 's'} added successfully", - project_name=project_name, - version_name=version_name, - ) - except Exception as e: - logging.exception("Error adding file:") - await quart.flash(f"Error adding file: {e!s}", "error") - - svn_form = await SvnImportForm.create_form() - release = await session.release(project_name, version_name) - - return await quart.render_template( - "draft-add-files.html", - asf_id=session.uid, - server_domain=session.host, - release=release, - form=form, - svn_form=svn_form, - ) - - # TODO: Rename to compose.release? @routes.committer("/compose/<project_name>/<version_name>") async def compose(session: routes.CommitterSession, project_name: str, version_name: str) -> response.Response | str: @@ -909,7 +832,7 @@ async def start(session: routes.CommitterSession, project_name: str) -> response async def svnload(session: routes.CommitterSession, project_name: str, version_name: str) -> response.Response | str: """Import files from SVN into a draft.""" await session.check_access(project_name) - form = await SvnImportForm.create_form() + form = await upload.SvnImportForm.create_form() release = await session.release(project_name, version_name, with_project=False) if not await form.validate_on_submit(): @@ -917,7 +840,7 @@ async def svnload(session: routes.CommitterSession, project_name: str, version_n for error in errors: await quart.flash(f"{error}", "error") return await session.redirect( - add_files, + upload.release, project_name=project_name, version_name=version_name, ) @@ -945,7 +868,7 @@ async def svnload(session: routes.CommitterSession, project_name: str, version_n except Exception: logging.exception("Error queueing SVN import task:") return await session.redirect( - add_files, + upload.release, error="Error queueing SVN import task", project_name=project_name, version_name=version_name, @@ -1388,49 +1311,3 @@ async def _revisions_process( "modified": sorted(list(modified_files)), } return revision_data, current_revision_files - - -async def _save_file(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 _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.""" - async with revision.create_and_manage(project_name, version_name, asf_uid) as ( - new_revision_dir, - _new_revision_name, - ): - - 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 = new_revision_dir / 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/routes/modules.py b/atr/routes/modules.py index fd8d6ab..b2ed77c 100644 --- a/atr/routes/modules.py +++ b/atr/routes/modules.py @@ -25,6 +25,7 @@ import atr.routes.preview as preview import atr.routes.projects as projects import atr.routes.release as release import atr.routes.root as root +import atr.routes.upload as upload # Export data for a custom linter script diff --git a/atr/routes/upload.py b/atr/routes/upload.py new file mode 100644 index 0000000..123e12d --- /dev/null +++ b/atr/routes/upload.py @@ -0,0 +1,154 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import asyncio +import logging +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.revision as revision +import atr.routes as routes +import atr.routes.draft as draft +import atr.util as util + + +class SvnImportForm(util.QuartFormTyped): + """Form for importing files from SVN into a draft.""" + + svn_url = wtforms.URLField( + "SVN URL", + validators=[ + wtforms.validators.InputRequired("SVN URL is required."), + wtforms.validators.URL(require_tld=False), + ], + description="The URL to the public SVN directory", + ) + revision = wtforms.StringField( + "Revision", + default="HEAD", + validators=[], + description="Specify an SVN revision number or leave as HEAD for the latest", + ) + target_subdirectory = wtforms.StringField( + "Target subdirectory", + validators=[], + description="Subdirectory to place imported files, defaulting to the root (optional)", + ) + submit = wtforms.SubmitField("Queue SVN import task") + + +@routes.committer("/upload/<project_name>/<version_name>", methods=["GET", "POST"]) +async def release(session: routes.CommitterSession, project_name: str, version_name: str) -> response.Response | str: + """Show a page to allow the user to add files to a candidate draft.""" + await session.check_access(project_name) + + class AddFilesForm(util.QuartFormTyped): + """Form for adding files to a release candidate.""" + + file_name = wtforms.StringField("File name (optional)") + file_data = wtforms.MultipleFileField( + "Files", validators=[wtforms.validators.InputRequired("At least one file is required")] + ) + submit = wtforms.SubmitField("Add files") + + def validate_file_name(self, field: wtforms.Field) -> bool: + if field.data and len(self.file_data.data) > 1: + raise wtforms.validators.ValidationError("File name can only be used when uploading a single file") + return True + + form = await AddFilesForm.create_form() + if await form.validate_on_submit(): + try: + file_name = None + if isinstance(form.file_name.data, str) and form.file_name.data: + 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) + return await session.redirect( + draft.compose, + success=f"{number_of_files} file{'' if number_of_files == 1 else 's'} added successfully", + project_name=project_name, + version_name=version_name, + ) + except Exception as e: + logging.exception("Error adding file:") + await quart.flash(f"Error adding file: {e!s}", "error") + + svn_form = await SvnImportForm.create_form() + release = await session.release(project_name, version_name) + + return await quart.render_template( + "draft-add-files.html", + asf_id=session.uid, + server_domain=session.host, + release=release, + form=form, + svn_form=svn_form, + ) + + +async def _save_file(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 _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.""" + async with revision.create_and_manage(project_name, version_name, asf_uid) as ( + new_revision_dir, + _new_revision_name, + ): + + 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 = new_revision_dir / 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/templates/draft-content.html b/atr/templates/draft-content.html index 09845af..bdfaffb 100644 --- a/atr/templates/draft-content.html +++ b/atr/templates/draft-content.html @@ -86,7 +86,7 @@ <h5 class="mb-0">Draft actions</h5> </div> <div class="card-body d-flex flex-wrap gap-2"> - <a href="{{ as_url(routes.draft.add_files, project_name=release.project.name, version_name=release.version) }}" + <a href="{{ as_url(routes.upload.release, project_name=release.project.name, version_name=release.version) }}" title="Upload files to this draft" class="btn btn-primary"><i class="fas fa-upload me-1"></i> Upload files</a> <a href="{{ as_url(routes.draft.revisions, project_name=release.project.name, version_name=release.version) }}" diff --git a/atr/templates/draft-evaluate.html b/atr/templates/draft-evaluate.html index 931438d..5a452a6 100644 --- a/atr/templates/draft-evaluate.html +++ b/atr/templates/draft-evaluate.html @@ -195,7 +195,7 @@ </div> <div class="card-body"> <p> - <a href="{{ as_url(routes.draft.add_files, project_name=release.project.name, version_name=release.version) }}">Upload a file in the browser</a>, or use the command below to add or modify files in this release using rsync: + <a href="{{ as_url(routes.upload.release, project_name=release.project.name, version_name=release.version) }}">Upload a file in the browser</a>, or use the command below to add or modify files in this release using rsync: </p> </div> <div class="card-footer bg-light border-1 pt-4 pb-4 position-relative"> diff --git a/atr/templates/drafts.html b/atr/templates/drafts.html index 79a382d..88475cc 100644 --- a/atr/templates/drafts.html +++ b/atr/templates/drafts.html @@ -48,7 +48,7 @@ title="Project for {{ release.project.display_name }} {{ release.version }}" class="btn btn-sm btn-outline-secondary">Project</a> <br /> - <a href="{{ as_url(routes.draft.add_files, project_name=release.project.name, version_name=release.version) }}" + <a href="{{ as_url(routes.upload.release, project_name=release.project.name, version_name=release.version) }}" title="Add files to {{ release.project.display_name }} {{ release.version }}" class="btn btn-sm btn-outline-primary">Add files</a> <a href="{{ as_url(routes.draft.vote_start, project_name=release.project.name, version=release.version, revision=release.revision) }}" --------------------------------------------------------------------- To unsubscribe, e-mail: commits-unsubscr...@tooling.apache.org For additional commands, e-mail: commits-h...@tooling.apache.org