Commit

Author:

Hash:

Timestamp:

+403 -2 +/-13 browse

Kevin Schoon [me@kevinschoon.com]

ba004e4277788b4ec62e8ad796d526ab8d7e613d

Wed, 16 Apr 2025 13:14:12 +0000 (1 month ago)

hackup primitive web interface
1diff --git a/Cargo.lock b/Cargo.lock
2index 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
108index 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
132new file mode 100644
133index 0000000..250a2d3
134 Binary files /dev/null and b/assets/logo.png differ
135 diff --git a/examples/server.rs b/examples/server.rs
136index 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
158index 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
171index 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
184new file mode 100644
185index 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
192new file mode 100644
193index 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
281new file mode 100644
282index 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
304new file mode 100644
305index 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
338new file mode 100644
339index 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
365new file mode 100644
366index 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
392new file mode 100644
393index 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+ }