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]