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 ae1afe7 Link to a new combined download options page from every phase
ae1afe7 is described below
commit ae1afe7ff8b2541c714b148b0767d9aed2a99e37
Author: Sean B. Palmer <[email protected]>
AuthorDate: Thu May 1 20:05:43 2025 +0100
Link to a new combined download options page from every phase
---
atr/routes/download.py | 146 ++++++++++++++++++++++++---
atr/routes/mapping.py | 19 ++--
atr/templates/candidate-resolve-release.html | 6 +-
atr/templates/check-selected.html | 22 ++--
atr/templates/download-all.html | 88 ++++++++++++++++
atr/templates/finish-selected.html | 21 ++--
atr/templates/releases.html | 4 +-
7 files changed, 256 insertions(+), 50 deletions(-)
diff --git a/atr/routes/download.py b/atr/routes/download.py
index 876e078..f1a0ff8 100644
--- a/atr/routes/download.py
+++ b/atr/routes/download.py
@@ -21,39 +21,157 @@ import pathlib
import aiofiles
import aiofiles.os
+import asfquart.base as base
import quart
import werkzeug.wrappers.response as response
+import atr.db as db
+import atr.db.models as models
import atr.routes as routes
+import atr.routes.mapping as mapping
import atr.routes.root as root
import atr.util as util
[email protected]("/download/<phase>/<project_name>/<version_name>/<path:file_path>")
-async def phase(
- session: routes.CommitterSession, phase: str, project_name: str,
version_name: str, file_path: str
-) -> response.Response | quart.Response:
- """Download a file from a release in any phase."""
- await session.check_access(project_name)
[email protected]("/download/all/<project_name>/<version_name>")
+async def all_selected(
+ session: routes.CommitterSession, project_name: str, version_name: str
+) -> response.Response | str:
+ """Display download commands for a release."""
+ async with db.session() as data:
+ release = await session.release(project_name=project_name,
version_name=version_name, phase=None, data=data)
+ if not release:
+ return await session.redirect(root.index, error="Release not
found")
+ user_ssh_keys = await data.ssh_key(asf_uid=session.uid).all()
+
+ back_url = mapping.release_as_url(release)
+
+ return await quart.render_template(
+ "download-all.html",
+ project_name=project_name,
+ version_name=version_name,
+ release=release,
+ asf_id=session.uid,
+ server_domain=session.host,
+ user_ssh_keys=user_ssh_keys,
+ back_url=back_url,
+ )
+
+
[email protected]("/download/path/<project_name>/<version_name>/<path:file_path>")
+async def path(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."""
+ return await _download_or_list(project_name, version_name, file_path)
+
+
[email protected]("/download/path/<project_name>/<version_name>/")
+async def path_empty(project_name: str, version_name: str) ->
response.Response | quart.Response:
+ """List files at the root of a release directory for download."""
+ return await _download_or_list(project_name, version_name, ".")
+
+
[email protected]("/download/urls/<project_name>/<version_name>")
+async def urls_selected(project_name: str, version_name: str) ->
response.Response | quart.Response:
+ try:
+ async with db.session() as session:
+ release = await session.release(project_name=project_name,
version=version_name).demand(
+ ValueError("Release not found")
+ )
+ url_list_str = await _generate_file_url_list(release)
+ return quart.Response(url_list_str + "\n", mimetype="text/plain")
+ except ValueError as e:
+ return quart.Response(f"Error: {e}", status=404, mimetype="text/plain")
+ except Exception as e:
+ return quart.Response(f"Internal server error: {e}", status=500,
mimetype="text/plain")
+
+
+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)
# Check that path is relative
- path = pathlib.Path(file_path)
- if not path.is_relative_to(path.anchor):
+ original_path = pathlib.Path(file_path)
+ if (file_path != ".") and (not
original_path.is_relative_to(original_path.anchor)):
raise routes.FlashError("Path must be relative")
- release = await session.release(project_name, version_name, phase=None)
+ # We allow downloading files from any phase
+ async with db.session() as session:
+ release = await session.release(project_name=project_name,
version=version_name).demand(
+ base.ASFQuartException("Release does not exist", errorcode=404)
+ )
# logging.warning(f"Downloading {file_path} from {release}")
full_path = util.release_directory(release) / file_path
- # logging.warning(f"Full path: {full_path}")
- # Check that the file exists
- if not await aiofiles.os.path.exists(full_path):
+ if await aiofiles.os.path.isdir(full_path):
+ return await _list(original_path, full_path, project_name,
version_name, file_path)
+
+ # Check that the path is a regular file
+ if not await aiofiles.os.path.isfile(full_path):
# Even using the following type declaration, mypy does not know the
type
# The same pattern is used in release.py, so this is a bug in mypy
# TODO: Report the bug upstream to mypy
- return await session.redirect(root.index, error="File not found")
+ await quart.flash("File or directory not found", "error")
+ return quart.redirect(util.as_url(root.index))
# Send the file with original filename
return await quart.send_file(
- full_path, as_attachment=True, attachment_filename=path.name,
mimetype="application/octet-stream"
+ full_path, as_attachment=True, attachment_filename=original_path.name,
mimetype="application/octet-stream"
)
+
+
+async def _generate_file_url_list(release: models.Release) -> str:
+ base_dir = util.release_directory(release)
+ urls = []
+ 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):
+ abs_url = util.as_url(
+ path,
+ project_name=release.project_name,
+ version_name=release.version,
+ file_path=str(rel_path),
+ _external=True,
+ )
+ urls.append(abs_url)
+ return "\n".join(sorted(urls))
+
+
+async def _list(
+ original_path: pathlib.Path, full_path: pathlib.Path, project_name: str,
version_name: str, file_path: str
+) -> response.Response | quart.Response:
+ # Build a list of files in the directory
+ files: list[pathlib.Path] = []
+ for file in await aiofiles.os.listdir(full_path):
+ file_in_dir = pathlib.Path(file)
+ # Include subdirectories in listing
+ is_file = await aiofiles.os.path.isfile(full_path / file_in_dir)
+ is_dir = await aiofiles.os.path.isdir(full_path / file_in_dir)
+ if is_file or is_dir:
+ files.append(file_in_dir)
+ files.sort()
+ html = []
+
+ # Add link to parent directory if not at root
+ if file_path != ".":
+ parent_path_str = str(original_path.parent)
+ parent_link_url = util.as_url(
+ path,
+ project_name=project_name,
+ version_name=version_name,
+ file_path=parent_path_str,
+ )
+ html.append(f'<a href="{parent_link_url}">../</a>')
+
+ # List files and directories
+ for item_in_dir in files:
+ relative_path_str = str(pathlib.Path(file_path) / item_in_dir)
+ link_url = util.as_url(
+ path,
+ project_name=project_name,
+ version_name=version_name,
+ file_path=relative_path_str,
+ )
+ display_name = f"{item_in_dir}/" if await
aiofiles.os.path.isdir(full_path / item_in_dir) else str(item_in_dir)
+ html.append(f'<a href="{link_url}">{display_name}</a>')
+ body = "<br>\n".join(html)
+ return quart.Response(body, mimetype="text/html")
diff --git a/atr/routes/mapping.py b/atr/routes/mapping.py
index e3bc8da..9dfc81c 100644
--- a/atr/routes/mapping.py
+++ b/atr/routes/mapping.py
@@ -18,16 +18,19 @@
import atr.db.models as models
import atr.routes.compose as compose
import atr.routes.finish as finish
+import atr.routes.release as routes_release
import atr.routes.vote as vote
import atr.util as util
def release_as_url(release: models.Release) -> str:
- if release.phase.value == "release_candidate_draft":
- return util.as_url(compose.selected,
project_name=release.project.name, version_name=release.version)
- elif release.phase.value == "release_candidate":
- return util.as_url(vote.selected, project_name=release.project.name,
version_name=release.version)
- elif release.phase.value == "release_preview":
- return util.as_url(finish.selected, project_name=release.project.name,
version_name=release.version)
- else:
- raise ValueError(f"Unknown release phase: {release.phase}")
+ match release.phase:
+ case models.ReleasePhase.RELEASE_CANDIDATE_DRAFT:
+ return util.as_url(compose.selected,
project_name=release.project.name, version_name=release.version)
+ case models.ReleasePhase.RELEASE_CANDIDATE:
+ return util.as_url(vote.selected,
project_name=release.project.name, version_name=release.version)
+ case models.ReleasePhase.RELEASE_PREVIEW:
+ return util.as_url(finish.selected,
project_name=release.project.name, version_name=release.version)
+ case models.ReleasePhase.RELEASE:
+ view = routes_release.view # type: ignore[has-type]
+ return util.as_url(view, project_name=release.project.name,
version_name=release.version)
diff --git a/atr/templates/candidate-resolve-release.html
b/atr/templates/candidate-resolve-release.html
index b370e64..219fb91 100644
--- a/atr/templates/candidate-resolve-release.html
+++ b/atr/templates/candidate-resolve-release.html
@@ -58,9 +58,11 @@
</div>
<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">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>
<a href="{{ as_url(routes.vote.selected,
project_name=release.project.name, version_name=release.version) }}"
- class="btn btn-success">Vote on release</a>
+ class="btn btn-success"><i class="bi bi-check-circle me-1"></i> Vote
on release</a>
</div>
</div>
diff --git a/atr/templates/check-selected.html
b/atr/templates/check-selected.html
index 230eace..82d6e44 100644
--- a/atr/templates/check-selected.html
+++ b/atr/templates/check-selected.html
@@ -99,9 +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">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>
<a href="{{ as_url(routes.resolve.selected,
project_name=release.project.name, version_name=release.version) }}"
- class="btn btn-success">Resolve vote</a>
+ class="btn btn-success"><i class="bi bi-clipboard-check
me-1"></i>Resolve vote</a>
</div>
{% endif %}
</div>
@@ -115,6 +117,9 @@
<a href="{{ as_url(routes.upload.selected,
project_name=release.project.name, version_name=release.version) }}"
title="Upload files to this draft"
class="btn btn-primary"><i class="bi bi-upload me-1"></i> Upload
files</a>
+ <a href="{{ as_url(routes.download.all_selected,
project_name=release.project.name, version_name=release.version) }}"
+ title="Download files"
+ class="btn btn-primary"><i class="bi bi-download me-1"></i>
Download files</a>
<a href="{{ as_url(routes.revisions.selected,
project_name=release.project.name, version_name=release.version) }}"
title="View revision history"
class="btn btn-secondary"><i class="bi bi-clock-history me-1"></i>
Revisions</a>
@@ -236,7 +241,7 @@
More
</button>
{% elif phase == "release_candidate" %}
- <a href="{{ as_url(routes.download.phase,
phase='candidate', project_name=release.project.name,
version_name=release.version, file_path=path) }}"
+ <a href="{{ as_url(routes.download.path,
project_name=release.project.name, version_name=release.version,
file_path=path) }}"
title="Download file {{ path }}"
class="btn btn-sm
btn-outline-secondary">Download</a>
{% endif %}
@@ -252,7 +257,7 @@
<div class="btn-group btn-group-sm"
role="group"
aria-label="More file actions for {{ path }}">
- <a href="{{ as_url(routes.download.phase,
phase='candidate-draft', project_name=release.project.name,
version_name=release.version, file_path=path) }}"
+ <a href="{{ as_url(routes.download.path,
project_name=release.project.name, version_name=release.version,
file_path=path) }}"
title="Download file {{ path }}"
class="btn btn-outline-secondary">Download</a>
<a href="{{ as_url(routes.draft.tools,
project_name=project_name, version_name=version_name, file_path=path) }}"
@@ -325,15 +330,6 @@
</div>
</form>
{% endif %}
-
- <h2 id="rsync-download">Rsync download</h2>
- <p>You can download 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" %}
-
{% endblock content %}
{% block javascripts %}
diff --git a/atr/templates/download-all.html b/atr/templates/download-all.html
new file mode 100644
index 0000000..86fd352
--- /dev/null
+++ b/atr/templates/download-all.html
@@ -0,0 +1,88 @@
+{% extends "layouts/base.html" %}
+
+{% block title %}
+ Download {{ release.project.display_name }} {{ release.version }} ~ ATR
+{% endblock title %}
+
+{% block description %}
+ Download commands for {{ release.project.display_name }} {{ release.version
}}.
+{% endblock description %}
+
+{% block content %}
+ {% set phase = release.phase.value %}
+ <p class="d-flex justify-content-between align-items-center">
+ <a href="{{ back_url }}" class="atr-back-link">← Back to {{
phase|replace('_', ' ') |title }}</a>
+ {% if phase != "release" %}
+ <span>
+ {% if phase == "release_candidate_draft" %}
+ <strong class="atr-phase-one atr-phase-symbol">①</strong>
+ <span class="atr-phase-one atr-phase-label">COMPOSE</span>
+ <span class="atr-phase-arrow">→</span>
+ <span class="atr-phase-symbol-other">②</span>
+ <span class="atr-phase-arrow">→</span>
+ <span class="atr-phase-symbol-other">③</span>
+ {% elif phase == "release_candidate" %}
+ <span class="atr-phase-symbol-other">①</span>
+ <span class="atr-phase-arrow">→</span>
+ <strong class="atr-phase-two atr-phase-symbol">②</strong>
+ <span class="atr-phase-two atr-phase-label">VOTE</span>
+ <span class="atr-phase-arrow">→</span>
+ <span class="atr-phase-symbol-other">③</span>
+ {% elif phase == "release_preview" %}
+ <span class="atr-phase-symbol-other">①</span>
+ <span class="atr-phase-arrow">→</span>
+ <span class="atr-phase-symbol-other">②</span>
+ <span class="atr-phase-arrow">→</span>
+ <strong class="atr-phase-three atr-phase-symbol">③</strong>
+ <span class="atr-phase-three atr-phase-label">FINISH</span>
+ {% endif %}
+ </span>
+ {% endif %}
+ </p>
+
+ <h1>
+ Download all files for <strong>{{ release.project.short_display_name
}}</strong> <em>{{ release.version }}</em>
+ </h1>
+
+ <p>
+ <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>
+ <!-- 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>
+ <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 %}
+ {{ super() }}
+{% endblock stylesheets %}
+
+{% block javascripts %}
+ {{ super() }}
+{% endblock javascripts %}
diff --git a/atr/templates/finish-selected.html
b/atr/templates/finish-selected.html
index 6fe03c7..ec9118b 100644
--- a/atr/templates/finish-selected.html
+++ b/atr/templates/finish-selected.html
@@ -37,19 +37,25 @@
<div>
<a title="Show files for {{ release.name }}"
href="{{ as_url(routes.preview.view,
project_name=release.project.name, version_name=release.version) }}"
- class="btn btn-sm btn-secondary me-2">
+ class="btn btn-secondary me-2">
<i class="bi bi-archive"></i>
Show files
</a>
+ <a title="Download all files"
+ href="{{ as_url(routes.download.all_selected,
project_name=release.project.name, version_name=release.version) }}"
+ class="btn btn-secondary me-2">
+ <i class="bi bi-download"></i>
+ Download all files
+ </a>
<a title="Show revisions for {{ release.name }}"
href="{{ as_url(routes.revisions.selected,
project_name=release.project.name, version_name=release.version) }}"
- class="btn btn-sm btn-secondary me-2">
+ class="btn btn-secondary me-2">
<i class="bi bi-clock-history"></i>
Show revisions
</a>
<a title="Announce and distribute {{ release.name }}"
href="{{ as_url(routes.announce.selected,
project_name=release.project.name, version_name=release.version) }}"
- class="btn btn-sm btn-success">
+ class="btn btn-success">
<i class="bi bi-check-circle"></i>
Announce and distribute
</a>
@@ -99,15 +105,6 @@
File moving is disabled as all files are currently in the same directory
or the revision is empty.
</div>
{% endif %}
-
- <h2 id="rsync-download">Rsync download</h2>
- <p>You can download 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" %}
-
{% endblock content %}
{% block javascripts %}
diff --git a/atr/templates/releases.html b/atr/templates/releases.html
index 17adeac..e36e55b 100644
--- a/atr/templates/releases.html
+++ b/atr/templates/releases.html
@@ -36,7 +36,9 @@
</div>
<div class="d-flex gap-3 align-items-center pt-2">
<a class="btn btn-outline-primary"
- href="{{ as_url(routes.release.view,
project_name=release.project.name, version_name=release.version) }}">View
files</a>
+ href="{{ as_url(routes.release.view,
project_name=release.project.name, version_name=release.version) }}"><i
class="bi bi-eye me-1"></i> View files</a>
+ <a class="btn btn-outline-primary"
+ href="{{ as_url(routes.download.all_selected,
project_name=release.project.name, version_name=release.version) }}"><i
class="bi bi-download me-1"></i> Download files</a>
</div>
</div>
</div>
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]