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