Author:
Hash:
Timestamp:
+101 -86 +/-8 browse
Kevin Schoon [me@kevinschoon.com]
43ae7b1a3e98a0622276657ed9aa10565d20ba60
Tue, 22 Apr 2025 15:46:53 +0000 (1.1 years ago)
| 1 | diff --git a/src/axum/web/router.rs b/src/axum/web/router.rs |
| 2 | index c9aae33..9ec6bb8 100644 |
| 3 | --- a/src/axum/web/router.rs |
| 4 | +++ b/src/axum/web/router.rs |
| 5 | @@ -13,7 +13,7 @@ use crate::{Namespace, axum::AppState, oci_interface::OciInterface, storage::Sto |
| 6 | |
| 7 | use super::{ |
| 8 | link_tree, |
| 9 | - template::{self, LOGO, Repository, RepositoryIndex, STYLESHEET}, |
| 10 | + template::{self, LOGO, RepositoryIndex, STYLESHEET}, |
| 11 | }; |
| 12 | |
| 13 | pub async fn logo() -> Response { |
| 14 | @@ -30,7 +30,7 @@ pub async fn stylesheet() -> Response { |
| 15 | res |
| 16 | } |
| 17 | |
| 18 | - pub async fn index( |
| 19 | + pub async fn browser( |
| 20 | Extension(namespace): Extension<Option<Namespace>>, |
| 21 | current_tag: Option<Path<String>>, |
| 22 | State(state): State<Arc<AppState>>, |
| 23 | @@ -49,6 +49,7 @@ pub async fn index( |
| 24 | tree: &tree, |
| 25 | namespace: None, |
| 26 | manifest: None, |
| 27 | + config: None, |
| 28 | tags: Vec::new(), |
| 29 | }; |
| 30 | |
| 31 | @@ -75,6 +76,11 @@ pub async fn index( |
| 32 | .oci |
| 33 | .read_manifest(&ns, &crate::TagOrDigest::Tag(current_tag.to_string())) |
| 34 | .await?; |
| 35 | + let manifest_config_spec = manifest.config(); |
| 36 | + if manifest_config_spec.media_type() == &oci_spec::image::MediaType::ImageConfig { |
| 37 | + let cfg = state.oci.read_config(manifest_config_spec.digest()).await?; |
| 38 | + template.config = Some(template::ImageConfig::from(cfg)); |
| 39 | + }; |
| 40 | template.manifest = Some(template::Manifest { |
| 41 | pretty: manifest.to_string_pretty().unwrap(), |
| 42 | created_at: manifest.annotations().as_ref().and_then(|annotations| { |
| 43 | @@ -105,9 +111,9 @@ pub async fn index( |
| 44 | pub fn router(storage: &Storage) -> Router { |
| 45 | let store = Arc::new(storage.inner()); |
| 46 | Router::new() |
| 47 | - .route("/", get(index)) |
| 48 | - .route("/index", get(index)) |
| 49 | - .route("/tag/{name}", get(index)) |
| 50 | + .route("/", get(browser)) |
| 51 | + .route("/index", get(browser)) |
| 52 | + .route("/tag/{name}", get(browser)) |
| 53 | .route("/style.css", get(stylesheet)) |
| 54 | .route("/logo.png", get(logo)) |
| 55 | .with_state(Arc::new(AppState { |
| 56 | diff --git a/src/axum/web/template.rs b/src/axum/web/template.rs |
| 57 | index f6f4019..9a258c0 100644 |
| 58 | --- a/src/axum/web/template.rs |
| 59 | +++ b/src/axum/web/template.rs |
| 60 | @@ -1,12 +1,25 @@ |
| 61 | use std::fmt::Display; |
| 62 | |
| 63 | use askama::Template; |
| 64 | + use oci_spec::image::ImageConfiguration; |
| 65 | use serde::Serialize; |
| 66 | |
| 67 | pub const STYLESHEET: &[u8] = include_bytes!("../../../templates/style.css"); |
| 68 | pub const LOGO: &[u8] = include_bytes!("../../../assets/logo.png"); |
| 69 | |
| 70 | #[derive(Debug, Serialize)] |
| 71 | + pub struct ImageConfig { |
| 72 | + pub pretty: String, |
| 73 | + } |
| 74 | + |
| 75 | + impl From<ImageConfiguration> for ImageConfig { |
| 76 | + fn from(value: ImageConfiguration) -> Self { |
| 77 | + let pretty = serde_json::ser::to_string_pretty(&value).unwrap(); |
| 78 | + Self { pretty } |
| 79 | + } |
| 80 | + } |
| 81 | + |
| 82 | + #[derive(Debug, Serialize)] |
| 83 | pub struct Tag { |
| 84 | pub name: String, |
| 85 | pub namespace: String, |
| 86 | @@ -58,17 +71,12 @@ impl Display for Manifest { |
| 87 | } |
| 88 | |
| 89 | #[derive(Template)] |
| 90 | - #[template(path = "index.html")] |
| 91 | + #[template(path = "browser.html")] |
| 92 | pub struct RepositoryIndex<'a> { |
| 93 | pub title: &'a str, |
| 94 | pub tree: &'a str, |
| 95 | pub namespace: Option<Namespace>, |
| 96 | pub tags: Vec<Tag>, |
| 97 | pub manifest: Option<Manifest>, |
| 98 | - } |
| 99 | - |
| 100 | - #[derive(Template)] |
| 101 | - #[template(path = "repository.html")] |
| 102 | - pub struct Repository<'a> { |
| 103 | - pub title: &'a str, |
| 104 | + pub config: Option<ImageConfig>, |
| 105 | } |
| 106 | diff --git a/src/oci_interface.rs b/src/oci_interface.rs |
| 107 | index 4c1538f..e9f65b9 100644 |
| 108 | --- a/src/oci_interface.rs |
| 109 | +++ b/src/oci_interface.rs |
| 110 | @@ -4,7 +4,7 @@ use bytes::Bytes; |
| 111 | use futures::{Stream, StreamExt}; |
| 112 | use oci_spec::{ |
| 113 | distribution::{TagList, TagListBuilder}, |
| 114 | - image::{Digest, ImageManifest}, |
| 115 | + image::{Digest, ImageConfiguration, ImageManifest}, |
| 116 | }; |
| 117 | use sha2::{Digest as HashDigest, Sha256}; |
| 118 | use uuid::Uuid; |
| 119 | @@ -267,6 +267,23 @@ impl OciInterface { |
| 120 | Ok(manifest) |
| 121 | } |
| 122 | |
| 123 | + pub async fn read_config( |
| 124 | + &self, |
| 125 | + digest: &Digest, |
| 126 | + ) -> Result<oci_spec::image::ImageConfiguration, Error> { |
| 127 | + let addr = Address::Blob { |
| 128 | + digest: digest.clone(), |
| 129 | + }; |
| 130 | + let config_bytes = self |
| 131 | + .storage |
| 132 | + .read_bytes(&addr) |
| 133 | + .await |
| 134 | + .map_err(Error::Storage)?; |
| 135 | + let config: ImageConfiguration = serde_json::de::from_slice(config_bytes.iter().as_slice()) |
| 136 | + .expect("invalid configuration"); |
| 137 | + Ok(config) |
| 138 | + } |
| 139 | + |
| 140 | pub async fn has_blob(&self, digest: &Digest) -> Result<bool, Error> { |
| 141 | let blob_addr = Address::Blob { |
| 142 | digest: digest.clone(), |
| 143 | diff --git a/templates/base.html b/templates/base.html |
| 144 | index 9cb9954..4984dde 100644 |
| 145 | --- a/templates/base.html |
| 146 | +++ b/templates/base.html |
| 147 | @@ -11,7 +11,7 @@ |
| 148 | <div class="logo-container"> |
| 149 | <a href="/"><img src="/logo.png" alt="Logo" class="logo"></a> |
| 150 | </div> |
| 151 | - <ul class="nav-list"></ul> |
| 152 | + <ul class="nav-list">not authenticated</ul> |
| 153 | </nav> |
| 154 | </header> |
| 155 | <main> |
| 156 | diff --git a/templates/browser.html b/templates/browser.html |
| 157 | new file mode 100644 |
| 158 | index 0000000..a516f58 |
| 159 | --- /dev/null |
| 160 | +++ b/templates/browser.html |
| 161 | @@ -0,0 +1,52 @@ |
| 162 | + {% extends "base.html" %} |
| 163 | + {% block content %} |
| 164 | + <section class="pane"> |
| 165 | + <pre> |
| 166 | + podman pull asdfasdfasdf/asdfasdf |
| 167 | + </pre> |
| 168 | + </section> |
| 169 | + <section class="flex"> |
| 170 | + <section class="pane tree"> |
| 171 | + <ul class="tree"> |
| 172 | + {{ tree | safe }} |
| 173 | + </ul> |
| 174 | + </section> |
| 175 | + <section class="pane tags"> |
| 176 | + <ul> |
| 177 | + {% for tag in tags %} |
| 178 | + <li> <code class="tag"><a href="/{{tag.namespace}}/tag/{{tag}}">{{ tag }}</a></code> </li> |
| 179 | + {% endfor %} |
| 180 | + </ul> |
| 181 | + </section> |
| 182 | + <section class="pane manifest"> |
| 183 | + {% if let Some(manifest) = manifest %} |
| 184 | + <section class="pane"> |
| 185 | + <header> |
| 186 | + <span class="badge">fuu</span> |
| 187 | + <span class="badge">bar</span> |
| 188 | + <h2>Manifest</h2> |
| 189 | + </header> |
| 190 | + <pre>{{ manifest.pretty | safe }}</pre> |
| 191 | + </section> |
| 192 | + {% if let Some(config) = config %} |
| 193 | + <section class="pane"> |
| 194 | + <header> |
| 195 | + <h2>Configuration</h2> |
| 196 | + </header> |
| 197 | + <pre>{{ config.pretty | safe }}</pre> |
| 198 | + </section> |
| 199 | + {% endif %} |
| 200 | + <section class="pane"> |
| 201 | + <header> |
| 202 | + <h2>Layers</h2> |
| 203 | + </header> |
| 204 | + {% for layer in manifest.layers %} |
| 205 | + <section class="pane"> |
| 206 | + {{ layer }} <span class="badge"> 100mb |
| 207 | + </section> |
| 208 | + {% endfor %} |
| 209 | + </section> |
| 210 | + {% endif %} |
| 211 | + </section> |
| 212 | + </section> |
| 213 | + {% endblock %} |
| 214 | diff --git a/templates/index.html b/templates/index.html |
| 215 | deleted file mode 100644 |
| 216 | index 7654879..0000000 |
| 217 | --- a/templates/index.html |
| 218 | +++ /dev/null |
| 219 | @@ -1,45 +0,0 @@ |
| 220 | - {% extends "base.html" %} |
| 221 | - {% block content %} |
| 222 | - {% if let Some(manifest) = manifest %} |
| 223 | - <section class="pane details"> |
| 224 | - <span id="total-size"></span> |
| 225 | - |
| 226 | - <p>Created At:</p> |
| 227 | - <span class="badge">hello</span> |
| 228 | - |
| 229 | - <p>Revision:</p> |
| 230 | - <span id="revision"></span> |
| 231 | - |
| 232 | - <p>Url:</p> |
| 233 | - <span id="url"></span> |
| 234 | - </section> |
| 235 | - {% endif %} |
| 236 | - <section class="flex"> |
| 237 | - <section class="pane tree"> |
| 238 | - <ul class="tree"> |
| 239 | - {{ tree | safe }} |
| 240 | - </ul> |
| 241 | - </section> |
| 242 | - <section class="pane tags"> |
| 243 | - <ul> |
| 244 | - {% for tag in tags %} |
| 245 | - <li> <code class="tag"><a href="/{{tag.namespace}}/tag/{{tag}}">{{ tag }}</a></code> </li> |
| 246 | - {% endfor %} |
| 247 | - </ul> |
| 248 | - </section> |
| 249 | - <section class="pane manifest"> |
| 250 | - {% if let Some(manifest) = manifest %} |
| 251 | - <h2>Manifest</h2> |
| 252 | - <section class="pane"> |
| 253 | - <pre>{{ manifest.pretty | safe }}</pre> |
| 254 | - </section> |
| 255 | - <h2>Layers</h2> |
| 256 | - {% for layer in manifest.layers %} |
| 257 | - <section class="pane"> |
| 258 | - {{ layer }} |
| 259 | - </section> |
| 260 | - {% endfor %} |
| 261 | - {% endif %} |
| 262 | - </section> |
| 263 | - </section> |
| 264 | - {% endblock %} |
| 265 | diff --git a/templates/repository.html b/templates/repository.html |
| 266 | deleted file mode 100644 |
| 267 | index 069a68c..0000000 |
| 268 | --- a/templates/repository.html |
| 269 | +++ /dev/null |
| 270 | @@ -1,21 +0,0 @@ |
| 271 | - {% extends "base.html" %} |
| 272 | - {% block content %} |
| 273 | - <table> |
| 274 | - <thead> |
| 275 | - <tr> |
| 276 | - <th>Repository</th> |
| 277 | - <th>Url</th> |
| 278 | - <th>Tag Count</th> |
| 279 | - <th>Updated</th> |
| 280 | - </tr> |
| 281 | - </thead> |
| 282 | - <tbody> |
| 283 | - <tr> |
| 284 | - <td>NAME</td> |
| 285 | - <td>LINK</td> |
| 286 | - <td>N_TAGS</td> |
| 287 | - <td>LAST_UPDATED</td> |
| 288 | - </tr> |
| 289 | - </tbody> |
| 290 | - </table> |
| 291 | - {% endblock %} |
| 292 | diff --git a/templates/style.css b/templates/style.css |
| 293 | index ff23674..f8ba640 100644 |
| 294 | --- a/templates/style.css |
| 295 | +++ b/templates/style.css |
| 296 | @@ -21,7 +21,7 @@ code.tag { |
| 297 | font-weight: bold |
| 298 | } |
| 299 | |
| 300 | - section.pane.manifest >h2 { |
| 301 | + section > header > h2 { |
| 302 | float:right; |
| 303 | } |
| 304 | |
| 305 | @@ -29,7 +29,7 @@ section.pane.manifest >h2 { |
| 306 | background-color: #f7dc6f; /* yellow */ |
| 307 | color: #333; |
| 308 | padding: 10px 20px; |
| 309 | - border-radius: 50%; |
| 310 | + border-radius: 10%; |
| 311 | font-size: 16px; |
| 312 | font-weight: bold; |
| 313 | } |
| 314 | @@ -45,10 +45,10 @@ section.pane.manifest >h2 { |
| 315 | section.flex { |
| 316 | display: flex; |
| 317 | padding: 2em; |
| 318 | - margin: 3em auto; |
| 319 | } |
| 320 | |
| 321 | section.pane { |
| 322 | + margin-top: 20px; |
| 323 | margin-right: 10px; |
| 324 | padding: 20px; |
| 325 | border: 1px solid #ccc; |
| 326 | @@ -116,7 +116,7 @@ footer { |
| 327 | } |
| 328 | |
| 329 | .logo { |
| 330 | - height: 10em; |
| 331 | + height: 5em; |
| 332 | width: auto; |
| 333 | margin-right: 0.5rem; |
| 334 | } |
| 335 | @@ -143,8 +143,6 @@ footer { |
| 336 | color: #555; |
| 337 | } |
| 338 | |
| 339 | - |
| 340 | - |
| 341 | /* |
| 342 | Tree structure using CSS: |
| 343 | http://stackoverflow.com/questions/14922247/how-to-get-a-tree-in-html-using-pure-css |