fire_http/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 fire_http as fire;
69/// use fire::fs::StaticFiles;
70///
71/// const FILES: StaticFiles = StaticFiles::new("/files", "./www/");
72///
73/// #[tokio::main]
74/// async fn main() {
75/// 	let mut server = fire::build("127.0.0.1:0").await.unwrap();
76/// 	server.add_route(FILES);
77/// }
78/// ```
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub struct StaticFiles {
81	uri: &'static str,
82	path: &'static str,
83	caching: CachingBuilder,
84}
85
86impl StaticFiles {
87	/// Creates a `StaticFiles` with Default caching settings
88	pub const fn new(uri: &'static str, path: &'static str) -> Self {
89		Self {
90			uri,
91			path,
92			caching: CachingBuilder::Default,
93		}
94	}
95
96	pub const fn no_cache(uri: &'static str, path: &'static str) -> Self {
97		Self {
98			uri,
99			path,
100			caching: CachingBuilder::None,
101		}
102	}
103
104	pub const fn cache_with_age(
105		uri: &'static str,
106		path: &'static str,
107		max_age: Duration,
108	) -> Self {
109		Self {
110			uri,
111			path,
112			caching: CachingBuilder::MaxAge(max_age),
113		}
114	}
115}
116
117impl IntoRoute for StaticFiles {
118	type IntoRoute = StaticFilesRoute;
119
120	fn into_route(self) -> StaticFilesRoute {
121		StaticFilesRoute {
122			uri: self.uri.into(),
123			path: self.path.into(),
124			caching: self.caching.into(),
125		}
126	}
127}
128
129/// Static get handler which servers files from a directory.
130///
131/// ## Example
132/// ```
133/// # use fire_http as fire;
134/// use fire::fs::StaticFilesOwned;
135///
136/// #[tokio::main]
137/// async fn main() {
138/// 	let mut server = fire::build("127.0.0.1:0").await.unwrap();
139/// 	server.add_route(
140/// 		StaticFilesOwned::new("/files".into(), "./www/".into())
141/// 	);
142/// }
143/// ```
144#[derive(Debug, Clone, PartialEq, Eq)]
145pub struct StaticFilesOwned {
146	uri: String,
147	path: String,
148	caching: CachingBuilder,
149}
150
151impl StaticFilesOwned {
152	/// Creates a `StaticFiles` with Default caching settings
153	pub fn new(uri: String, path: String) -> Self {
154		Self {
155			uri,
156			path,
157			caching: CachingBuilder::Default,
158		}
159	}
160
161	pub fn no_cache(uri: String, path: String) -> Self {
162		Self {
163			uri,
164			path,
165			caching: CachingBuilder::None,
166		}
167	}
168
169	pub fn cache_with_age(
170		uri: String,
171		path: String,
172		max_age: Duration,
173	) -> Self {
174		Self {
175			uri,
176			path,
177			caching: CachingBuilder::MaxAge(max_age),
178		}
179	}
180}
181
182impl IntoRoute for StaticFilesOwned {
183	type IntoRoute = StaticFilesRoute;
184
185	fn into_route(self) -> StaticFilesRoute {
186		StaticFilesRoute {
187			uri: self.uri.trim_end_matches('/').to_string().into(),
188			path: self.path.into(),
189			caching: self.caching.into(),
190		}
191	}
192}
193
194#[doc(hidden)]
195pub struct StaticFilesRoute {
196	// should not end with a trailing slash
197	uri: Cow<'static, str>,
198	path: Cow<'static, str>,
199	caching: Option<Caching>,
200}
201
202impl Route for StaticFilesRoute {
203	fn validate_requirements(&self, _params: &ParamsNames, _data: &Resources) {}
204
205	fn path(&self) -> RoutePath {
206		RoutePath {
207			method: Some(Method::GET),
208			path: format!("{}/{{*rem}}", self.uri).into(),
209		}
210	}
211
212	fn call<'a>(
213		&'a self,
214		req: &'a mut Request,
215		_params: &'a PathParams,
216		_: &'a Resources,
217	) -> PinnedFuture<'a, crate::Result<Response>> {
218		let uri = &self.uri;
219		let caching = self.caching.clone();
220
221		PinnedFuture::new(async move {
222			let res_path_buf =
223				req.header().uri().path()[uri.len()..].into_path_buf();
224
225			// validate path buf
226			// if path is a directory serve_file will return NotFound
227			let path_buf = res_path_buf
228				.map_err(|e| Error::new(ClientErrorKind::BadRequest, e))?;
229
230			// build full pathbuf
231			let path_buf = Path::new(&*self.path).join(path_buf);
232
233			serve_file(path_buf, &req, caching)
234				.await
235				.map_err(Error::from_client_io)
236		})
237	}
238}
239
240/// Static get handler which servers/returns a file.
241///
242/// ## Example
243/// ```
244/// # use fire_http as fire;
245/// use fire::fs::StaticFile;
246///
247/// const INDEX: StaticFile = StaticFile::new("/", "./www/index.html");
248/// ```
249#[derive(Debug, Clone, Copy, PartialEq, Eq)]
250pub struct StaticFile {
251	uri: &'static str,
252	path: &'static str,
253	caching: CachingBuilder,
254}
255
256impl StaticFile {
257	/// Creates a `StaticFile` with Default caching settings
258	pub const fn new(uri: &'static str, path: &'static str) -> Self {
259		Self {
260			uri,
261			path,
262			caching: CachingBuilder::Default,
263		}
264	}
265
266	pub const fn no_cache(uri: &'static str, path: &'static str) -> Self {
267		Self {
268			uri,
269			path,
270			caching: CachingBuilder::None,
271		}
272	}
273
274	pub const fn cache_with_age(
275		uri: &'static str,
276		path: &'static str,
277		max_age: Duration,
278	) -> Self {
279		Self {
280			uri,
281			path,
282			caching: CachingBuilder::MaxAge(max_age),
283		}
284	}
285}
286
287impl IntoRoute for StaticFile {
288	type IntoRoute = StaticFileRoute;
289
290	fn into_route(self) -> StaticFileRoute {
291		StaticFileRoute {
292			uri: self.uri.trim_end_matches('/').into(),
293			path: self.path.into(),
294			caching: self.caching.into(),
295		}
296	}
297}
298
299/// Static get handler which servers/returns a file.
300///
301/// ## Example
302/// ```
303/// # use fire_http as fire;
304/// use fire::fs::StaticFileOwned;
305///
306/// #[tokio::main]
307/// async fn main() {
308/// 	let mut server = fire::build("127.0.0.1:0").await.unwrap();
309/// 	server.add_route(
310/// 		StaticFileOwned::new("/files/file".into(), "./www/file".into())
311/// 	);
312/// }
313/// ```
314#[derive(Debug, Clone, PartialEq, Eq)]
315pub struct StaticFileOwned {
316	uri: String,
317	path: String,
318	caching: CachingBuilder,
319}
320
321impl StaticFileOwned {
322	/// Creates a `StaticFile` with Default caching settings
323	pub const fn new(uri: String, path: String) -> Self {
324		Self {
325			uri,
326			path,
327			caching: CachingBuilder::Default,
328		}
329	}
330
331	pub const fn no_cache(uri: String, path: String) -> Self {
332		Self {
333			uri,
334			path,
335			caching: CachingBuilder::None,
336		}
337	}
338
339	pub const fn cache_with_age(
340		uri: String,
341		path: String,
342		max_age: Duration,
343	) -> Self {
344		Self {
345			uri,
346			path,
347			caching: CachingBuilder::MaxAge(max_age),
348		}
349	}
350}
351
352impl IntoRoute for StaticFileOwned {
353	type IntoRoute = StaticFileRoute;
354
355	fn into_route(self) -> StaticFileRoute {
356		StaticFileRoute {
357			uri: self.uri.into(),
358			path: self.path.into(),
359			caching: self.caching.into(),
360		}
361	}
362}
363
364#[doc(hidden)]
365pub struct StaticFileRoute {
366	uri: Cow<'static, str>,
367	path: Cow<'static, str>,
368	caching: Option<Caching>,
369}
370
371impl Route for StaticFileRoute {
372	fn validate_requirements(&self, _params: &ParamsNames, _data: &Resources) {}
373
374	fn path(&self) -> RoutePath {
375		RoutePath {
376			method: Some(Method::GET),
377			path: self.uri.clone(),
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}