1mod serve;
4
5use sova_core::extend::normalize_path;
6use sova_core::{App, Plugin, Request, Router};
7use std::path::{Path, PathBuf};
8use std::sync::Arc;
9use std::time::Duration;
10
11pub struct Static {
13 mount: String,
14 dir: PathBuf,
15 max_age: Duration,
16 max_age_explicit: bool,
17 immutable: bool,
18 allow_dotfiles: bool,
19 index: bool,
20}
21
22impl Static {
23 pub fn new(mount: impl Into<String>, dir: impl Into<PathBuf>) -> Self {
24 let mut mount = normalize_path(&mount.into());
25 while mount.len() > 1 && mount.ends_with('/') {
26 mount.pop();
27 }
28 Self {
29 mount,
30 dir: dir.into(),
31 max_age: Duration::from_secs(3600),
32 max_age_explicit: false,
33 immutable: false,
34 allow_dotfiles: false,
35 index: true,
36 }
37 }
38
39 pub fn max_age(mut self, max_age: Duration) -> Self {
41 self.max_age = max_age;
42 self.max_age_explicit = true;
43 self
44 }
45
46 pub fn immutable(mut self, on: bool) -> Self {
48 self.immutable = on;
49 self
50 }
51
52 pub fn dotfiles_allow(mut self) -> Self {
54 self.allow_dotfiles = true;
55 self
56 }
57
58 pub fn index(mut self, on: bool) -> Self {
60 self.index = on;
61 self
62 }
63
64 pub fn register(self, router: &mut Router) {
66 let opts = Arc::new(serve::StaticOpts {
67 max_age: self.max_age,
68 immutable: self.immutable,
69 allow_dotfiles: self.allow_dotfiles,
70 });
71 let dir = Arc::new(self.dir);
72 let mount = self.mount;
73
74 if self.index {
75 let dir_index = Arc::clone(&dir);
76 let opts_index = Arc::clone(&opts);
77 router.get(&mount, move |req: Request| {
78 let dir = Arc::clone(&dir_index);
79 let opts = Arc::clone(&opts_index);
80 async move {
81 serve::serve_in(
82 dir.as_path(),
83 Path::new("index.html"),
84 serve::FileOptions::from_request(&req),
85 &opts,
86 )
87 .await
88 }
89 });
90 }
91
92 let wildcard = if mount == "/" {
93 "/*path".to_string()
94 } else {
95 format!("{mount}/*path")
96 };
97
98 let dir_files = Arc::clone(&dir);
99 router.get(&wildcard, move |req: Request| {
100 let dir = Arc::clone(&dir_files);
101 let opts = Arc::clone(&opts);
102 async move {
103 let rel = req
104 .param("path")
105 .map(PathBuf::from)
106 .unwrap_or_else(|| PathBuf::from("index.html"));
107 serve::serve_in(
108 dir.as_path(),
109 &rel,
110 serve::FileOptions::from_request(&req),
111 &opts,
112 )
113 .await
114 }
115 });
116 }
117}
118
119impl Plugin for Static {
120 fn id(&self) -> &'static str {
121 "static"
122 }
123
124 fn meta(&self) -> sova_core::PluginMeta {
125 sova_core::PluginMeta::new("Static files")
126 .description("Serve files from a directory under a mount path")
127 .version(env!("CARGO_PKG_VERSION"))
128 }
129
130 fn install(mut self, app: &mut App) {
131 if !self.max_age_explicit {
132 if let Some(doc) = app.config_doc() {
133 if let Some(section) = doc.section("static") {
134 if let Some(s) = section.get("max_age").and_then(|v| v.as_str()) {
135 if let Ok(d) = sova_core::extend::parse_duration(s) {
136 self.max_age = d;
137 }
138 } else if let Some(n) = section.get("max_age").and_then(|v| v.as_integer()) {
139 self.max_age = Duration::from_secs(n as u64);
140 }
141 }
142 }
143 }
144 self.register(app);
145 }
146}