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        let path = req.uri().path();
31        
32        // Remove prefix if configured
33        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        // Clean up leading slashes to make it relative
44        let file_path = file_path.trim_start_matches('/');
45        
46        // Security: prevent directory traversal
47        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        // Check if path is a directory, if so try index.html
54        let full_path = if full_path.is_dir() {
55            full_path.join("index.html")
56        } else {
57            full_path
58        };
59
60        // Read file
61        match std::fs::read_to_string(&full_path) {
62            Ok(content) => {
63                // Set content type based on extension
64                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                // Return 404 Response instead of Error
86                Ok(OxiditeResponse::html("404 Not Found"))
87            }
88        }
89    }
90}
91
92/// Create a static file handler for a specific directory.
93/// 
94/// # Example
95/// ```rust
96/// router.get("/assets/*", static_handler("public"));
97/// ```
98pub 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
110/// Helper function to serve static files from the "public" directory.
111/// 
112/// This handler serves files relative to the root of the "public" directory.
113/// For example, a request to `/style.css` will serve `public/style.css`.
114pub async fn serve_static(req: OxiditeRequest) -> Result<OxiditeResponse> {
115    let static_files = StaticFiles::new("public", None);
116    static_files.serve(req).await
117}