Skip to main content

oxidite_template/
static_files.rs

1use 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/// Configuration for static file serving
9#[derive(Clone)]
10pub struct StaticFiles {
11    root: String,
12    url_prefix: Option<String>,
13}
14
15impl StaticFiles {
16    /// Create a new StaticFiles handler
17    /// 
18    /// # Arguments
19    /// * `root` - The directory on the filesystem to serve files from (e.g., "public")
20    /// * `url_prefix` - Optional URL prefix to strip from the request path (e.g., "/public")
21    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    /// Serve a static file based on the request
29    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        // Remove prefix if configured
38        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        // Clean up leading slashes to make it relative
49        let file_path = file_path.trim_start_matches('/');
50        
51        // Security: prevent directory traversal
52        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        // Check if path is a directory, if so try index.html
59        let full_path = if full_path.is_dir() {
60            full_path.join("index.html")
61        } else {
62            full_path
63        };
64
65        // Read file asynchronously as bytes
66        match tokio::fs::read(&full_path).await {
67            Ok(content) => {
68                // Determine content type based on extension
69                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                // Return 404 Not Found
108                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                // Return 500 Internal Server Error for other errors
119                Err(Error::InternalServerError(format!("Failed to read file: {}", e)))
120            }
121        }
122    }
123}
124
125/// Create a static file handler for a specific directory.
126/// 
127/// # Example
128/// ```rust
129/// router.get("/assets/*", static_handler("public"));
130/// ```
131pub 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
143/// Helper function to serve static files from the "public" directory.
144/// 
145/// This handler serves files relative to the root of the "public" directory.
146/// For example, a request to `/style.css` will serve `public/style.css`.
147pub async fn serve_static(req: OxiditeRequest) -> Result<OxiditeResponse> {
148    let static_files = StaticFiles::new("public", None);
149    static_files.serve(req).await
150}