Commit

Author:

Hash:

Timestamp:

+362 -37 +/-9 browse

Kevin Schoon [me@kevinschoon.com]

50721fa0683a63b18a093b27915a4a5cfe2509c7

Thu, 24 Apr 2025 13:31:39 +0000 (1 month ago)

more ui features
1diff --git a/Cargo.lock b/Cargo.lock
2index ef5e606..8a14aee 100644
3--- a/Cargo.lock
4+++ b/Cargo.lock
5 @@ -310,6 +310,17 @@ dependencies = [
6 ]
7
8 [[package]]
9+ name = "displaydoc"
10+ version = "0.2.5"
11+ source = "registry+https://github.com/rust-lang/crates.io-index"
12+ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
13+ dependencies = [
14+ "proc-macro2",
15+ "quote",
16+ "syn",
17+ ]
18+
19+ [[package]]
20 name = "fnv"
21 version = "1.0.7"
22 source = "registry+https://github.com/rust-lang/crates.io-index"
23 @@ -547,12 +558,151 @@ dependencies = [
24 ]
25
26 [[package]]
27+ name = "icu_collections"
28+ version = "1.5.0"
29+ source = "registry+https://github.com/rust-lang/crates.io-index"
30+ checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526"
31+ dependencies = [
32+ "displaydoc",
33+ "yoke",
34+ "zerofrom",
35+ "zerovec",
36+ ]
37+
38+ [[package]]
39+ name = "icu_locid"
40+ version = "1.5.0"
41+ source = "registry+https://github.com/rust-lang/crates.io-index"
42+ checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637"
43+ dependencies = [
44+ "displaydoc",
45+ "litemap",
46+ "tinystr",
47+ "writeable",
48+ "zerovec",
49+ ]
50+
51+ [[package]]
52+ name = "icu_locid_transform"
53+ version = "1.5.0"
54+ source = "registry+https://github.com/rust-lang/crates.io-index"
55+ checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e"
56+ dependencies = [
57+ "displaydoc",
58+ "icu_locid",
59+ "icu_locid_transform_data",
60+ "icu_provider",
61+ "tinystr",
62+ "zerovec",
63+ ]
64+
65+ [[package]]
66+ name = "icu_locid_transform_data"
67+ version = "1.5.1"
68+ source = "registry+https://github.com/rust-lang/crates.io-index"
69+ checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d"
70+
71+ [[package]]
72+ name = "icu_normalizer"
73+ version = "1.5.0"
74+ source = "registry+https://github.com/rust-lang/crates.io-index"
75+ checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f"
76+ dependencies = [
77+ "displaydoc",
78+ "icu_collections",
79+ "icu_normalizer_data",
80+ "icu_properties",
81+ "icu_provider",
82+ "smallvec",
83+ "utf16_iter",
84+ "utf8_iter",
85+ "write16",
86+ "zerovec",
87+ ]
88+
89+ [[package]]
90+ name = "icu_normalizer_data"
91+ version = "1.5.1"
92+ source = "registry+https://github.com/rust-lang/crates.io-index"
93+ checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7"
94+
95+ [[package]]
96+ name = "icu_properties"
97+ version = "1.5.1"
98+ source = "registry+https://github.com/rust-lang/crates.io-index"
99+ checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5"
100+ dependencies = [
101+ "displaydoc",
102+ "icu_collections",
103+ "icu_locid_transform",
104+ "icu_properties_data",
105+ "icu_provider",
106+ "tinystr",
107+ "zerovec",
108+ ]
109+
110+ [[package]]
111+ name = "icu_properties_data"
112+ version = "1.5.1"
113+ source = "registry+https://github.com/rust-lang/crates.io-index"
114+ checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2"
115+
116+ [[package]]
117+ name = "icu_provider"
118+ version = "1.5.0"
119+ source = "registry+https://github.com/rust-lang/crates.io-index"
120+ checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9"
121+ dependencies = [
122+ "displaydoc",
123+ "icu_locid",
124+ "icu_provider_macros",
125+ "stable_deref_trait",
126+ "tinystr",
127+ "writeable",
128+ "yoke",
129+ "zerofrom",
130+ "zerovec",
131+ ]
132+
133+ [[package]]
134+ name = "icu_provider_macros"
135+ version = "1.5.0"
136+ source = "registry+https://github.com/rust-lang/crates.io-index"
137+ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
138+ dependencies = [
139+ "proc-macro2",
140+ "quote",
141+ "syn",
142+ ]
143+
144+ [[package]]
145 name = "ident_case"
146 version = "1.0.1"
147 source = "registry+https://github.com/rust-lang/crates.io-index"
148 checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
149
150 [[package]]
151+ name = "idna"
152+ version = "1.0.3"
153+ source = "registry+https://github.com/rust-lang/crates.io-index"
154+ checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e"
155+ dependencies = [
156+ "idna_adapter",
157+ "smallvec",
158+ "utf8_iter",
159+ ]
160+
161+ [[package]]
162+ name = "idna_adapter"
163+ version = "1.2.0"
164+ source = "registry+https://github.com/rust-lang/crates.io-index"
165+ checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71"
166+ dependencies = [
167+ "icu_normalizer",
168+ "icu_properties",
169+ ]
170+
171+ [[package]]
172 name = "itoa"
173 version = "1.0.15"
174 source = "registry+https://github.com/rust-lang/crates.io-index"
175 @@ -571,6 +721,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
176 checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
177
178 [[package]]
179+ name = "litemap"
180+ version = "0.7.5"
181+ source = "registry+https://github.com/rust-lang/crates.io-index"
182+ checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856"
183+
184+ [[package]]
185 name = "lock_api"
186 version = "0.4.12"
187 source = "registry+https://github.com/rust-lang/crates.io-index"
188 @@ -697,6 +853,7 @@ dependencies = [
189 "tower-http",
190 "tracing",
191 "tracing-subscriber",
192+ "url",
193 "uuid",
194 "walkdir",
195 ]
196 @@ -974,6 +1131,12 @@ dependencies = [
197 ]
198
199 [[package]]
200+ name = "stable_deref_trait"
201+ version = "1.2.0"
202+ source = "registry+https://github.com/rust-lang/crates.io-index"
203+ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
204+
205+ [[package]]
206 name = "strsim"
207 version = "0.11.1"
208 source = "registry+https://github.com/rust-lang/crates.io-index"
209 @@ -1016,6 +1179,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
210 checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
211
212 [[package]]
213+ name = "synstructure"
214+ version = "0.13.1"
215+ source = "registry+https://github.com/rust-lang/crates.io-index"
216+ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
217+ dependencies = [
218+ "proc-macro2",
219+ "quote",
220+ "syn",
221+ ]
222+
223+ [[package]]
224 name = "thiserror"
225 version = "2.0.12"
226 source = "registry+https://github.com/rust-lang/crates.io-index"
227 @@ -1046,6 +1220,16 @@ dependencies = [
228 ]
229
230 [[package]]
231+ name = "tinystr"
232+ version = "0.7.6"
233+ source = "registry+https://github.com/rust-lang/crates.io-index"
234+ checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f"
235+ dependencies = [
236+ "displaydoc",
237+ "zerovec",
238+ ]
239+
240+ [[package]]
241 name = "tokio"
242 version = "1.44.1"
243 source = "registry+https://github.com/rust-lang/crates.io-index"
244 @@ -1202,6 +1386,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
245 checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
246
247 [[package]]
248+ name = "url"
249+ version = "2.5.4"
250+ source = "registry+https://github.com/rust-lang/crates.io-index"
251+ checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60"
252+ dependencies = [
253+ "form_urlencoded",
254+ "idna",
255+ "percent-encoding",
256+ ]
257+
258+ [[package]]
259+ name = "utf16_iter"
260+ version = "1.0.5"
261+ source = "registry+https://github.com/rust-lang/crates.io-index"
262+ checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246"
263+
264+ [[package]]
265+ name = "utf8_iter"
266+ version = "1.0.4"
267+ source = "registry+https://github.com/rust-lang/crates.io-index"
268+ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
269+
270+ [[package]]
271 name = "uuid"
272 version = "1.16.0"
273 source = "registry+https://github.com/rust-lang/crates.io-index"
274 @@ -1368,3 +1575,82 @@ checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c"
275 dependencies = [
276 "bitflags",
277 ]
278+
279+ [[package]]
280+ name = "write16"
281+ version = "1.0.0"
282+ source = "registry+https://github.com/rust-lang/crates.io-index"
283+ checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936"
284+
285+ [[package]]
286+ name = "writeable"
287+ version = "0.5.5"
288+ source = "registry+https://github.com/rust-lang/crates.io-index"
289+ checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
290+
291+ [[package]]
292+ name = "yoke"
293+ version = "0.7.5"
294+ source = "registry+https://github.com/rust-lang/crates.io-index"
295+ checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40"
296+ dependencies = [
297+ "serde",
298+ "stable_deref_trait",
299+ "yoke-derive",
300+ "zerofrom",
301+ ]
302+
303+ [[package]]
304+ name = "yoke-derive"
305+ version = "0.7.5"
306+ source = "registry+https://github.com/rust-lang/crates.io-index"
307+ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154"
308+ dependencies = [
309+ "proc-macro2",
310+ "quote",
311+ "syn",
312+ "synstructure",
313+ ]
314+
315+ [[package]]
316+ name = "zerofrom"
317+ version = "0.1.6"
318+ source = "registry+https://github.com/rust-lang/crates.io-index"
319+ checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5"
320+ dependencies = [
321+ "zerofrom-derive",
322+ ]
323+
324+ [[package]]
325+ name = "zerofrom-derive"
326+ version = "0.1.6"
327+ source = "registry+https://github.com/rust-lang/crates.io-index"
328+ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
329+ dependencies = [
330+ "proc-macro2",
331+ "quote",
332+ "syn",
333+ "synstructure",
334+ ]
335+
336+ [[package]]
337+ name = "zerovec"
338+ version = "0.10.4"
339+ source = "registry+https://github.com/rust-lang/crates.io-index"
340+ checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079"
341+ dependencies = [
342+ "yoke",
343+ "zerofrom",
344+ "zerovec-derive",
345+ ]
346+
347+ [[package]]
348+ name = "zerovec-derive"
349+ version = "0.10.3"
350+ source = "registry+https://github.com/rust-lang/crates.io-index"
351+ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"
352+ dependencies = [
353+ "proc-macro2",
354+ "quote",
355+ "syn",
356+ ]
357 diff --git a/Cargo.toml b/Cargo.toml
358index 1348aac..360944a 100644
359--- a/Cargo.toml
360+++ b/Cargo.toml
361 @@ -28,6 +28,7 @@ base16ct = { version = "0.2.0", features = ["alloc"] }
362 base64 = "0.22.1"
363 askama = { version = "0.13.1", features = ["serde_json"], optional = true}
364 walkdir = "2.5.0"
365+ url = "2.5.4"
366
367 [dev-dependencies]
368 tokio = { version = "1.44.1", features = ["full"] }
369 diff --git a/examples/server.rs b/examples/server.rs
370index 3696881..3a866ae 100644
371--- a/examples/server.rs
372+++ b/examples/server.rs
373 @@ -5,6 +5,7 @@ use papyri::storage::Storage;
374 use tower::Layer;
375 use tower_http::{normalize_path::NormalizePathLayer, trace::TraceLayer};
376 use tracing::{Level, event, info_span};
377+ use url::Url;
378
379 const ADDRESS: &str = "127.0.0.1:8700";
380
381 @@ -27,8 +28,9 @@ async fn main() -> Result<(), Box<dyn Error>> {
382 fs.init()?;
383
384 // Registry middleware must be wrapped with namespace extraction/rewrite.
385- let web_interface = papyri::axum::web::router::router(&fs);
386- let registry = papyri::axum::router(&fs);
387+ let base_url = Url::parse("http://127.0.0.1:8700").unwrap();
388+ let web_interface = papyri::axum::web::router::router(&fs, &base_url);
389+ let registry = papyri::axum::router(&fs, &base_url);
390 let middleware = tower::util::MapRequestLayer::new(papyri::axum::extract_namespace);
391
392 let router = Router::new()
393 diff --git a/src/axum/mod.rs b/src/axum/mod.rs
394index 687136b..27de97b 100644
395--- a/src/axum/mod.rs
396+++ b/src/axum/mod.rs
397 @@ -9,6 +9,7 @@ use axum::routing::{delete, head, patch, post, put};
398 use axum::{Router, routing::get};
399 use http::Uri;
400 use serde_json::json;
401+ use url::Url;
402
403 use crate::Namespace;
404 use crate::oci_interface::OciInterface;
405 @@ -27,13 +28,19 @@ pub mod web;
406 #[derive(Clone)]
407 pub struct AppState {
408 pub oci: OciInterface,
409+ pub base_url: Url,
410 }
411
412 pub fn read_ns(uri: &str) -> Option<(Namespace, String)> {
413 let mut components: Vec<String> = uri.split("/").map(|cmp| cmp.to_string()).collect();
414 components.reverse();
415 let stop = components.iter().enumerate().find_map(|(i, entry)| {
416- if *entry == "blobs" || *entry == "manifests" || *entry == "tags" || *entry == "tag" || *entry == "index" {
417+ if *entry == "blobs"
418+ || *entry == "manifests"
419+ || *entry == "tags"
420+ || *entry == "tag"
421+ || *entry == "index"
422+ {
423 Some(i + 1)
424 } else {
425 None
426 @@ -98,7 +105,7 @@ pub async fn index() -> Result<Json<serde_json::Value>, error::Error> {
427
428 const MAXIMUM_MANIFEST_SIZE: usize = 5_000_000;
429
430- pub fn router(storage: &Storage) -> Router {
431+ pub fn router(storage: &Storage, base_url: &Url) -> Router {
432 let store = Arc::new(storage.inner());
433 Router::new()
434 .route("/v2", get(index))
435 @@ -149,5 +156,7 @@ pub fn router(storage: &Storage) -> Router {
436 .layer(from_fn(global_headers))
437 .with_state(Arc::new(AppState {
438 oci: OciInterface { storage: store },
439+
440+ base_url: base_url.clone(),
441 }))
442 }
443 diff --git a/src/axum/web/router.rs b/src/axum/web/router.rs
444index 9ec6bb8..a904f20 100644
445--- a/src/axum/web/router.rs
446+++ b/src/axum/web/router.rs
447 @@ -8,12 +8,13 @@ use axum::{
448 };
449 use bytes::Bytes;
450 use http::header::CONTENT_TYPE;
451+ use url::Url;
452
453 use crate::{Namespace, axum::AppState, oci_interface::OciInterface, storage::Storage};
454
455 use super::{
456 link_tree,
457- template::{self, LOGO, RepositoryIndex, STYLESHEET},
458+ template::{self, LOGO, BrowserTemplate, STYLESHEET},
459 };
460
461 pub async fn logo() -> Response {
462 @@ -44,16 +45,19 @@ pub async fn browser(
463 &link_builder,
464 namespace.as_ref().map(|ns| ns.to_string()).as_ref(),
465 );
466- let mut template = RepositoryIndex {
467+ let mut template = BrowserTemplate {
468 title: "Repositories",
469 tree: &tree,
470- namespace: None,
471+ base_url: &state.base_url,
472 manifest: None,
473 config: None,
474 tags: Vec::new(),
475+ namespace: None,
476+ tag: current_tag.as_ref().map(|tag| tag.as_str()),
477 };
478
479- if let Some(ns) = namespace {
480+ if let Some(ns) = namespace.as_ref() {
481+ template.namespace = Some(ns);
482 let tags = state
483 .oci
484 .list_tags(&ns, None, None)
485 @@ -71,7 +75,7 @@ pub async fn browser(
486 .collect()
487 });
488 template.tags = tags;
489- if let Some(current_tag) = current_tag {
490+ if let Some(current_tag) = current_tag.as_ref() {
491 let manifest = state
492 .oci
493 .read_manifest(&ns, &crate::TagOrDigest::Tag(current_tag.to_string()))
494 @@ -100,15 +104,12 @@ pub async fn browser(
495 .collect(),
496 });
497 }
498- template.namespace = Some(template::Namespace {
499- name: ns.to_string(),
500- });
501 };
502
503 Ok(Html::from(template.to_string()))
504 }
505
506- pub fn router(storage: &Storage) -> Router {
507+ pub fn router(storage: &Storage, base_url: &Url) -> Router {
508 let store = Arc::new(storage.inner());
509 Router::new()
510 .route("/", get(browser))
511 @@ -118,5 +119,6 @@ pub fn router(storage: &Storage) -> Router {
512 .route("/logo.png", get(logo))
513 .with_state(Arc::new(AppState {
514 oci: OciInterface { storage: store },
515+ base_url: base_url.clone(),
516 }))
517 }
518 diff --git a/src/axum/web/template.rs b/src/axum/web/template.rs
519index 9a258c0..bb08ca4 100644
520--- a/src/axum/web/template.rs
521+++ b/src/axum/web/template.rs
522 @@ -3,10 +3,46 @@ use std::fmt::Display;
523 use askama::Template;
524 use oci_spec::image::ImageConfiguration;
525 use serde::Serialize;
526+ use url::Url;
527+
528+ use crate::Namespace;
529
530 pub const STYLESHEET: &[u8] = include_bytes!("../../../templates/style.css");
531 pub const LOGO: &[u8] = include_bytes!("../../../assets/logo.png");
532
533+ mod filters {
534+ use url::Url;
535+
536+ pub fn pull_url(
537+ base_url: &Url,
538+ ns: &Option<&crate::namespace::Namespace>,
539+ tag: &Option<&str>,
540+ ) -> askama::Result<String> {
541+ let mut copied = base_url.clone();
542+ if let Some(ns) = ns {
543+ let mut path = ns.to_string();
544+ if let Some(tag) = tag {
545+ path.push(':');
546+ path.push_str(tag);
547+ }
548+ copied.set_path(&path);
549+ }
550+ Ok(copied.to_string().trim_start_matches("http://").trim_start_matches("https://").to_string())
551+ }
552+
553+ pub fn digest_url(base_url: &Url, ns: &Option<&crate::namespace::Namespace>, digest: &String) -> askama::Result<String> {
554+ let mut copied = base_url.clone();
555+ copied.set_path(&format!("/v2/{}/blobs/{}", ns.unwrap(), digest));
556+ Ok(copied.to_string())
557+ }
558+
559+ pub fn manifest_url(base_url: &Url, ns: &Option<&crate::namespace::Namespace>, tag: &Option<&str>) -> askama::Result<String> {
560+ let mut copied = base_url.clone();
561+ copied.set_path(&format!("/v2/{}/manifests/{}", ns.unwrap(), tag.unwrap()));
562+ Ok(copied.to_string())
563+ }
564+ }
565+
566 #[derive(Debug, Serialize)]
567 pub struct ImageConfig {
568 pub pretty: String,
569 @@ -33,17 +69,6 @@ impl Display for Tag {
570 }
571
572 #[derive(Debug, Serialize)]
573- pub struct Namespace {
574- pub name: String,
575- }
576-
577- impl Display for Namespace {
578- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
579- write!(f, "{}", self.name)
580- }
581- }
582-
583- #[derive(Debug, Serialize)]
584 pub struct Layer {
585 pub digest: String,
586 pub size: u64,
587 @@ -64,7 +89,7 @@ pub struct Manifest {
588 pub upstream_url: Option<String>,
589 }
590
591- impl Display for Manifest {
592+ impl<'a> Display for Manifest {
593 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
594 todo!()
595 }
596 @@ -72,11 +97,13 @@ impl Display for Manifest {
597
598 #[derive(Template)]
599 #[template(path = "browser.html")]
600- pub struct RepositoryIndex<'a> {
601+ pub struct BrowserTemplate<'a> {
602 pub title: &'a str,
603 pub tree: &'a str,
604- pub namespace: Option<Namespace>,
605+ pub base_url: &'a Url,
606 pub tags: Vec<Tag>,
607+ pub tag: Option<&'a str>,
608+ pub namespace: Option<&'a Namespace>,
609 pub manifest: Option<Manifest>,
610 pub config: Option<ImageConfig>,
611 }
612 diff --git a/src/namespace.rs b/src/namespace.rs
613index abfe0e5..7023304 100644
614--- a/src/namespace.rs
615+++ b/src/namespace.rs
616 @@ -11,7 +11,7 @@ use crate::error::Error;
617 const NAME_REGEXP_MATCH: &str = r"^[a-z0-9]+(?:[._-][a-z0-9]+)*";
618
619 // TODO: Consider 255 char namespace limit - hostname length per spec docs
620- #[derive(Clone, Debug, Hash, PartialEq, Eq)]
621+ #[derive(Clone, Debug, Hash, PartialEq, Eq, serde::Serialize)]
622 pub struct Namespace(Vec<String>);
623
624 impl Namespace {
625 diff --git a/templates/base.html b/templates/base.html
626index 4984dde..9cb9954 100644
627--- a/templates/base.html
628+++ b/templates/base.html
629 @@ -11,7 +11,7 @@
630 <div class="logo-container">
631 <a href="/"><img src="/logo.png" alt="Logo" class="logo"></a>
632 </div>
633- <ul class="nav-list">not authenticated</ul>
634+ <ul class="nav-list"></ul>
635 </nav>
636 </header>
637 <main>
638 diff --git a/templates/browser.html b/templates/browser.html
639index a516f58..16ae160 100644
640--- a/templates/browser.html
641+++ b/templates/browser.html
642 @@ -1,10 +1,5 @@
643 {% extends "base.html" %}
644 {% block content %}
645- <section class="pane">
646- <pre>
647- podman pull asdfasdfasdf/asdfasdf
648- </pre>
649- </section>
650 <section class="flex">
651 <section class="pane tree">
652 <ul class="tree">
653 @@ -21,10 +16,12 @@
654 <section class="pane manifest">
655 {% if let Some(manifest) = manifest %}
656 <section class="pane">
657+ <code>podman pull {{ base_url | pull_url(namespace, tag) }}</code>
658+ </section>
659+ <section class="pane">
660 <header>
661- <span class="badge">fuu</span>
662- <span class="badge">bar</span>
663 <h2>Manifest</h2>
664+ <span> <a href="{{ base_url | manifest_url(namespace, tag) }}">Download</a></span>
665 </header>
666 <pre>{{ manifest.pretty | safe }}</pre>
667 </section>
668 @@ -42,7 +39,8 @@
669 </header>
670 {% for layer in manifest.layers %}
671 <section class="pane">
672- {{ layer }} <span class="badge"> 100mb
673+ {{ layer }} <span class="badge"> Bytes: {{ layer.size }} </span>
674+ <span> <a href="{{ base_url | digest_url(namespace, layer.digest) }}"> Download </a> </span>
675 </section>
676 {% endfor %}
677 </section>