Author:
Hash:
Timestamp:
+403 -2 +/-13 browse
Kevin Schoon [me@kevinschoon.com]
ba004e4277788b4ec62e8ad796d526ab8d7e613d
Wed, 16 Apr 2025 13:14:12 +0000 (1 month ago)
1 | diff --git a/Cargo.lock b/Cargo.lock |
2 | index e1762b5..3dfa26a 100644 |
3 | --- a/Cargo.lock |
4 | +++ b/Cargo.lock |
5 | @@ -27,6 +27,48 @@ dependencies = [ |
6 | ] |
7 | |
8 | [[package]] |
9 | + name = "askama" |
10 | + version = "0.13.1" |
11 | + source = "registry+https://github.com/rust-lang/crates.io-index" |
12 | + checksum = "5d4744ed2eef2645831b441d8f5459689ade2ab27c854488fbab1fbe94fce1a7" |
13 | + dependencies = [ |
14 | + "askama_derive", |
15 | + "itoa", |
16 | + "percent-encoding", |
17 | + "serde", |
18 | + "serde_json", |
19 | + ] |
20 | + |
21 | + [[package]] |
22 | + name = "askama_derive" |
23 | + version = "0.13.1" |
24 | + source = "registry+https://github.com/rust-lang/crates.io-index" |
25 | + checksum = "d661e0f57be36a5c14c48f78d09011e67e0cb618f269cca9f2fd8d15b68c46ac" |
26 | + dependencies = [ |
27 | + "askama_parser", |
28 | + "basic-toml", |
29 | + "memchr", |
30 | + "proc-macro2", |
31 | + "quote", |
32 | + "rustc-hash", |
33 | + "serde", |
34 | + "serde_derive", |
35 | + "syn", |
36 | + ] |
37 | + |
38 | + [[package]] |
39 | + name = "askama_parser" |
40 | + version = "0.13.0" |
41 | + source = "registry+https://github.com/rust-lang/crates.io-index" |
42 | + checksum = "cf315ce6524c857bb129ff794935cf6d42c82a6cff60526fe2a63593de4d0d4f" |
43 | + dependencies = [ |
44 | + "memchr", |
45 | + "serde", |
46 | + "serde_derive", |
47 | + "winnow", |
48 | + ] |
49 | + |
50 | + [[package]] |
51 | name = "async-trait" |
52 | version = "0.1.88" |
53 | source = "registry+https://github.com/rust-lang/crates.io-index" |
54 | @@ -137,6 +179,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" |
55 | checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" |
56 | |
57 | [[package]] |
58 | + name = "basic-toml" |
59 | + version = "0.1.10" |
60 | + source = "registry+https://github.com/rust-lang/crates.io-index" |
61 | + checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a" |
62 | + dependencies = [ |
63 | + "serde", |
64 | + ] |
65 | + |
66 | + [[package]] |
67 | name = "bitflags" |
68 | version = "2.9.0" |
69 | source = "registry+https://github.com/rust-lang/crates.io-index" |
70 | @@ -624,6 +675,7 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" |
71 | name = "papyri" |
72 | version = "0.1.0" |
73 | dependencies = [ |
74 | + "askama", |
75 | "async-trait", |
76 | "axum", |
77 | "base16ct", |
78 | @@ -780,6 +832,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" |
79 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" |
80 | |
81 | [[package]] |
82 | + name = "rustc-hash" |
83 | + version = "2.1.1" |
84 | + source = "registry+https://github.com/rust-lang/crates.io-index" |
85 | + checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" |
86 | + |
87 | + [[package]] |
88 | name = "rustversion" |
89 | version = "1.0.20" |
90 | source = "registry+https://github.com/rust-lang/crates.io-index" |
91 | @@ -1265,6 +1323,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" |
92 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" |
93 | |
94 | [[package]] |
95 | + name = "winnow" |
96 | + version = "0.7.6" |
97 | + source = "registry+https://github.com/rust-lang/crates.io-index" |
98 | + checksum = "63d3fcd9bba44b03821e7d699eeee959f3126dcc4aa8e4ae18ec617c2a5cea10" |
99 | + dependencies = [ |
100 | + "memchr", |
101 | + ] |
102 | + |
103 | + [[package]] |
104 | name = "wit-bindgen-rt" |
105 | version = "0.33.0" |
106 | source = "registry+https://github.com/rust-lang/crates.io-index" |
107 | diff --git a/Cargo.toml b/Cargo.toml |
108 | index 6d7c9c0..a3810c2 100644 |
109 | --- a/Cargo.toml |
110 | +++ b/Cargo.toml |
111 | @@ -26,6 +26,7 @@ sha2 = "0.10.8" |
112 | hex-literal = "1.0.0" |
113 | base16ct = { version = "0.2.0", features = ["alloc"] } |
114 | base64 = "0.22.1" |
115 | + askama = { version = "0.13.1", features = ["serde_json"], optional = true} |
116 | |
117 | [dev-dependencies] |
118 | tokio = { version = "1.44.1", features = ["full"] } |
119 | @@ -46,6 +47,11 @@ storage-fs = [ |
120 | "tokio-util" |
121 | ] |
122 | |
123 | + web = [ |
124 | + "axum-router", |
125 | + "askama" |
126 | + ] |
127 | + |
128 | [[example]] |
129 | name = "server" |
130 | path = "examples/server.rs" |
131 | diff --git a/assets/logo.png b/assets/logo.png |
132 | new file mode 100644 |
133 | index 0000000..250a2d3 |
134 | Binary files /dev/null and b/assets/logo.png differ |
135 | diff --git a/examples/server.rs b/examples/server.rs |
136 | index 35b6a36..1299471 100644 |
137 | --- a/examples/server.rs |
138 | +++ b/examples/server.rs |
139 | @@ -27,6 +27,7 @@ async fn main() -> Result<(), Box<dyn Error>> { |
140 | fs.init()?; |
141 | |
142 | // Registry middleware must be wrapped with namespace extraction/rewrite. |
143 | + let web_interface = papyri::axum::web::router::router(&fs); |
144 | let registry = papyri::axum::router(&fs); |
145 | let middleware = tower::util::MapRequestLayer::new(papyri::axum::extract_namespace); |
146 | |
147 | @@ -49,7 +50,8 @@ async fn main() -> Result<(), Box<dyn Error>> { |
148 | span |
149 | }), |
150 | ) |
151 | - .layer(NormalizePathLayer::trim_trailing_slash()); |
152 | + .layer(NormalizePathLayer::trim_trailing_slash()) |
153 | + .fallback_service(middleware.layer(web_interface)); |
154 | |
155 | axum::serve(listener, router).await?; |
156 | Ok(()) |
157 | diff --git a/scripts/conformance_test.sh b/scripts/conformance_test.sh |
158 | index edcc388..75b91e9 100755 |
159 | --- a/scripts/conformance_test.sh |
160 | +++ b/scripts/conformance_test.sh |
161 | @@ -16,7 +16,7 @@ podman run --rm \ |
162 | -e OCI_PASSWORD="mypass" \ |
163 | -e OCI_TEST_PULL=1 \ |
164 | -e OCI_TEST_PUSH=0 \ |
165 | - -e OCI_TEST_CONTENT_DISCOVERY=0 \ |
166 | + -e OCI_TEST_CONTENT_DISCOVERY=1 \ |
167 | -e OCI_TEST_CONTENT_MANAGEMENT=0 \ |
168 | -e OCI_HIDE_SKIPPED_WORKFLOWS=0 \ |
169 | -e OCI_DEBUG=0 \ |
170 | diff --git a/src/axum/mod.rs b/src/axum/mod.rs |
171 | index 1b3fa14..ab18274 100644 |
172 | --- a/src/axum/mod.rs |
173 | +++ b/src/axum/mod.rs |
174 | @@ -20,6 +20,8 @@ mod handlers_blob; |
175 | mod handlers_manifest; |
176 | mod handlers_tag; |
177 | mod paths; |
178 | + #[cfg(all(feature = "web", feature = "axum"))] |
179 | + pub mod web; |
180 | |
181 | #[derive(Clone)] |
182 | pub(crate) struct AppState { |
183 | diff --git a/src/axum/web/mod.rs b/src/axum/web/mod.rs |
184 | new file mode 100644 |
185 | index 0000000..bedf762 |
186 | --- /dev/null |
187 | +++ b/src/axum/web/mod.rs |
188 | @@ -0,0 +1,2 @@ |
189 | + pub mod template; |
190 | + pub mod router; |
191 | diff --git a/src/axum/web/router.rs b/src/axum/web/router.rs |
192 | new file mode 100644 |
193 | index 0000000..9d3012d |
194 | --- /dev/null |
195 | +++ b/src/axum/web/router.rs |
196 | @@ -0,0 +1,83 @@ |
197 | + use std::sync::Arc; |
198 | + |
199 | + use axum::{ |
200 | + Extension, Router, |
201 | + response::{Html, Response}, |
202 | + routing::get, |
203 | + }; |
204 | + use bytes::Bytes; |
205 | + use http::header::CONTENT_TYPE; |
206 | + |
207 | + use crate::{Namespace, axum::AppState, oci_interface::OciInterface, storage::Storage}; |
208 | + |
209 | + use super::template::{LOGO, Repository, RepositoryIndex, STYLESHEET}; |
210 | + |
211 | + pub async fn logo() -> Response { |
212 | + let mut res = Response::new(Bytes::from_static(LOGO).into()); |
213 | + res.headers_mut() |
214 | + .insert(CONTENT_TYPE, "image/png".parse().unwrap()); |
215 | + res |
216 | + } |
217 | + |
218 | + pub async fn stylesheet() -> Response { |
219 | + let mut res = Response::new(Bytes::from_static(STYLESHEET).into()); |
220 | + res.headers_mut() |
221 | + .insert(CONTENT_TYPE, "text/css".parse().unwrap()); |
222 | + res |
223 | + } |
224 | + |
225 | + pub async fn index() -> Result<Html<String>, crate::axum::error::Error> { |
226 | + let template = RepositoryIndex { |
227 | + title: "Repositories", |
228 | + repositories: vec![String::from("Hello")], |
229 | + }; |
230 | + Ok(Html::from(template.to_string())) |
231 | + } |
232 | + |
233 | + pub async fn repository( |
234 | + Extension(namespace): Extension<Namespace>, |
235 | + ) -> Result<Html<String>, crate::axum::error::Error> { |
236 | + let template = Repository { |
237 | + title: "REPOSITORY A", |
238 | + }; |
239 | + Ok(Html::from(template.to_string())) |
240 | + } |
241 | + |
242 | + pub fn router(storage: &Storage) -> Router { |
243 | + let store = Arc::new(storage.inner()); |
244 | + Router::new() |
245 | + .route("/", get(index)) |
246 | + .route("/style.css", get(stylesheet)) |
247 | + .route("/logo.png", get(logo)) |
248 | + .route("/overview", get(repository)) |
249 | + // // .route("/{name}/blobs/{digest}", head(crate::handlers::stat_blob)) |
250 | + // // .route( |
251 | + // // "/{name}/manifests/{reference}", |
252 | + // // get(crate::handlers::read_manifest), |
253 | + // // ) |
254 | + // // .route( |
255 | + // // "/{name}/manifests/{reference}", |
256 | + // // head(crate::handlers::read_manifest), |
257 | + // // ) |
258 | + // // .route("/{name}/blobs/uploads", post(crate::handlers::initiate_blob)) |
259 | + // // .route( |
260 | + // // "/{name}/blobs/uploads/{reference}", |
261 | + // // patch(crate::handlers::write_blob), |
262 | + // // ) |
263 | + // // .route( |
264 | + // // "/{name}/manifests/{reference}", |
265 | + // // put(crate::handlers::write_manifest), |
266 | + // // ) |
267 | + // // .route("/{name}/tags/list", get(crate::handlers::read_tags)) |
268 | + // // .route( |
269 | + // // "/{name}/manifests/{reference}", |
270 | + // // delete(crate::handlers::delete_manifest), |
271 | + // // ) |
272 | + // // .route( |
273 | + // // "/{name}/blobs/{digest}", |
274 | + // // delete(crate::handlers::delete_blob), |
275 | + // // ) |
276 | + .with_state(Arc::new(AppState { |
277 | + oci: OciInterface { storage: store }, |
278 | + })) |
279 | + } |
280 | diff --git a/src/axum/web/template.rs b/src/axum/web/template.rs |
281 | new file mode 100644 |
282 | index 0000000..a93e77a |
283 | --- /dev/null |
284 | +++ b/src/axum/web/template.rs |
285 | @@ -0,0 +1,17 @@ |
286 | + use askama::Template; |
287 | + |
288 | + pub const STYLESHEET: &[u8] = include_bytes!("../../../templates/style.css"); |
289 | + pub const LOGO: &[u8] = include_bytes!("../../../assets/logo.png"); |
290 | + |
291 | + #[derive(Template)] |
292 | + #[template(path = "index.html")] |
293 | + pub struct RepositoryIndex<'a> { |
294 | + pub title: &'a str, |
295 | + pub repositories: Vec<String>, |
296 | + } |
297 | + |
298 | + #[derive(Template)] |
299 | + #[template(path = "repository.html")] |
300 | + pub struct Repository<'a> { |
301 | + pub title: &'a str, |
302 | + } |
303 | diff --git a/templates/base.html b/templates/base.html |
304 | new file mode 100644 |
305 | index 0000000..5f378ad |
306 | --- /dev/null |
307 | +++ b/templates/base.html |
308 | @@ -0,0 +1,28 @@ |
309 | + <!DOCTYPE html> |
310 | + <html lang="en"> |
311 | + <head> |
312 | + <title>{{ title }}</title> |
313 | + <link rel="stylesheet" type="text/css" href="/style.css" /> |
314 | + {% block head %}{% endblock %} |
315 | + </head> |
316 | + <body> |
317 | + <header> |
318 | + <nav> |
319 | + <div class="logo"> |
320 | + <a href="/"><img src="/logo.png" alt="Logo"></a> |
321 | + </div> |
322 | + <!-- |
323 | + <ul class="nav-links"> |
324 | + <li><a href="#home">Home</a></li> |
325 | + </ul> |
326 | + --> |
327 | + </nav> |
328 | + </header> |
329 | + <main> |
330 | + {% block content %}{% endblock %} |
331 | + </main> |
332 | + <footer> |
333 | + <p>Papyri</p> |
334 | + </footer> |
335 | + </body> |
336 | + </html> |
337 | diff --git a/templates/index.html b/templates/index.html |
338 | new file mode 100644 |
339 | index 0000000..069a68c |
340 | --- /dev/null |
341 | +++ b/templates/index.html |
342 | @@ -0,0 +1,21 @@ |
343 | + {% extends "base.html" %} |
344 | + {% block content %} |
345 | + <table> |
346 | + <thead> |
347 | + <tr> |
348 | + <th>Repository</th> |
349 | + <th>Url</th> |
350 | + <th>Tag Count</th> |
351 | + <th>Updated</th> |
352 | + </tr> |
353 | + </thead> |
354 | + <tbody> |
355 | + <tr> |
356 | + <td>NAME</td> |
357 | + <td>LINK</td> |
358 | + <td>N_TAGS</td> |
359 | + <td>LAST_UPDATED</td> |
360 | + </tr> |
361 | + </tbody> |
362 | + </table> |
363 | + {% endblock %} |
364 | diff --git a/templates/repository.html b/templates/repository.html |
365 | new file mode 100644 |
366 | index 0000000..069a68c |
367 | --- /dev/null |
368 | +++ b/templates/repository.html |
369 | @@ -0,0 +1,21 @@ |
370 | + {% extends "base.html" %} |
371 | + {% block content %} |
372 | + <table> |
373 | + <thead> |
374 | + <tr> |
375 | + <th>Repository</th> |
376 | + <th>Url</th> |
377 | + <th>Tag Count</th> |
378 | + <th>Updated</th> |
379 | + </tr> |
380 | + </thead> |
381 | + <tbody> |
382 | + <tr> |
383 | + <td>NAME</td> |
384 | + <td>LINK</td> |
385 | + <td>N_TAGS</td> |
386 | + <td>LAST_UPDATED</td> |
387 | + </tr> |
388 | + </tbody> |
389 | + </table> |
390 | + {% endblock %} |
391 | diff --git a/templates/style.css b/templates/style.css |
392 | new file mode 100644 |
393 | index 0000000..529e6f2 |
394 | --- /dev/null |
395 | +++ b/templates/style.css |
396 | @@ -0,0 +1,152 @@ |
397 | + :root { |
398 | + --background-color: #000000; |
399 | + --text-color: #00ff00; |
400 | + --border-color: #00ff00; |
401 | + --header-background-color: #000000; |
402 | + --header-text-color: #00ff00; |
403 | + --even-row-background-color: #111111; |
404 | + --hover-background-color: #222222; |
405 | + } |
406 | + |
407 | + @media (prefers-color-scheme: light) { |
408 | + :root { |
409 | + --background-color: #ffffff; |
410 | + --text-color: #000000; |
411 | + --border-color: #000000; |
412 | + --header-background-color: #ffffff; |
413 | + --header-text-color: #000000; |
414 | + --even-row-background-color: #f9f9f9; |
415 | + --hover-background-color: #e9e9e9; |
416 | + } |
417 | + } |
418 | + |
419 | + /* Global Styles */ |
420 | + |
421 | + * { |
422 | + box-sizing: border-box; |
423 | + margin: 0; |
424 | + padding: 0; |
425 | + } |
426 | + |
427 | + body { |
428 | + font-family: Arial, sans-serif; |
429 | + line-height: 1.6; |
430 | + color: #333; |
431 | + background-color: #f9f9f9; |
432 | + } |
433 | + |
434 | + /* Navigation Bar Styles */ |
435 | + |
436 | + nav { |
437 | + display: flex; |
438 | + justify-content: space-between; |
439 | + align-items: center; |
440 | + padding: 16px; |
441 | + background-color: var(--background-color); |
442 | + color: var(--text-color); |
443 | + } |
444 | + |
445 | + .logo { |
446 | + margin-right: 20px; |
447 | + } |
448 | + |
449 | + .logo img { |
450 | + width: 100px; |
451 | + height: 100px; |
452 | + border-radius: 50%; |
453 | + } |
454 | + |
455 | + .nav-links { |
456 | + list-style: none; |
457 | + margin: 0; |
458 | + padding: 0; |
459 | + display: flex; |
460 | + } |
461 | + |
462 | + .nav-links li { |
463 | + margin-right: 20px; |
464 | + } |
465 | + |
466 | + .nav-links a { |
467 | + color: var(--text-color); |
468 | + text-decoration: none; |
469 | + transition: color 0.2s ease-in-out; |
470 | + } |
471 | + |
472 | + .nav-links a:hover { |
473 | + color: #ccc; |
474 | + } |
475 | + |
476 | + .menu-btn { |
477 | + background-color: var(--background-color); |
478 | + border: none; |
479 | + padding: 10px 20px; |
480 | + font-size: 16px; |
481 | + cursor: pointer; |
482 | + } |
483 | + |
484 | + /* Responsive Styles */ |
485 | + |
486 | + @media only screen and (max-width: 768px) { |
487 | + nav { |
488 | + flex-direction: column; |
489 | + align-items: center; |
490 | + } |
491 | + |
492 | + .logo { |
493 | + margin-bottom: 10px; |
494 | + } |
495 | + |
496 | + .nav-links { |
497 | + flex-direction: column; |
498 | + padding: 0; |
499 | + } |
500 | + |
501 | + .nav-links li { |
502 | + margin-bottom: 5px; |
503 | + } |
504 | + } |
505 | + |
506 | + body { |
507 | + font-family: 'Courier New', Courier, monospace; |
508 | + background-color: var(--background-color); |
509 | + color: var(--text-color); |
510 | + margin: 0; |
511 | + padding: 20px; |
512 | + } |
513 | + |
514 | + h1 { |
515 | + text-align: center; |
516 | + font-size: 2em; |
517 | + border-bottom: 2px solid var(--border-color); |
518 | + padding-bottom: 10px; |
519 | + } |
520 | + table { |
521 | + width: 80%; |
522 | + border-collapse: collapse; |
523 | + margin: 20px auto; |
524 | + background-color: var(--background-color); |
525 | + border: 1px solid var(--border-color); |
526 | + } |
527 | + th, td { |
528 | + padding: 10px; |
529 | + text-align: left; |
530 | + border: 1px solid var(--border-color); |
531 | + } |
532 | + |
533 | + th { |
534 | + background-color: var(--header-background-color); |
535 | + color: var(--header-text-color); |
536 | + font-weight: bold; |
537 | + } |
538 | + tr:nth-child(even) { |
539 | + background-color: var(--even-row-background-color); |
540 | + } |
541 | + |
542 | + tr:hover { |
543 | + background-color: var(--hover-background-color); |
544 | + } |
545 | + |
546 | + footer { |
547 | + text-align: center; |
548 | + } |