Преглед на файлове

DCBA repo browse view and artifact detail page

/repo/{name} for a DCBA artifact repository now renders a real browse
view instead of erroring: the hero carries a DCBA chip, the artifact
count and artifact byte total, the add-repository card uses the dcba
wording with the "sudo usm dcba add-repo <base>repo/<name>/repo.dcbar"
snippet, and an Artifacts table lists every ARTIFACTS.usml entry (name
linking to the detail page, version, size or "?", download link). The
USM source repository rendering is byte-identical to before: the page
branches on the new state "repo_kind" field, the usmc sections keep
their exact markup, and the search form stays usmc-only.

New /repo/{name}/artifact/{file} page: hero with the artifact name and
version taken from the listing entry (never re-parsed from the
filename), repository breadcrumb, SHA-512/size/modified stats and a
download button; a hint card explains what a .dcba is and shows the
add-repo snippet with the repo's rewritten repo.dcbar URL; and a
one-directional "Source package" cross-link appears only when a loaded
USM repository's listing carries a package with the same name AND the
same version string (plain comparison). Unknown artifact files (and
artifact routes on a USM repository) redirect back to the repository
browse page.

PackagesState builds the DCBA repo-scoped state that replaces its old
INVALID_CONFIGURATION throw; the overview build is unchanged. The page
is registered between the spry markers in main.vala and meson.build
(ArtifactDetailPage).

Verified against a mixed fixture (one .usmr repo with app-1.0.0.usmc,
one .dcbar repo with app-1.0.0.dcba + other-2.0.0.dcba) built with the
fresh usm CLI: 27 curl assertions on the statum entrypoint states and
served pages, byte-matched .dcba downloads, and an end-to-end
"usm dcba add-repo" against the running instance.
saddle-Clanker-Saddle преди 1 месец
родител
ревизия
c61fa73479
променени са 7 файла, в които са добавени 305 реда и са изтрити 23 реда
  1. 8 0
      meson.build
  2. 60 13
      src/PackagesState.vala
  3. 119 0
      src/entrypoints/ArtifactDetailEntrypoint.vala
  4. 7 3
      src/entrypoints/RepoBrowseEntrypoint.vala
  5. 1 0
      src/main.vala
  6. 57 0
      src/pages/artifact.html
  7. 53 7
      src/pages/repo.html

+ 8 - 0
meson.build

@@ -49,6 +49,12 @@ package_detail_page = custom_target('package-detail-page',
     command: [statum_mkpstm, '-o', '@OUTPUT@', '-n', 'PackageDetailPage', '--ns', 'UsmWeb', '@INPUT@'],
     depend_files: files('src/pages/main.html')
 )
+artifact_detail_page = custom_target('artifact-detail-page',
+    input: 'src/pages/artifact.html',
+    output: 'ArtifactDetailPage.vala',
+    command: [statum_mkpstm, '-o', '@OUTPUT@', '-n', 'ArtifactDetailPage', '--ns', 'UsmWeb', '@INPUT@'],
+    depend_files: files('src/pages/main.html')
+)
 # spry:pages-end
 
 app_sources = files(
@@ -61,6 +67,7 @@ app_sources = files(
     'src/entrypoints/HomeEntrypoint.vala',
     'src/entrypoints/RepoBrowseEntrypoint.vala',
     'src/entrypoints/PackageDetailEntrypoint.vala',
+    'src/entrypoints/ArtifactDetailEntrypoint.vala',
     'src/actions/SearchPackagesAction.vala',
     'src/endpoints/PackagesListingEndpoint.vala',
     'src/endpoints/ArtifactsListingEndpoint.vala',
@@ -77,6 +84,7 @@ app_generated = [
     home_page,
     repo_browse_page,
     package_detail_page,
+    artifact_detail_page,
     # spry:generated-end
 ]
 

+ 60 - 13
src/PackagesState.vala

@@ -16,6 +16,21 @@ namespace UsmWeb {
         public string repo { get; set; default = ""; }
     }
 
+    /**
+     * One artifact row in a DCBA repository's browse table: the listing
+     * entry's name/version plus the page links the row renders — the
+     * detail page ({@link href}) and the raw `.dcba` download
+     * ({@link download_href}).
+     */
+    public class ArtifactRow : Object {
+        public string name { get; set; default = ""; }
+        public string version { get; set; default = ""; }
+        public string size { get; set; default = ""; }
+        public string file { get; set; default = ""; }
+        public string href { get; set; default = ""; }
+        public string download_href { get; set; default = ""; }
+    }
+
     /**
      * One repository summary in the overview: the stats card fields plus the
      * per-repository add-repository instruction fields ({@link base_uri},
@@ -75,16 +90,20 @@ namespace UsmWeb {
          * pre-built artifacts surface in the detail views, never in the
          * home package table.
          *
-         * A repo-scoped build for a DCBA repository throws instead: the
-         * package-table state this builder authors does not describe an
-         * artifact repository, so the browse entrypoint degrades to its
-         * error card for those names (the dedicated DCBA browse view owns
-         * that page).
+         * A repo-scoped build for a DCBA repository renders that
+         * repository's browse view instead: the same overview-card
+         * fields (carrying its artifact count and artifact byte total),
+         * the `usm dcba add-repo` instruction and one
+         * {@link ArtifactRow} per `ARTIFACTS.usml` entry. The query
+         * never filters artifacts (search is source-package-only), so
+         * those rows ignore `query` entirely and the page omits the
+         * search form on the DCBA branch.
          */
         public static State build(RepositoryService repositories, UsmWebConfig config,
                 ActionRegistry action_registry, string base_uri, string query, string? repo_name = null) throws Error {
             var repo_rows = new Vector<RepoSummary>();
             var package_rows = new Vector<PackageRow>();
+            var dcba_artifact_rows = new Vector<ArtifactRow>();
             int64 total_bytes = 0;
             uint total_count = 0;
             uint matched = 0;
@@ -96,16 +115,31 @@ namespace UsmWeb {
                 }
 
                 if (entry.kind == RepositoryKind.DCBA) {
-                    if (repo_name != null) {
-                        throw new UsmWebError.INVALID_CONFIGURATION(
-                            @"Repository \"$repo_name\" is a DCBA artifact repository — its pre-built artifacts are not listed as packages");
-                    }
-
                     int64 artifact_bytes = 0;
                     uint artifact_count = 0;
-                    foreach (var artifact_entry in entry.dcba_listing_entries()) {
-                        artifact_bytes += (int64) artifact_entry.size;
-                        artifact_count++;
+                    var artifact_rows = new Vector<ArtifactRow>();
+                    if (repo_name != null) {
+                        foreach (var artifact_entry in entry.dcba_listing_entries()) {
+                            var file = Path.get_basename(artifact_entry.path);
+                            artifact_bytes += (int64) artifact_entry.size;
+                            artifact_count++;
+                            artifact_rows.add(new ArtifactRow() {
+                                name = artifact_entry.name,
+                                version = artifact_entry.version,
+                                size = artifact_entry.size > 0
+                                    ? format_size((int64) artifact_entry.size)
+                                    : "?",
+                                file = file,
+                                href = "/repo/" + repo.name + "/artifact/" + file,
+                                download_href = "/repo/" + repo.name + "/" + file
+                            });
+                        }
+                        dcba_artifact_rows.add_all(artifact_rows);
+                    } else {
+                        foreach (var artifact_entry in entry.dcba_listing_entries()) {
+                            artifact_bytes += (int64) artifact_entry.size;
+                            artifact_count++;
+                        }
                     }
 
                     repo_rows.add(new RepoSummary() {
@@ -180,13 +214,17 @@ namespace UsmWeb {
                 var summary = repo_rows.first();
                 dict.set_native<string>("repo_name", summary.name);
                 dict.set_native<string>("repo_summary", summary.summary);
+                dict.set_native<string>("repo_kind", summary.kind);
                 dict.set_native<string>("key_fingerprint", summary.key_fingerprint);
                 dict.set_native<int>("package_count", summary.package_count);
                 dict.set_native<string>("total_size", summary.total_size);
                 dict.set_native<string>("base_uri", summary.base_uri);
                 dict.set_native<string>("usmr_href", summary.usmr_href);
                 dict.set_native<string>("repos_d_snippet", summary.repos_d_snippet);
+                dict.set_native<int>("artifact_count", summary.artifact_count);
+                dict.set_native<string>("artifact_total_size", summary.artifact_total_size);
             }
+            dict["artifacts"] = artifact_list(dcba_artifact_rows);
             dict.set_native<string>("query", query);
             dict.set_native<string>("result_note", result_note(query, matched, total_count));
             dict.set_native<string>("installer_mode", config.installer.to_string());
@@ -221,6 +259,15 @@ namespace UsmWeb {
             return new JsonElement.from_elements(arr);
         }
 
+        /** Builds the artifact rows' JSON array (the same pattern as {@link list}). */
+        public static JsonElement artifact_list(Lot<ArtifactRow> rows) throws Error {
+            var arr = new JsonArray();
+            foreach (var row in rows) {
+                arr.add(new JsonElement.from_properties(GObjectMapping.to_properties(row)));
+            }
+            return new JsonElement.from_elements(arr);
+        }
+
         /** Builds the repository summaries' JSON array. */
         public static JsonElement repo_list(Lot<RepoSummary> rows) throws Error {
             var arr = new JsonArray();

+ 119 - 0
src/entrypoints/ArtifactDetailEntrypoint.vala

@@ -0,0 +1,119 @@
+using Invercargill;
+using Invercargill.DataStructures;
+using InvercargillJson;
+using Statum;
+using Usm;
+
+namespace UsmWeb {
+
+    /**
+     * Entrypoint for /repo/{name}/artifact/{file}: hydrates the `artifact`
+     * PAGE slot from that DCBA repository's `ARTIFACTS.usml` entry whose
+     * path basename is `file` — the name and version shown come from the
+     * listing entry, never re-parsed out of the filename — plus the
+     * `.dcba`'s on-disk size/mtime and the listing's sha512.
+     *
+     * When a loaded USM source repository's `PACKAGES.usml` carries a
+     * package with the same name AND the same version string (a plain
+     * string comparison on both, mirroring how DCBA clients match
+     * artifacts), the page cross-links to that package's detail view.
+     * Unknown repositories redirect to the overview; unknown artifact
+     * files redirect to the repository's browse page.
+     */
+    public class ArtifactDetailEntrypoint : StatumEntrypoint {
+
+        protected RepositoryService repositories = Inversion.inject<RepositoryService>();
+        protected UsmWebConfig config = Inversion.inject<UsmWebConfig>();
+        protected Astralis.HttpContext http_context = Inversion.inject<Astralis.HttpContext>();
+
+        public override async DirectiveBuilder handle() throws GLib.Error {
+            var name = request.route("name") ?? "";
+            var file = request.route("file") ?? "";
+            var base_uri = DerivedBase.derive(http_context.request, config);
+
+            var entry = repositories.find(name);
+            if (entry == null) {
+                return directives().navigate("/");
+            }
+            if (((!)entry).kind != RepositoryKind.DCBA) {
+                return directives().navigate(@"/repo/$name");
+            }
+
+            DcbaListingEntry? listing_entry = null;
+            foreach (var artifact_entry in ((!)entry).dcba_listing_entries()) {
+                if (Path.get_basename(artifact_entry.path) == file) {
+                    listing_entry = artifact_entry;
+                    break;
+                }
+            }
+            if (listing_entry == null) {
+                return directives().navigate(@"/repo/$name");
+            }
+
+            var repo = ((!)entry).repo();
+            var path = ((!)entry).artifact_location(file);
+            var info = file_info(path);
+
+            var dict = new PropertyDictionary();
+            dict.set_native<string>("name", ((!)listing_entry).name);
+            dict.set_native<string>("version", ((!)listing_entry).version);
+            dict.set_native<string>("repo", repo.name);
+            dict.set_native<string>("file", file);
+            dict.set_native<string>("download_href", @"/repo/$name/$file");
+            dict.set_native<string>("size", ((!)listing_entry).size > 0
+                ? PackagesState.format_size((int64)((!)listing_entry).size)
+                : (info.size > 0 ? PackagesState.format_size(info.size) : "?"));
+            dict.set_native<string>("modified", format_mtime(info.modified));
+            dict.set_native<string>("sha512", PackagesState.fingerprint(((!)listing_entry).sha512sum.to_hex()));
+            dict.set_native<string>("repo_dcbar_href", base_uri + "repo/" + repo.name + "/repo.dcbar");
+            dict.set_native<string>("repos_d_snippet", "sudo usm dcba add-repo %srepo/%s/repo.dcbar"
+                .printf(base_uri, repo.name));
+
+            // Source-package cross-link, one direction only: the first
+            // USM repository whose listing carries a package with the
+            // same name AND version string. Plain string comparison on
+            // both — no Version parse round trip, matching how DCBA
+            // clients match artifacts.
+            foreach (var usm_entry in repositories.usm_entries()) {
+                foreach (var listing in usm_entry.listing_entries()) {
+                    if (listing.manifest.name == ((!)listing_entry).name
+                            && listing.manifest.version.to_string() == ((!)listing_entry).version) {
+                        dict.set_native<string>("source_package_repo", usm_entry.repo().name);
+                        dict.set_native<string>("source_package_href",
+                            @"/repo/$(usm_entry.repo().name)/package/$(Path.get_basename(listing.path))");
+                        break;
+                    }
+                }
+                if (dict.has("source_package_href")) {
+                    break;
+                }
+            }
+
+            var state = new State() {
+                type_name = "artifact",
+                public_data = dict,
+                private_data = new PropertyDictionary()
+            };
+            return directives().set(state_service.new_slot(Scope.PAGE, state));
+        }
+
+        private static FileStat file_info(string path) throws Error {
+            var info = File.new_for_path(path).query_info(
+                FileAttribute.STANDARD_SIZE + "," + FileAttribute.TIME_MODIFIED,
+                FileQueryInfoFlags.NONE);
+            int64 size = info.get_size();
+            int64 modified = (int64) info.get_attribute_uint64(FileAttribute.TIME_MODIFIED);
+            return new FileStat() { size = size, modified = modified };
+        }
+
+        private static string format_mtime(int64 unix_seconds) {
+            var dt = new DateTime.from_unix_local(unix_seconds);
+            return dt.format("%Y-%m-%d %H:%M");
+        }
+
+        private class FileStat : Object {
+            public int64 size { get; set; }
+            public int64 modified { get; set; }
+        }
+    }
+}

+ 7 - 3
src/entrypoints/RepoBrowseEntrypoint.vala

@@ -7,9 +7,13 @@ namespace UsmWeb {
     /**
      * Entrypoint for /repo/{name}: hydrates the
      * {@link PackagesState.SLOT_TYPE} PAGE slot scoped to one repository —
-     * its overview card, add-repository instructions and package table.
-     * Unknown repositories redirect to the overview; a load failure degrades
-     * to an error card instead of a failed hydration.
+     * its overview card, add-repository instructions and the table of what
+     * it carries: the package table for a USM source repository, the
+     * artifact table for a DCBA artifact repository (the state's `kind`
+     * picks the branch the page renders; a DCBA repository never shows a
+     * search form — search is source-package-only). Unknown repositories
+     * redirect to the overview; a load failure degrades to an error card
+     * instead of a failed hydration.
      */
     public class RepoBrowseEntrypoint : StatumEntrypoint {
 

+ 1 - 0
src/main.vala

@@ -40,6 +40,7 @@ int main(string[] args) {
         statum.add_page<HomePage, HomeEntrypoint>();
         statum.add_page<RepoBrowsePage, RepoBrowseEntrypoint>();
         statum.add_page<PackageDetailPage, PackageDetailEntrypoint>();
+        statum.add_page<ArtifactDetailPage, ArtifactDetailEntrypoint>();
         // spry:pages-end
 
         // spry:actions-begin

+ 57 - 0
src/pages/artifact.html

@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <pstm-uri>/repo/{name}/artifact/{file}</pstm-uri>
+    <pstm-template>main</pstm-template>
+</head>
+<body>
+    <div stm-if="artifact != null">
+        <p class="breadcrumb"><a href="/">← All repositories</a>&nbsp;/&nbsp;<a
+            stm-attribute.href="'/repo/' + artifact.repo" stm-text="artifact.repo">repository</a></p>
+
+        <section class="card hero">
+            <div class="chips">
+                <span class="chip mono">DCBA · pre-built artifact</span>
+            </div>
+            <div class="detail-head">
+                <div>
+                    <h1 stm-text="artifact.name">artifact</h1>
+                    <p class="version mono" stm-text="artifact.version">0.0</p>
+                </div>
+                <a class="button" stm-attribute.href="artifact.download_href">Download
+                    <span stm-text="artifact.size">0 B</span></a>
+            </div>
+            <dl class="stats">
+                <div class="stat-wide"><dt>SHA-512</dt>
+                    <dd class="mono fingerprint" stm-text="artifact.sha512">—</dd></div>
+                <div><dt>Artifact</dt><dd class="mono" stm-text="artifact.file">file.dcba</dd></div>
+                <div><dt>Size</dt><dd class="mono" stm-text="artifact.size">—</dd></div>
+                <div><dt>Modified</dt><dd class="mono" stm-text="artifact.modified">—</dd></div>
+            </dl>
+        </section>
+
+        <section class="card" stm-if="artifact.source_package_href != null">
+            <h2>Source package</h2>
+            <p>This artifact was built from the source package of the same name and
+                version in the
+                <a stm-attribute.href="artifact.source_package_href"
+                   stm-text="artifact.source_package_repo">repository</a> repository.</p>
+            <p class="hint">The link is one-directional on purpose: a package page
+                lists every DCBA repository carrying its pre-built artifacts.</p>
+        </section>
+
+        <section class="card">
+            <h2>What is a .dcba?</h2>
+            <p>A <span class="mono">.dcba</span> is a pre-built build artifact
+                distributed by a DCBA artifact repository. When this repository is
+                configured, <span class="mono">usm install</span> fetches it
+                automatically instead of building from source.</p>
+            <p>Add this repository with USM:</p>
+            <pre class="mono snippet"><code stm-text="artifact.repos_d_snippet"></code></pre>
+            <p class="hint">The signed repository definition:
+                <a class="mono" stm-attribute.href="artifact.repo_dcbar_href"
+                   stm-text="artifact.repo_dcbar_href">repo.dcbar</a></p>
+        </section>
+    </div>
+</body>
+</html>

+ 53 - 7
src/pages/repo.html

@@ -15,11 +15,18 @@
         <p class="breadcrumb"><a href="/">← All repositories</a></p>
 
         <section class="card hero">
+            <div class="chips" stm-if="packages.repo_kind == 'dcba'">
+                <span class="chip mono">DCBA · pre-built artifacts</span>
+            </div>
             <h1 stm-text="packages.repo_name">Repository</h1>
             <p class="summary" stm-text="packages.repo_summary">Loading…</p>
             <dl class="stats">
-                <div><dt>Packages</dt><dd stm-text="packages.package_count">—</dd></div>
-                <div><dt>Total size</dt><dd stm-text="packages.total_size">—</dd></div>
+                <div stm-if="packages.repo_kind == 'dcba'"><dt>Artifacts</dt>
+                    <dd stm-text="packages.artifact_count">—</dd></div>
+                <div stm-if="packages.repo_kind == 'dcba'"><dt>Total artifact size</dt>
+                    <dd stm-text="packages.artifact_total_size">—</dd></div>
+                <div stm-if="packages.repo_kind == 'usm'"><dt>Packages</dt><dd stm-text="packages.package_count">—</dd></div>
+                <div stm-if="packages.repo_kind == 'usm'"><dt>Total size</dt><dd stm-text="packages.total_size">—</dd></div>
                 <div class="stat-wide"><dt>Signing key fingerprint</dt>
                     <dd class="mono fingerprint" stm-text="packages.key_fingerprint">—</dd></div>
             </dl>
@@ -34,21 +41,60 @@
         </section>
 
         <section class="card">
-            <h2>Add this repository</h2>
-            <p>Point USM at this repository — its URI is
+            <h2 stm-if="packages.repo_kind == 'dcba'">Add this artifact repository</h2>
+            <h2 stm-if="packages.repo_kind == 'usm'">Add this repository</h2>
+            <p stm-if="packages.repo_kind == 'dcba'">Point USM at this DCBA artifact repository — its URI is
+                <code class="mono" stm-text="packages.base_uri">…</code></p>
+            <p stm-if="packages.repo_kind == 'usm'">Point USM at this repository — its URI is
                 <code class="mono" stm-text="packages.base_uri">…</code></p>
             <ol class="steps">
                 <li>The signed repository definition:
                     <a class="mono" stm-attribute.href="packages.usmr_href"
                        stm-text="packages.usmr_href">repo.usmr</a></li>
-                <li>Add it with USM:</li>
+                <li stm-if="packages.repo_kind == 'dcba'">Add it with USM (DCBA repositories use the
+                    <code class="mono">dcba</code> subcommand):</li>
+                <li stm-if="packages.repo_kind == 'usm'">Add it with USM:</li>
             </ol>
             <pre class="mono snippet"><code stm-text="packages.repos_d_snippet"></code></pre>
-            <p class="hint">Then <code class="mono">usm repository list</code> should show
+            <p class="hint" stm-if="packages.repo_kind == 'dcba'">Then <code class="mono">usm dcba repository list</code> should show
+                <span class="mono" stm-text="packages.repo_name">the repository</span>.</p>
+            <p class="hint" stm-if="packages.repo_kind == 'usm'">Then <code class="mono">usm repository list</code> should show
                 <span class="mono" stm-text="packages.repo_name">the repository</span>.</p>
         </section>
 
-        <section class="card">
+        <section class="card" stm-if="packages.repo_kind == 'dcba'">
+            <h2>Artifacts</h2>
+            <p class="summary">Pre-built build artifacts served by this DCBA repository —
+                fetched automatically by <span class="mono">usm install</span> when this
+                repository is configured. They are not source packages.</p>
+            <div class="table-scroll">
+                <table>
+                    <thead>
+                        <tr>
+                            <th>Name</th>
+                            <th>Version</th>
+                            <th class="num">Size</th>
+                            <th class="num">Download</th>
+                        </tr>
+                    </thead>
+                    <tbody>
+                        <tr stm-for-artifact-in="packages.artifacts" stm-key="artifact.file">
+                            <td><a stm-attribute.href="artifact.href"
+                               stm-text="artifact.name">artifact</a></td>
+                            <td class="mono" stm-text="artifact.version">0.0</td>
+                            <td class="num mono" stm-text="artifact.size">0 B</td>
+                            <td class="num"><a class="download" title="Download .dcba"
+                               stm-attribute.href="artifact.download_href">&#8681;</a></td>
+                        </tr>
+                        <tr stm-if="packages.artifacts.length == 0">
+                            <td colspan="4" class="empty">This repository lists no artifacts.</td>
+                        </tr>
+                    </tbody>
+                </table>
+            </div>
+        </section>
+
+        <section class="card" stm-if="packages.repo_kind == 'usm'">
             <h2>Packages</h2>
             <form class="search" stm-action="packages.search">
                 <input type="hidden" name="repo" stm-attribute.value="packages.repo_name">