chuchi/fs/
static_files.rs

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
16/// returns io::Error not found if the path is a directory
17pub async fn serve_file(
18	path: impl AsRef<Path>,
19	req: &Request,
20	caching: Option<Caching>,
21) -> io::Result<Response> {
22	// check caching and if the etag matches return NOT_MODIFIED
23	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	// set etag
35	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/// Static get handler which servers files from a directory.
65///
66/// ## Example
67/// ```
68/// use chuchi::fs::StaticFiles;
69///
70/// const FILES: StaticFiles = StaticFiles::new("/files", "./www/");
71///
72/// #[tokio::main]
73/// async fn main() {
74/// 	let mut server = chuchi::build("127.0.0.1:0").await.unwrap();
75/// 	server.add_route(FILES);
76/// }
77/// ```
78#[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	/// Creates a `StaticFiles` with Default caching settings
87	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/// Static get handler which servers files from a directory.
129///
130/// ## Example
131/// ```
132/// use chuchi::fs::StaticFilesOwned;
133///
134/// #[tokio::main]
135/// async fn main() {
136/// 	let mut server = chuchi::build("127.0.0.1:0").await.unwrap();
137/// 	server.add_route(
138/// 		StaticFilesOwned::new("/files".into(), "./www/".into())
139/// 	);
140/// }
141/// ```
142#[derive(Debug, Clone, PartialEq, Eq)]
143pub struct StaticFilesOwned {
144	uri: String,
145	path: String,
146	caching: CachingBuilder,
147}
148
149impl StaticFilesOwned {
150	/// Creates a `StaticFiles` with Default caching settings
151	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	// should not end with a trailing slash
195	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			// validate path buf
224			// if path is a directory serve_file will return NotFound
225			let path_buf = res_path_buf
226				.map_err(|e| Error::new(ClientErrorKind::BadRequest, e))?;
227
228			// build full pathbuf
229			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/// Static get handler which servers/returns a file.
239///
240/// ## Example
241/// ```
242/// use chuchi::fs::StaticFile;
243///
244/// const INDEX: StaticFile = StaticFile::new("/", "./www/index.html");
245/// ```
246#[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	/// Creates a `StaticFile` with Default caching settings
255	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/// Static get handler which servers/returns a file.
297///
298/// ## Example
299/// ```
300/// use chuchi::fs::StaticFileOwned;
301///
302/// #[tokio::main]
303/// async fn main() {
304/// 	let mut server = chuchi::build("127.0.0.1:0").await.unwrap();
305/// 	server.add_route(
306/// 		StaticFileOwned::new("/files/file".into(), "./www/file".into())
307/// 	);
308/// }
309/// ```
310#[derive(Debug, Clone, PartialEq, Eq)]
311pub struct StaticFileOwned {
312	uri: String,
313	path: String,
314	caching: CachingBuilder,
315}
316
317impl StaticFileOwned {
318	/// Creates a `StaticFile` with Default caching settings
319	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}