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 a0cca54 Add ZIP downloads and set them as the primary download method
a0cca54 is described below
commit a0cca5495d2b0db6fc835c7aff4bd35d4060de22
Author: Sean B. Palmer <[email protected]>
AuthorDate: Thu May 1 20:34:41 2025 +0100
Add ZIP downloads and set them as the primary download method
---
atr/routes/download.py | 35 ++++++++++++++++++++++++++++++
atr/templates/check-selected.html | 6 +++---
atr/templates/download-all.html | 40 +++++++++++++++++++++++------------
atr/templates/draft-tools.html | 2 +-
atr/templates/releases-completed.html | 2 +-
poetry.lock | 14 +++++++++++-
pyproject.toml | 1 +
7 files changed, 80 insertions(+), 20 deletions(-)
diff --git a/atr/routes/download.py b/atr/routes/download.py
index f1a0ff8..0bb1ad4 100644
--- a/atr/routes/download.py
+++ b/atr/routes/download.py
@@ -18,12 +18,14 @@
"""download.py"""
import pathlib
+from collections.abc import AsyncGenerator
import aiofiles
import aiofiles.os
import asfquart.base as base
import quart
import werkzeug.wrappers.response as response
+import zipstream
import atr.db as db
import atr.db.models as models
@@ -85,6 +87,39 @@ async def urls_selected(project_name: str, version_name:
str) -> response.Respon
return quart.Response(f"Internal server error: {e}", status=500,
mimetype="text/plain")
[email protected]("/download/zip/<project_name>/<version_name>")
+async def zip_selected(
+ session: routes.CommitterSession, project_name: str, version_name: str
+) -> response.Response | quart.wrappers.response.Response:
+ try:
+ release = await session.release(project_name=project_name,
version_name=version_name, phase=None)
+ except ValueError as e:
+ return quart.Response(f"Error: {e}", status=404, mimetype="text/plain")
+ except Exception as e:
+ return quart.Response(f"Server error: {e}", status=500,
mimetype="text/plain")
+
+ base_dir = util.release_directory(release)
+ files_to_zip = []
+ try:
+ for rel_path in await util.paths_recursive(base_dir):
+ full_item_path = base_dir / rel_path
+ if await aiofiles.os.path.isfile(full_item_path):
+ files_to_zip.append({"file": str(full_item_path), "name":
str(rel_path)})
+ except FileNotFoundError:
+ return quart.Response("Error: Release directory not found.",
status=404, mimetype="text/plain")
+
+ async def stream_zip(file_list: list[dict[str, str]]) ->
AsyncGenerator[bytes]:
+ aiozip = zipstream.AioZipStream(file_list, chunksize=32768)
+ async for chunk in aiozip.stream():
+ yield chunk
+
+ headers = {
+ "Content-Disposition": f'attachment; filename="{release.name}.zip"',
+ "Content-Type": "application/zip",
+ }
+ return quart.Response(stream_zip(files_to_zip), headers=headers,
mimetype="application/zip")
+
+
async def _download_or_list(project_name: str, version_name: str, file_path:
str) -> response.Response | quart.Response:
"""Download a file or list a directory from a release in any phase."""
# await session.check_access(project_name)
diff --git a/atr/templates/check-selected.html
b/atr/templates/check-selected.html
index 82d6e44..0aed8f0 100644
--- a/atr/templates/check-selected.html
+++ b/atr/templates/check-selected.html
@@ -99,11 +99,11 @@
{% if phase == "release_candidate" %}
<div class="card-body">
<a href="{{ as_url(routes.candidate.view,
project_name=release.project.name, version_name=release.version) }}"
- class="btn btn-primary me-2"><i class="bi bi-eye me-1"></i>View
files</a>
+ class="btn btn-primary me-2"><i class="bi bi-eye me-1"></i> View
files</a>
<a href="{{ as_url(routes.download.all_selected,
project_name=release.project.name, version_name=release.version) }}"
- class="btn btn-primary me-2"><i class="bi bi-download
me-1"></i>Download files</a>
+ class="btn btn-primary me-2"><i class="bi bi-download me-1"></i>
Download files</a>
<a href="{{ as_url(routes.resolve.selected,
project_name=release.project.name, version_name=release.version) }}"
- class="btn btn-success"><i class="bi bi-clipboard-check
me-1"></i>Resolve vote</a>
+ class="btn btn-success"><i class="bi bi-clipboard-check me-1"></i>
Resolve vote</a>
</div>
{% endif %}
</div>
diff --git a/atr/templates/download-all.html b/atr/templates/download-all.html
index 86fd352..8bd1194 100644
--- a/atr/templates/download-all.html
+++ b/atr/templates/download-all.html
@@ -41,42 +41,54 @@
</p>
<h1>
- Download all files for <strong>{{ release.project.short_display_name
}}</strong> <em>{{ release.version }}</em>
+ Download all files in <strong>{{ release.project.short_display_name
}}</strong> <em>{{ release.version }}</em>
</h1>
+ <h2 id="download-zip">Download ZIP archive</h2>
<p>
+ Download a single ZIP archive containing all files for this release below.
+ The archive is generated on the fly, which may take a while for very large
releases.
+ </p>
+ <p>
+ <a href="{{ as_url(routes.download.zip_selected,
project_name=release.project.name, version_name=release.version) }}"
+ class="btn btn-primary btn-lg">
+ <i class="bi bi-file-earmark-zip me-2"></i>Download {{ release.name
}}.zip
+ </a>
+ </p>
+
+ <h2>Alternative methods</h2>
+ <p>If you prefer, you can download the files using other methods.</p>
+ <p>
+ <a href="#download-browser"
+ class="btn btn-sm btn-outline-secondary"
+ title="Download using your browser">Browser</a>
<a href="#download-rsync"
class="btn btn-sm btn-outline-secondary me-2"
title="Download using rsync">Rsync</a>
<a href="#download-wget"
class="btn btn-sm btn-outline-secondary me-2"
title="Download using wget">Wget</a>
- <a href="#download-browser"
- class="btn btn-sm btn-outline-secondary"
- title="Download using your browser">Browser</a>
</p>
-
- <h2 id="download-rsync">Using rsync</h2>
- <p>You can download the files in this release using rsync with the following
command:</p>
+ <h3 id="download-browser" class="mt-4">Using your browser</h3>
+ <p>
+ You can download the files one by one using your browser from the <a
href="{{ as_url(routes.download.path_empty, project_name=release.project.name,
version_name=release.version) }}">download folder</a>. Clicking a link to any
file will download it, as it is served as <code>application/octet-stream</code>.
+ </p>
+ <h3 id="download-rsync" class="mt-4">Using rsync</h3>
+ <p>You can download all of the files in this release using rsync with the
following command:</p>
<!-- TODO: Add a button to copy the command to the clipboard -->
<pre class="bg-light p-3 mb-3">
rsync -av -e 'ssh -p 2222' {{ asf_id }}@{{ server_domain }}:/{{
release.project.name }}/{{ release.version }}/ ${DOWNLOAD_PATH}/
</pre>
{% include "user-ssh-keys.html" %}
- <h2 id="download-wget">Using wget</h2>
- <p>You can download the files in this release using wget with the following
command:</p>
+ <h3 id="download-wget" class="mt-4">Using wget</h3>
+ <p>You can download all of the files in this release using wget with the
following command:</p>
<pre class="bg-light p-3 mb-3">
wget -r -np -nH --cut-dirs=3{% if server_domain == "127.0.0.1" %}
--no-check-certificate{% endif %} https://{{ server_domain }}{{
as_url(routes.download.path_empty, project_name=release.project.name,
version_name=release.version) }}
</pre>
<p>
This downloads the files into the <em>current directory</em>. Ensure that
you create a new empty directory, and change to it, before running the command.
</p>
-
- <h2 id="download-browser">Using your browser</h2>
- <p>
- You can also download the files one by one using your browser from the <a
href="{{ as_url(routes.download.path_empty, project_name=release.project.name,
version_name=release.version) }}">download folder</a>. Clicking a link to any
file will download it, as it is served as
<code>application/octet-stream</code>. We do not offer an archive of the entire
release due to the size of large releases.
- </p>
{% endblock content %}
{% block stylesheets %}
diff --git a/atr/templates/draft-tools.html b/atr/templates/draft-tools.html
index 5780c58..a17518f 100644
--- a/atr/templates/draft-tools.html
+++ b/atr/templates/draft-tools.html
@@ -29,7 +29,7 @@
<h3>Generate hash files</h3>
<p>Generate an SHA256 or SHA512 hash file for this file.</p>
<div class="alert alert-warning">
- <i class="bi bi-exclamation-triangle me-2"></i>IMPORTANT: The ASF security
team <a href="https://infra.apache.org/release-signing.html#sha-checksum"
+ <i class="bi bi-exclamation-triangle me-2"></i> IMPORTANT: The ASF
security team <a
href="https://infra.apache.org/release-signing.html#sha-checksum"
class="alert-link">recommends using SHA512</a> as the hash algorithm.
Please select SHA512 unless you have a specific reason to use SHA256.
</div>
diff --git a/atr/templates/releases-completed.html
b/atr/templates/releases-completed.html
index 5fc1d71..12a48de 100644
--- a/atr/templates/releases-completed.html
+++ b/atr/templates/releases-completed.html
@@ -50,7 +50,7 @@
</div>
{% else %}
<div class="alert alert-info mt-4" role="alert">
- <i class="bi bi-info-circle me-2"></i>There are no completed releases
recorded for {{ project_display_name }}.
+ <i class="bi bi-info-circle me-2"></i> There are no completed releases
recorded for {{ project_display_name }}.
</div>
{% endif %}
{% endblock content %}
diff --git a/poetry.lock b/poetry.lock
index 3e2d6d6..6bb0abb 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -189,6 +189,18 @@ typing_extensions = ">=4.0"
dev = ["attribution (==1.7.1)", "black (==24.3.0)", "build (>=1.2)",
"coverage[toml] (==7.6.10)", "flake8 (==7.0.0)", "flake8-bugbear (==24.12.12)",
"flit (==3.10.1)", "mypy (==1.14.1)", "ufmt (==2.5.1)", "usort (==1.0.8.post1)"]
docs = ["sphinx (==8.1.3)", "sphinx-mdinclude (==0.6.1)"]
+[[package]]
+name = "aiozipstream"
+version = "0.4"
+description = "Creating zip files on the fly"
+optional = false
+python-versions = "*"
+groups = ["main"]
+files = [
+ {file = "aiozipstream-0.4-py2.py3-none-any.whl", hash =
"sha256:a58bad8c75aba319c07bd3d817da7caec7417c1eb4f4c692e00b173fb9ded9c6"},
+ {file = "aiozipstream-0.4.tar.gz", hash =
"sha256:ccc5cec35c2580b8a13185c916b1581bfcb4278ddf6ea3f7f834b6c9c47d6c61"},
+]
+
[[package]]
name = "alembic"
version = "1.15.2"
@@ -2980,4 +2992,4 @@ propcache = ">=0.2.1"
[metadata]
lock-version = "2.1"
python-versions = "~=3.13"
-content-hash =
"35f766c3f0adadcf4faf77453dce39cfbd488a0d780617f83422bcc14c805e9f"
+content-hash =
"6ff06f78389576f0ae1fdda7a7547d7fa6d32cca37acf7eacecaf52108e7e20f"
diff --git a/pyproject.toml b/pyproject.toml
index 8708a91..30bf8a6 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -15,6 +15,7 @@ dependencies = [
"aioshutil (>=1.5,<2.0)",
"aiosmtplib (>=4.0.0,<5.0.0)",
"aiosqlite>=0.21.0,<0.22.0",
+ "aiozipstream (>=0.4,<0.5)",
"alembic~=1.14",
"asfquart @ git+https://github.com/apache/infrastructure-asfquart.git@main",
"asyncssh>=2.20.0,<3.0.0",
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]