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 b7d3603  Add a tool to generate sha256 or sha512 files
b7d3603 is described below

commit b7d36030b04b4e14b02cb04063c7dc0c692298ef
Author: Sean B. Palmer <[email protected]>
AuthorDate: Tue Mar 25 20:03:09 2025 +0200

    Add a tool to generate sha256 or sha512 files
---
 atr/routes/files.py            | 58 ++++++++++++++++++++++++++++++++++++++++++
 atr/tasks/hashing.py           | 12 ++++-----
 atr/templates/files-tools.html | 19 +++++++++++++-
 atr/util.py                    |  8 ++----
 4 files changed, 83 insertions(+), 14 deletions(-)

diff --git a/atr/routes/files.py b/atr/routes/files.py
index 09f8fd7..1ede576 100644
--- a/atr/routes/files.py
+++ b/atr/routes/files.py
@@ -21,6 +21,7 @@ from __future__ import annotations
 
 import asyncio
 import datetime
+import hashlib
 import logging
 import pathlib
 import re
@@ -39,6 +40,7 @@ import atr.analysis as analysis
 import atr.db as db
 import atr.db.models as models
 import atr.routes as routes
+import atr.tasks as tasks
 import atr.user as user
 import atr.util as util
 
@@ -528,3 +530,59 @@ async def root_files_delete(
 
     await quart.flash("File deleted successfully", "success")
     return quart.redirect(quart.url_for("root_files_list", 
project_name=project_name, version_name=version_name))
+
+
+@committer_route("/files/generate-hash/<project_name>/<version_name>/<path:file_path>",
 methods=["POST"])
+async def root_files_generate_hash(
+    session: CommitterSession, project_name: str, version_name: str, 
file_path: str
+) -> response.Response:
+    """Generate an sha256 or sha512 hash file for a candidate draft file."""
+    # Check that the user has access to the project
+    if not any((p.name == project_name) for p in (await 
session.user_projects)):
+        raise base.ASFQuartException("You do not have access to this project", 
errorcode=403)
+
+    async with db.session() as data:
+        # Check that the release exists
+        release = await data.release(name=f"{project_name}-{version_name}", 
_project=True).demand(
+            base.ASFQuartException("Release does not exist", errorcode=404)
+        )
+
+        # Get the hash type from the form data
+        # This is just a button, so we don't make a whole form validation 
schema for it
+        form = await quart.request.form
+        hash_type = form.get("hash_type")
+        if hash_type not in {"sha256", "sha512"}:
+            raise base.ASFQuartException("Invalid hash type", errorcode=400)
+
+        # Construct paths
+        base_path = util.get_candidate_draft_dir() / project_name / 
version_name
+        full_path = base_path / file_path
+        hash_path = file_path + f".{hash_type}"
+        full_hash_path = base_path / hash_path
+
+        # Check that the source file exists
+        if not await aiofiles.os.path.exists(full_path):
+            raise base.ASFQuartException("Source file does not exist", 
errorcode=404)
+
+        # Check that the hash file does not already exist
+        if await aiofiles.os.path.exists(full_hash_path):
+            raise base.ASFQuartException(f"{hash_type} file already exists", 
errorcode=400)
+
+        # Read the file and compute the hash
+        hash_obj = hashlib.sha256() if hash_type == "sha256" else 
hashlib.sha512()
+        async with aiofiles.open(full_path, "rb") as f:
+            while chunk := await f.read(8192):
+                hash_obj.update(chunk)
+
+        # Write the hash file
+        hash_value = hash_obj.hexdigest()
+        async with aiofiles.open(full_hash_path, "w") as f:
+            await f.write(f"{hash_value}  {file_path}\n")
+
+        # Add any relevant tasks to the database
+        for task in await tasks.sha_checks(release, str(hash_path)):
+            data.add(task)
+        await data.commit()
+
+    await quart.flash(f"{hash_type} file generated successfully", "success")
+    return quart.redirect(quart.url_for("root_files_list", 
project_name=project_name, version_name=version_name))
diff --git a/atr/tasks/hashing.py b/atr/tasks/hashing.py
index 2335111..0fbf1a0 100644
--- a/atr/tasks/hashing.py
+++ b/atr/tasks/hashing.py
@@ -55,17 +55,15 @@ async def _check_core(
         hash_func = hashlib.sha512
     else:
         raise task.Error(f"Unsupported hash algorithm: {algorithm}")
-    h = hash_func()
+    hash_obj = hash_func()
     async with aiofiles.open(original_file, mode="rb") as f:
-        while True:
-            chunk = await f.read(4096)
-            if not chunk:
-                break
-            h.update(chunk)
-    computed_hash = h.hexdigest()
+        while chunk := await f.read(4096):
+            hash_obj.update(chunk)
+    computed_hash = hash_obj.hexdigest()
     async with aiofiles.open(hash_file) as f:
         expected_hash = await f.read()
     # May be in the format "HASH FILENAME\n"
+    # TODO: Check the FILENAME part
     expected_hash = expected_hash.strip().split()[0]
     if secrets.compare_digest(computed_hash, expected_hash):
         return task.COMPLETED, None, ({"computed_hash": computed_hash, 
"expected_hash": expected_hash},)
diff --git a/atr/templates/files-tools.html b/atr/templates/files-tools.html
index 8c813ac..4a773e1 100644
--- a/atr/templates/files-tools.html
+++ b/atr/templates/files-tools.html
@@ -26,10 +26,27 @@
   </div>
 
   <h2>Tools</h2>
+  <h3>Generate hash files</h3>
+  <p>Generate SHA256 or SHA512 hash files for this file.</p>
+  <div class="d-flex gap-2 mb-4">
+    <form method="post"
+          action="{{ url_for('root_files_generate_hash', 
project_name=project_name, version_name=version_name, file_path=file_path) }}">
+      <input type="hidden" name="hash_type" value="sha256" />
+      <button type="submit" class="btn btn-outline-secondary">Generate 
SHA256</button>
+    </form>
+    <form method="post"
+          action="{{ url_for('root_files_generate_hash', 
project_name=project_name, version_name=version_name, file_path=file_path) }}">
+      <input type="hidden" name="hash_type" value="sha512" />
+      <button type="submit" class="btn btn-outline-secondary">Generate 
SHA512</button>
+    </form>
+  </div>
+
   <h3>Delete file</h3>
   <p>This tool deletes the file from the candidate draft.</p>
   <form method="post"
         action="{{ url_for('root_files_delete', project_name=project_name, 
version_name=version_name, file_path=file_path) }}">
-    <button type="submit" class="btn btn-danger">Delete file</button>
+    <button type="submit"
+            class="btn btn-danger"
+            onclick="return confirm('Are you sure you want to delete this 
file?')">Delete file</button>
   </form>
 {% endblock content %}
diff --git a/atr/util.py b/atr/util.py
index 5dbd6a6..f2f8af1 100644
--- a/atr/util.py
+++ b/atr/util.py
@@ -90,10 +90,8 @@ async def compute_sha512(file_path: pathlib.Path) -> str:
     """Compute SHA-512 hash of a file."""
     sha512 = hashlib.sha512()
     async with aiofiles.open(file_path, "rb") as f:
-        chunk = await f.read(4096)
-        while chunk:
+        while chunk := await f.read(4096):
             sha512.update(chunk)
-            chunk = await f.read(4096)
     return sha512.hexdigest()
 
 
@@ -101,10 +99,8 @@ async def file_sha3(path: str) -> str:
     """Compute SHA3-256 hash of a file."""
     sha3 = hashlib.sha3_256()
     async with aiofiles.open(path, "rb") as f:
-        chunk = await f.read(4096)
-        while chunk:
+        while chunk := await f.read(4096):
             sha3.update(chunk)
-            chunk = await f.read(4096)
     return sha3.hexdigest()
 
 


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

Reply via email to