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-atr-experiments.git


The following commit(s) were added to refs/heads/main by this push:
     new 7a333f6  Allow package deletion
7a333f6 is described below

commit 7a333f61052adb37f37f84db4a6efbcadbbb30c2
Author: Sean B. Palmer <s...@miscoranda.com>
AuthorDate: Thu Feb 20 18:47:33 2025 +0200

    Allow package deletion
---
 atr/routes.py                       | 80 +++++++++++++++++++++++++++++++++++++
 atr/templates/candidate-review.html | 34 +++++++++++++++-
 docs/plan.html                      |  4 +-
 docs/plan.md                        |  4 +-
 4 files changed, 117 insertions(+), 5 deletions(-)

diff --git a/atr/routes.py b/atr/routes.py
index 1536891..e55f70d 100644
--- a/atr/routes.py
+++ b/atr/routes.py
@@ -671,6 +671,86 @@ async def root_candidate_review() -> str:
         return await render_template("candidate-review.html", 
releases=user_releases)
 
 
+async def delete_package_files(package: Package, uploads_path: Path) -> None:
+    """Delete the artifact and signature files associated with a package."""
+    if package.artifact_sha3:
+        artifact_path = uploads_path / package.artifact_sha3
+        if await aiofiles.os.path.exists(artifact_path):
+            await aiofiles.os.remove(artifact_path)
+
+    if package.signature_sha3:
+        signature_path = uploads_path / package.signature_sha3
+        if await aiofiles.os.path.exists(signature_path):
+            await aiofiles.os.remove(signature_path)
+
+
+async def validate_package_deletion(
+    db_session: AsyncSession, artifact_sha3: str, release_key: str, 
session_uid: str
+) -> Package:
+    """Validate package deletion request and return the package if valid."""
+    # Get the package and its associated release
+    if Package.release is None:
+        raise FlashError("Package has no associated release")
+    if Release.pmc is None:
+        raise FlashError("Release has no associated PMC")
+
+    pkg_release = cast(InstrumentedAttribute[Release], Package.release)
+    rel_pmc = cast(InstrumentedAttribute[PMC], Release.pmc)
+    statement = (
+        select(Package)
+        .options(selectinload(pkg_release).selectinload(rel_pmc))
+        .where(Package.artifact_sha3 == artifact_sha3)
+    )
+    result = await db_session.execute(statement)
+    package = result.scalar_one_or_none()
+
+    if not package:
+        raise FlashError("Package not found")
+
+    if package.release_key != release_key:
+        raise FlashError("Invalid release key")
+
+    # Check permissions
+    if package.release and package.release.pmc:
+        if session_uid not in package.release.pmc.pmc_members and session_uid 
not in package.release.pmc.committers:
+            raise FlashError("You don't have permission to delete this 
package")
+
+    return package
+
+
+@APP.route("/package/delete", methods=["POST"])
+@require(Requirements.committer)
+async def root_package_delete() -> Response:
+    """Delete a package from a release candidate."""
+    session = await session_read()
+    if (session is None) or (session.uid is None):
+        raise ASFQuartException("Not authenticated", errorcode=401)
+
+    form = await request.form
+    artifact_sha3 = form.get("artifact_sha3")
+    release_key = form.get("release_key")
+
+    if not artifact_sha3 or not release_key:
+        await flash("Missing required parameters", "error")
+        return redirect(url_for("root_candidate_review"))
+
+    async with get_session() as db_session:
+        async with db_session.begin():
+            try:
+                package = await validate_package_deletion(db_session, 
artifact_sha3, release_key, session.uid)
+                await delete_package_files(package, 
Path(get_release_storage_dir()))
+                await db_session.delete(package)
+            except FlashError as e:
+                await flash(str(e), "error")
+                return redirect(url_for("root_candidate_review"))
+            except Exception as e:
+                await flash(f"Error deleting files: {e!s}", "error")
+                return redirect(url_for("root_candidate_review"))
+
+    await flash("Package deleted successfully", "success")
+    return redirect(url_for("root_candidate_review"))
+
+
 async def save_file_by_hash(base_dir: Path, file: FileStorage) -> tuple[str, 
int]:
     """
     Save a file using its SHA3-256 hash as the filename.
diff --git a/atr/templates/candidate-review.html 
b/atr/templates/candidate-review.html
index 94dfe5f..f2b8a06 100644
--- a/atr/templates/candidate-review.html
+++ b/atr/templates/candidate-review.html
@@ -61,6 +61,25 @@
           color: white;
       }
 
+      .delete-form {
+          display: inline-block;
+          margin-left: 1rem;
+      }
+
+      .delete-button {
+          padding: 0.5rem 1rem;
+          background: #dc3545;
+          color: white;
+          border: none;
+          border-radius: 4px;
+          cursor: pointer;
+          font-weight: 500;
+      }
+
+      .delete-button:hover {
+          background: #c82333;
+      }
+
       .package-separator {
           height: 2rem;
           background-color: #f5f5f5;
@@ -181,8 +200,21 @@
             <td class="prose">
               <a class="verify-link"
                  href="{{ url_for('root_candidate_signatures_verify', 
release_key=release.storage_key) }}">
-                Verify Signatures
+                Verify signatures
               </a>
+              <form method="post"
+                    action="{{ url_for('root_package_delete') }}"
+                    class="delete-form">
+                <input type="hidden"
+                       name="artifact_sha3"
+                       value="{{ package.artifact_sha3 }}" />
+                <input type="hidden" name="release_key" value="{{ 
release.storage_key }}" />
+                <button type="submit"
+                        class="delete-button"
+                        onclick="return confirm('Are you sure you want to 
delete this package?')">
+                  Delete package
+                </button>
+              </form>
             </td>
           </tr>
         {% endfor %}
diff --git a/docs/plan.html b/docs/plan.html
index 782115c..961e6b6 100644
--- a/docs/plan.html
+++ b/docs/plan.html
@@ -8,8 +8,8 @@
 <li>[DONE] Allow upload of checksum file alongside artifacts and 
signatures</li>
 <li>[DONE] Add a form field to choose the RC artifact type</li>
 <li>[DONE] Allow extra types of artifact, such as reproducible binary and 
convenience binary</li>
-<li>Differentiate between podling PPMCs and top level PMCs</li>
-<li>Allow package deletion</li>
+<li>[DONE] Differentiate between podling PPMCs and top level PMCs</li>
+<li>[DONE] Allow package deletion</li>
 </ul>
 </li>
 <li>
diff --git a/docs/plan.md b/docs/plan.md
index ad55597..366cbd4 100644
--- a/docs/plan.md
+++ b/docs/plan.md
@@ -8,8 +8,8 @@ This is a rough plan of immediate tasks. The priority of these 
tasks may change,
    - [DONE] Allow upload of checksum file alongside artifacts and signatures
    - [DONE] Add a form field to choose the RC artifact type
    - [DONE] Allow extra types of artifact, such as reproducible binary and 
convenience binary
-   - Differentiate between podling PPMCs and top level PMCs
-   - Allow package deletion
+   - [DONE] Differentiate between podling PPMCs and top level PMCs
+   - [DONE] Allow package deletion
 
 2. Enhance RC display
    - [DONE] Augment raw file hashes with the original filenames in the UI


---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscr...@tooling.apache.org
For additional commands, e-mail: dev-h...@tooling.apache.org

Reply via email to