tako_rs_streams/static/
dir.rs1use std::path::Path;
2use std::path::PathBuf;
3
4use http::header;
5
6#[doc(alias = "static")]
8#[doc(alias = "serve_dir")]
9pub struct ServeDir {
10 pub(crate) base_dir: PathBuf,
11 pub(crate) fallback: Option<PathBuf>,
12 pub(crate) index_files: Vec<String>,
13 pub(crate) precompressed: PrecompressedPolicy,
14 pub(crate) sanitized_base: Option<PathBuf>,
15}
16
17#[derive(Debug, Clone, Copy, Default)]
20pub struct PrecompressedPolicy {
21 pub brotli: bool,
23 pub gzip: bool,
25}
26
27impl PrecompressedPolicy {
28 pub const fn both() -> Self {
30 Self {
31 brotli: true,
32 gzip: true,
33 }
34 }
35
36 pub const fn brotli_only() -> Self {
38 Self {
39 brotli: true,
40 gzip: false,
41 }
42 }
43
44 pub const fn gzip_only() -> Self {
46 Self {
47 brotli: false,
48 gzip: true,
49 }
50 }
51}
52
53#[must_use]
55pub struct ServeDirBuilder {
56 base_dir: PathBuf,
57 fallback: Option<PathBuf>,
58 index_files: Vec<String>,
59 precompressed: PrecompressedPolicy,
60}
61
62impl ServeDirBuilder {
63 #[inline]
65 pub fn new<P: Into<PathBuf>>(base_dir: P) -> Self {
66 Self {
67 base_dir: base_dir.into(),
68 fallback: None,
69 index_files: vec!["index.html".into(), "index.htm".into()],
70 precompressed: PrecompressedPolicy::default(),
71 }
72 }
73
74 #[inline]
76 pub fn fallback<P: Into<PathBuf>>(mut self, fallback: P) -> Self {
77 self.fallback = Some(fallback.into());
78 self
79 }
80
81 #[inline]
84 pub fn index_files<I, S>(mut self, names: I) -> Self
85 where
86 I: IntoIterator<Item = S>,
87 S: Into<String>,
88 {
89 self.index_files = names.into_iter().map(Into::into).collect();
90 self
91 }
92
93 #[inline]
95 pub fn precompressed(mut self, policy: PrecompressedPolicy) -> Self {
96 self.precompressed = policy;
97 self
98 }
99
100 #[inline]
102 pub fn build(self) -> ServeDir {
103 let sanitized_base = self.base_dir.canonicalize().ok();
104 ServeDir {
105 base_dir: self.base_dir,
106 fallback: self.fallback,
107 index_files: self.index_files,
108 precompressed: self.precompressed,
109 sanitized_base,
110 }
111 }
112}
113
114impl ServeDir {
115 pub fn builder<P: Into<PathBuf>>(base_dir: P) -> ServeDirBuilder {
117 ServeDirBuilder::new(base_dir)
118 }
119
120 pub(crate) fn sanitize_path(&self, req_path: &str) -> Option<PathBuf> {
122 let rel_path = req_path.trim_start_matches('/');
123 if rel_path
125 .split(['/', '\\'])
126 .any(|seg| seg == ".." || seg == ".")
127 {
128 return None;
129 }
130 let joined = self.base_dir.join(rel_path);
131 let canonical = joined.canonicalize().ok()?;
132 let base = self
133 .sanitized_base
134 .clone()
135 .or_else(|| self.base_dir.canonicalize().ok())?;
136 if canonical.starts_with(&base) {
137 Some(canonical)
138 } else {
139 None
140 }
141 }
142
143 fn accepts(headers: &http::HeaderMap, encoding: &str) -> bool {
144 let Some(v) = headers
145 .get(header::ACCEPT_ENCODING)
146 .and_then(|v| v.to_str().ok())
147 else {
148 return false;
149 };
150 for part in v.split(',') {
151 let part = part.trim();
152 let mut name_q = part.split(';');
154 let name = name_q.next().unwrap_or("").trim();
155 let q_zero = name_q.any(|p| p.trim().strip_prefix("q=").is_some_and(|q| q.trim() == "0"));
156 if q_zero {
157 continue;
158 }
159 if name.eq_ignore_ascii_case(encoding) || name == "*" {
160 return true;
161 }
162 }
163 false
164 }
165
166 pub(crate) fn canonical_within_base(&self, p: &Path) -> Option<PathBuf> {
171 let canonical = p.canonicalize().ok()?;
172 let base = self
173 .sanitized_base
174 .clone()
175 .or_else(|| self.base_dir.canonicalize().ok())?;
176 if canonical.starts_with(&base) {
177 Some(canonical)
178 } else {
179 None
180 }
181 }
182
183 pub(crate) fn precompressed_variant(
184 &self,
185 file_path: &Path,
186 headers: &http::HeaderMap,
187 ) -> Option<(PathBuf, &'static str)> {
188 if self.precompressed.brotli && Self::accepts(headers, "br") {
189 let mut p = file_path.as_os_str().to_owned();
190 p.push(".br");
191 let p = PathBuf::from(p);
192 if let Some(canonical) = self.canonical_within_base(&p) {
193 return Some((canonical, "br"));
194 }
195 }
196 if self.precompressed.gzip && Self::accepts(headers, "gzip") {
197 let mut p = file_path.as_os_str().to_owned();
198 p.push(".gz");
199 let p = PathBuf::from(p);
200 if let Some(canonical) = self.canonical_within_base(&p) {
201 return Some((canonical, "gzip"));
202 }
203 }
204 None
205 }
206}