Author:
Hash:
Timestamp:
+101 -86 +/-8 browse
Kevin Schoon [me@kevinschoon.com]
43ae7b1a3e98a0622276657ed9aa10565d20ba60
Tue, 22 Apr 2025 15:46:53 +0000 (1 month 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 |