1mod cache;
9
10use std::path::PathBuf;
11
12use axum::Router;
13use axum::body::Body;
14use axum::http::{Request, StatusCode};
15use axum::response::Response;
16use notify::Watcher;
17use std::sync::Arc;
18
19use cache::Cache;
20
21pub struct DevFileServerConfig {
22 pub root_dir: PathBuf,
23 pub spa_fallback: bool,
25}
26
27pub struct DevFileServer {
28 root: PathBuf,
29 spa: bool,
30 cache: Arc<Cache>,
31}
32
33impl DevFileServer {
34 pub fn new(config: DevFileServerConfig) -> Self {
35 let cache = Arc::new(Cache::new(&config.root_dir));
36 let s = Self {
37 root: config.root_dir,
38 spa: config.spa_fallback,
39 cache,
40 };
41 s.spawn_watcher();
42 s
43 }
44
45 fn spawn_watcher(&self) {
46 let root = self.root.clone();
47 let cache = self.cache.clone();
48
49 std::thread::spawn(move || {
50 let (tx, rx) = std::sync::mpsc::channel();
51 let mut watcher =
52 notify::recommended_watcher(move |res: Result<notify::Event, notify::Error>| {
53 if let Ok(e) = res {
54 let _ = tx.send(e);
55 }
56 })
57 .ok();
58
59 if let Some(ref mut w) = watcher {
60 let _ = w.watch(&root, notify::RecursiveMode::Recursive);
61 }
62
63 for event in rx {
64 for path in &event.paths {
65 if let Ok(rel) = path.strip_prefix(&root) {
66 let key = rel.to_string_lossy().replace('\\', "/");
67 if !key.is_empty() {
68 cache.invalidate(&key);
69 }
70 }
71 }
72 }
73 });
74 }
75
76 pub fn into_router(self) -> Router {
77 let cache = self.cache.clone();
78 let spa = self.spa;
79
80 Router::new().fallback(axum::routing::any(move |req: Request<Body>| {
81 let cache = cache.clone();
82 async move {
83 let path = req.uri().path().trim_start_matches('/');
84 serve_file(&cache, path, spa).await
85 }
86 }))
87 }
88}
89
90async fn serve_file(cache: &Cache, path: &str, spa: bool) -> Response {
91 let key = if path.is_empty() { "index.html" } else { path };
92
93 if let Some(resp) = cache.get(key) {
94 return resp;
95 }
96
97 let index_key = format!("{}/index.html", key.trim_end_matches('/'));
99 if let Some(resp) = cache.get(&index_key) {
100 return resp;
101 }
102
103 if spa
105 && !path.contains('.')
106 && let Some(resp) = cache.get("index.html")
107 {
108 return resp;
109 }
110
111 let mut resp = Response::new(Body::from("Not Found"));
112 *resp.status_mut() = StatusCode::NOT_FOUND;
113 resp
114}