spa.rs
⎇
Raw
1use std::borrow::Cow;
2
3use axum::http::Uri;
4use axum::http::{HeaderMap, Method, StatusCode, header};
5use axum::response::{IntoResponse, Response};
6
7const DEV_HINT: &str = r#"<!doctype html>
8<html lang="en"><head><meta charset="utf-8"><title>filebrowser-ng</title></head>
9<body style="font-family:system-ui;display:grid;place-items:center;min-height:100vh;margin:0;background:#f6f7f9">
10<div style="max-width:520px;padding:24px;border:1px solid #e5e7eb;border-radius:12px;background:#fff">
11<h1>filebrowser-ng</h1>
12<p>The frontend has not been built yet.</p>
13<p>In development, run <code>just dev-web</code> in a second terminal and open
14<code>http://localhost:8080</code> (Trunk serves the app and proxies <code>/api</code> to this backend).</p>
15<p>If you hit this page directly from the backend, make sure Trunk's output
16directory is visible — the server looks in <code>target/web</code> or <code>$FBNG_DIST</code>.</p>
17<p>For the production single binary: <code>just build</code>.</p>
18</div></body></html>"#;
19
20/// Serve the frontend: static assets, with a fallback to index.html so that
21/// client-side routes (e.g. `/s/<token>`) work.
22pub(super) async fn fallback(method: Method, uri: Uri, headers: HeaderMap) -> Response {
23 let path = uri.path();
24 if path.starts_with("/api/") {
25 return (StatusCode::NOT_FOUND, "unknown endpoint").into_response();
26 }
27 if method != Method::GET && method != Method::HEAD {
28 return StatusCode::METHOD_NOT_ALLOWED.into_response();
29 }
30
31 let rel = path.trim_start_matches('/');
32 let asset = if rel.is_empty() {
33 crate::assets::get_asset("index.html")
34 } else {
35 crate::assets::get_asset(rel)
36 };
37
38 match asset {
39 Some((bytes, mime, cache, etag)) => {
40 // The embedded assets carry a content hash, so a repeat visit can
41 // be answered without sending the body again.
42 if let Some(tag) = &etag
43 && headers
44 .get(header::IF_NONE_MATCH)
45 .and_then(|v| v.to_str().ok())
46 .is_some_and(|v| v.split(',').any(|t| t.trim() == tag || t.trim() == "*"))
47 {
48 return (
49 StatusCode::NOT_MODIFIED,
50 [(header::CACHE_CONTROL, cache), (header::ETAG, tag.clone())],
51 )
52 .into_response();
53 }
54 let mut res = (
55 [(header::CONTENT_TYPE, mime), (header::CACHE_CONTROL, cache)],
56 body(bytes),
57 )
58 .into_response();
59 if let Some(tag) = etag
60 && let Ok(v) = tag.parse()
61 {
62 res.headers_mut().insert(header::ETAG, v);
63 }
64 res
65 }
66 None => match crate::assets::get_asset("index.html") {
67 Some((bytes, mime, _, _)) => (
68 [
69 (header::CONTENT_TYPE, mime),
70 (header::CACHE_CONTROL, "no-cache".to_string()),
71 ],
72 body(bytes),
73 )
74 .into_response(),
75 None => DEV_HINT.into_response(),
76 },
77 }
78}
79
80/// Asset bytes as a response body, without copying the embedded ones.
81fn body(bytes: Cow<'static, [u8]>) -> axum::body::Bytes {
82 match bytes {
83 Cow::Borrowed(b) => axum::body::Bytes::from_static(b),
84 Cow::Owned(v) => axum::body::Bytes::from(v),
85 }
86}
87