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 use hyper::{Response, header};
31 use http_body_util::{Full, BodyExt};
32 use bytes::Bytes;
33 use http::StatusCode;
34
35 let path = req.uri().path();
36
37 let file_path = if let Some(prefix) = &self.url_prefix {
39 if path.starts_with(prefix) {
40 path.strip_prefix(prefix).unwrap_or(path)
41 } else {
42 path
43 }
44 } else {
45 path
46 };
47
48 let file_path = file_path.trim_start_matches('/');
50
51 if file_path.contains("..") {
53 return Err(Error::BadRequest("Invalid path".to_string()));
54 }
55
56 let full_path = Path::new(&self.root).join(file_path);
57
58 let full_path = if full_path.is_dir() {
60 full_path.join("index.html")
61 } else {
62 full_path
63 };
64
65 match tokio::fs::read(&full_path).await {
67 Ok(content) => {
68 let content_type = full_path.extension()
70 .and_then(|ext| ext.to_str())
71 .map(|ext| match ext.to_lowercase().as_str() {
72 "html" | "htm" => "text/html",
73 "css" => "text/css",
74 "js" | "mjs" => "application/javascript",
75 "json" => "application/json",
76 "png" => "image/png",
77 "jpg" | "jpeg" => "image/jpeg",
78 "gif" => "image/gif",
79 "svg" => "image/svg+xml",
80 "ico" => "image/x-icon",
81 "webp" => "image/webp",
82 "woff" => "font/woff",
83 "woff2" => "font/woff2",
84 "ttf" => "font/ttf",
85 "otf" => "font/otf",
86 "eot" => "application/vnd.ms-fontobject",
87 "wasm" => "application/wasm",
88 "mp4" => "video/mp4",
89 "webm" => "video/webm",
90 "txt" => "text/plain",
91 "xml" => "text/xml",
92 _ => "application/octet-stream",
93 })
94 .unwrap_or("application/octet-stream");
95
96 let res = Response::builder()
97 .status(StatusCode::OK)
98 .header(header::CONTENT_TYPE, content_type)
99 .header(header::CONTENT_LENGTH, content.len())
100 .header(header::SERVER, "Oxidite/2.0.1")
101 .body(Full::new(Bytes::from(content)).map_err(|e| match e {}).boxed())
102 .map_err(|e| Error::InternalServerError(format!("Failed to build response: {}", e)))?;
103
104 Ok(OxiditeResponse::new(res))
105 },
106 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
107 let res = Response::builder()
109 .status(StatusCode::NOT_FOUND)
110 .header(header::CONTENT_TYPE, "text/plain")
111 .header(header::SERVER, "Oxidite/2.0.1")
112 .body(Full::new(Bytes::from("404 Not Found")).map_err(|e| match e {}).boxed())
113 .map_err(|e| Error::InternalServerError(format!("Failed to build response: {}", e)))?;
114
115 Ok(OxiditeResponse::new(res))
116 },
117 Err(e) => {
118 Err(Error::InternalServerError(format!("Failed to read file: {}", e)))
120 }
121 }
122 }
123}
124
125pub fn static_handler(root: impl Into<String>) -> impl Fn(OxiditeRequest) -> Pin<Box<dyn Future<Output = Result<OxiditeResponse>> + Send>> + Send + Sync + 'static {
132 let root = root.into();
133 let static_files = Arc::new(StaticFiles::new(root, None));
134
135 move |req| {
136 let static_files = static_files.clone();
137 Box::pin(async move {
138 static_files.serve(req).await
139 })
140 }
141}
142
143pub async fn serve_static(req: OxiditeRequest) -> Result<OxiditeResponse> {
148 let static_files = StaticFiles::new("public", None);
149 static_files.serve(req).await
150}