oxidite_template/
static_files.rs1use oxidite_core::{OxiditeRequest, OxiditeResponse, Error, Result};
2
3use std::path::Path;
4use std::sync::Arc;
5use std::future::Future;
6use std::pin::Pin;
7
8#[derive(Clone)]
10pub struct StaticFiles {
11 root: String,
12 url_prefix: Option<String>,
13}
14
15impl StaticFiles {
16 pub fn new(root: impl Into<String>, url_prefix: Option<String>) -> Self {
22 Self {
23 root: root.into(),
24 url_prefix,
25 }
26 }
27
28 pub async fn serve(&self, req: OxiditeRequest) -> Result<OxiditeResponse> {
30 let path = req.uri().path();
31
32 let file_path = if let Some(prefix) = &self.url_prefix {
34 if path.starts_with(prefix) {
35 path.strip_prefix(prefix).unwrap_or(path)
36 } else {
37 path
38 }
39 } else {
40 path
41 };
42
43 let file_path = file_path.trim_start_matches('/');
45
46 if file_path.contains("..") {
48 return Err(Error::BadRequest("Invalid path".to_string()));
49 }
50
51 let full_path = Path::new(&self.root).join(file_path);
52
53 let full_path = if full_path.is_dir() {
55 full_path.join("index.html")
56 } else {
57 full_path
58 };
59
60 match std::fs::read_to_string(&full_path) {
62 Ok(content) => {
63 let content_type = if full_path.extension().map_or(false, |ext| ext == "css") {
65 "text/css"
66 } else if full_path.extension().map_or(false, |ext| ext == "js") {
67 "application/javascript"
68 } else if full_path.extension().map_or(false, |ext| ext == "svg") {
69 "image/svg+xml"
70 } else if full_path.extension().map_or(false, |ext| ext == "png") {
71 "image/png"
72 } else if full_path.extension().map_or(false, |ext| ext == "jpg" || ext == "jpeg") {
73 "image/jpeg"
74 } else if full_path.extension().map_or(false, |ext| ext == "html") {
75 "text/html"
76 } else if full_path.extension().map_or(false, |ext| ext == "json") {
77 "application/json"
78 } else {
79 "text/plain"
80 };
81
82 Ok(OxiditeResponse::html(content))
83 },
84 Err(_) => {
85 Ok(OxiditeResponse::html("404 Not Found"))
87 }
88 }
89 }
90}
91
92pub fn static_handler(root: impl Into<String>) -> impl Fn(OxiditeRequest) -> Pin<Box<dyn Future<Output = Result<OxiditeResponse>> + Send>> + Send + Sync + 'static {
99 let root = root.into();
100 let static_files = Arc::new(StaticFiles::new(root, None));
101
102 move |req| {
103 let static_files = static_files.clone();
104 Box::pin(async move {
105 static_files.serve(req).await
106 })
107 }
108}
109
110pub async fn serve_static(req: OxiditeRequest) -> Result<OxiditeResponse> {
115 let static_files = StaticFiles::new("public", None);
116 static_files.serve(req).await
117}