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]

Reply via email to