1use super::{with_file, with_partial_file};
2use super::{Caching, IntoPathBuf, Range};
3
4use crate::error::ClientErrorKind;
5use crate::header::{Method, StatusCode};
6use crate::into::{IntoResponse, IntoRoute};
7use crate::routes::{ParamsNames, PathParams, Route, RoutePath};
8use crate::util::PinnedFuture;
9use crate::{Error, Request, Resources, Response};
10
11use std::borrow::Cow;
12use std::io;
13use std::path::Path;
14use std::time::Duration;
15
16pub async fn serve_file(
18 path: impl AsRef<Path>,
19 req: &Request,
20 caching: Option<Caching>,
21) -> io::Result<Response> {
22 if matches!(&caching, Some(c) if c.if_none_match(req.header())) {
24 return Ok(caching.unwrap().into_response());
25 }
26
27 let range = Range::parse(req.header());
28
29 let mut res = match range {
30 Some(range) => with_partial_file(path, range).await?.into_response(),
31 None => with_file(path).await?.into_response(),
32 };
33
34 if let Some(caching) = caching {
36 if matches!(
37 res.header.status_code,
38 StatusCode::OK | StatusCode::NOT_FOUND
39 ) {
40 caching.complete_header(&mut res.header);
41 }
42 }
43
44 Ok(res)
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub(super) enum CachingBuilder {
49 None,
50 Default,
51 MaxAge(Duration),
52}
53
54impl From<CachingBuilder> for Option<Caching> {
55 fn from(b: CachingBuilder) -> Self {
56 match b {
57 CachingBuilder::None => None,
58 CachingBuilder::Default => Some(Caching::default()),
59 CachingBuilder::MaxAge(age) => Some(Caching::new(age)),
60 }
61 }
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub struct StaticFiles {
80 uri: &'static str,
81 path: &'static str,
82 caching: CachingBuilder,
83}
84
85impl StaticFiles {
86 pub const fn new(uri: &'static str, path: &'static str) -> Self {
88 Self {
89 uri,
90 path,
91 caching: CachingBuilder::Default,
92 }
93 }
94
95 pub const fn no_cache(uri: &'static str, path: &'static str) -> Self {
96 Self {
97 uri,
98 path,
99 caching: CachingBuilder::None,
100 }
101 }
102
103 pub const fn cache_with_age(
104 uri: &'static str,
105 path: &'static str,
106 max_age: Duration,
107 ) -> Self {
108 Self {
109 uri,
110 path,
111 caching: CachingBuilder::MaxAge(max_age),
112 }
113 }
114}
115
116impl IntoRoute for StaticFiles {
117 type IntoRoute = StaticFilesRoute;
118
119 fn into_route(self) -> StaticFilesRoute {
120 StaticFilesRoute {
121 uri: self.uri.into(),
122 path: self.path.into(),
123 caching: self.caching.into(),
124 }
125 }
126}
127
128#[derive(Debug, Clone, PartialEq, Eq)]
143pub struct StaticFilesOwned {
144 uri: String,
145 path: String,
146 caching: CachingBuilder,
147}
148
149impl StaticFilesOwned {
150 pub fn new(uri: String, path: String) -> Self {
152 Self {
153 uri,
154 path,
155 caching: CachingBuilder::Default,
156 }
157 }
158
159 pub fn no_cache(uri: String, path: String) -> Self {
160 Self {
161 uri,
162 path,
163 caching: CachingBuilder::None,
164 }
165 }
166
167 pub fn cache_with_age(
168 uri: String,
169 path: String,
170 max_age: Duration,
171 ) -> Self {
172 Self {
173 uri,
174 path,
175 caching: CachingBuilder::MaxAge(max_age),
176 }
177 }
178}
179
180impl IntoRoute for StaticFilesOwned {
181 type IntoRoute = StaticFilesRoute;
182
183 fn into_route(self) -> StaticFilesRoute {
184 StaticFilesRoute {
185 uri: self.uri.trim_end_matches('/').to_string().into(),
186 path: self.path.into(),
187 caching: self.caching.into(),
188 }
189 }
190}
191
192#[doc(hidden)]
193pub struct StaticFilesRoute {
194 uri: Cow<'static, str>,
196 path: Cow<'static, str>,
197 caching: Option<Caching>,
198}
199
200impl Route for StaticFilesRoute {
201 fn validate_requirements(&self, _params: &ParamsNames, _data: &Resources) {}
202
203 fn path(&self) -> RoutePath {
204 RoutePath {
205 method: Some(Method::GET),
206 path: format!("{}/{{*rem}}", self.uri).into(),
207 }
208 }
209
210 fn call<'a>(
211 &'a self,
212 req: &'a mut Request,
213 _params: &'a PathParams,
214 _: &'a Resources,
215 ) -> PinnedFuture<'a, crate::Result<Response>> {
216 let uri = &self.uri;
217 let caching = self.caching.clone();
218
219 PinnedFuture::new(async move {
220 let res_path_buf =
221 req.header().uri().path()[uri.len()..].into_path_buf();
222
223 let path_buf = res_path_buf
226 .map_err(|e| Error::new(ClientErrorKind::BadRequest, e))?;
227
228 let path_buf = Path::new(&*self.path).join(path_buf);
230
231 serve_file(path_buf, &req, caching)
232 .await
233 .map_err(Error::from_client_io)
234 })
235 }
236}
237
238#[derive(Debug, Clone, Copy, PartialEq, Eq)]
247pub struct StaticFile {
248 uri: &'static str,
249 path: &'static str,
250 caching: CachingBuilder,
251}
252
253impl StaticFile {
254 pub const fn new(uri: &'static str, path: &'static str) -> Self {
256 Self {
257 uri,
258 path,
259 caching: CachingBuilder::Default,
260 }
261 }
262
263 pub const fn no_cache(uri: &'static str, path: &'static str) -> Self {
264 Self {
265 uri,
266 path,
267 caching: CachingBuilder::None,
268 }
269 }
270
271 pub const fn cache_with_age(
272 uri: &'static str,
273 path: &'static str,
274 max_age: Duration,
275 ) -> Self {
276 Self {
277 uri,
278 path,
279 caching: CachingBuilder::MaxAge(max_age),
280 }
281 }
282}
283
284impl IntoRoute for StaticFile {
285 type IntoRoute = StaticFileRoute;
286
287 fn into_route(self) -> StaticFileRoute {
288 StaticFileRoute {
289 uri: self.uri.trim_end_matches('/').into(),
290 path: self.path.into(),
291 caching: self.caching.into(),
292 }
293 }
294}
295
296#[derive(Debug, Clone, PartialEq, Eq)]
311pub struct StaticFileOwned {
312 uri: String,
313 path: String,
314 caching: CachingBuilder,
315}
316
317impl StaticFileOwned {
318 pub const fn new(uri: String, path: String) -> Self {
320 Self {
321 uri,
322 path,
323 caching: CachingBuilder::Default,
324 }
325 }
326
327 pub const fn no_cache(uri: String, path: String) -> Self {
328 Self {
329 uri,
330 path,
331 caching: CachingBuilder::None,
332 }
333 }
334
335 pub const fn cache_with_age(
336 uri: String,
337 path: String,
338 max_age: Duration,
339 ) -> Self {
340 Self {
341 uri,
342 path,
343 caching: CachingBuilder::MaxAge(max_age),
344 }
345 }
346}
347
348impl IntoRoute for StaticFileOwned {
349 type IntoRoute = StaticFileRoute;
350
351 fn into_route(self) -> StaticFileRoute {
352 StaticFileRoute {
353 uri: self.uri.trim_end_matches('/').to_string().into(),
354 path: self.path.into(),
355 caching: self.caching.into(),
356 }
357 }
358}
359
360#[doc(hidden)]
361pub struct StaticFileRoute {
362 uri: Cow<'static, str>,
363 path: Cow<'static, str>,
364 caching: Option<Caching>,
365}
366
367impl Route for StaticFileRoute {
368 fn validate_requirements(&self, _params: &ParamsNames, _data: &Resources) {}
369
370 fn path(&self) -> RoutePath {
371 RoutePath {
372 method: Some(Method::GET),
373 path: if self.uri.is_empty() {
374 "/".into()
375 } else {
376 self.uri.clone()
377 },
378 }
379 }
380
381 fn call<'a>(
382 &'a self,
383 req: &'a mut Request,
384 _params: &'a PathParams,
385 _data: &'a Resources,
386 ) -> PinnedFuture<'a, crate::Result<Response>> {
387 PinnedFuture::new(async move {
388 serve_file(&*self.path, &req, self.caching.clone())
389 .await
390 .map_err(Error::from_client_io)
391 })
392 }
393}