1use std::path::Path;
2
3pub trait FileHandler: Send + Sync {
4 fn can_handle(&self, path: &Path) -> bool;
5 fn get_content_type(&self, path: &Path) -> String;
6 fn handle_file(&self, path: &Path) -> crate::Result<Vec<u8>>;
7}
8
9pub struct DefaultFileHandler;
10
11impl FileHandler for DefaultFileHandler {
12 fn can_handle(&self, _path: &Path) -> bool {
13 true }
15
16 fn get_content_type(&self, path: &Path) -> String {
17 match path.extension().and_then(|ext| ext.to_str()) {
18 Some("html") | Some("htm") => "text/html".to_string(),
19 Some("css") => "text/css".to_string(),
20 Some("js") => "application/javascript".to_string(),
21 Some("json") => "application/json".to_string(),
22 Some("png") => "image/png".to_string(),
23 Some("jpg") | Some("jpeg") => "image/jpeg".to_string(),
24 Some("gif") => "image/gif".to_string(),
25 Some("svg") => "image/svg+xml".to_string(),
26 Some("ico") => "image/x-icon".to_string(),
27 Some("txt") => "text/plain".to_string(),
28 Some("xml") => "application/xml".to_string(),
29 Some("pdf") => "application/pdf".to_string(),
30 _ => "application/octet-stream".to_string(),
31 }
32 }
33
34 fn handle_file(&self, path: &Path) -> crate::Result<Vec<u8>> {
35 std::fs::read(path).map_err(crate::ServerError::from)
36 }
37}
38
39pub trait DirectoryHandler: Send + Sync {
40 fn handle_directory(&self, path: &Path, request_path: &str) -> crate::Result<String>;
41}
42
43pub struct DefaultDirectoryHandler;
44
45impl DirectoryHandler for DefaultDirectoryHandler {
46 fn handle_directory(&self, path: &Path, request_path: &str) -> crate::Result<String> {
47 let entries = std::fs::read_dir(path)?;
48
49 let mut listing = String::from(include_str!("../templates/directory_listing_start.html"));
50
51 if request_path != "/" {
52 listing.push_str(include_str!("../templates/directory_parent_link.html"));
53 }
54
55 listing.push_str(include_str!("../templates/directory_content_start.html"));
56
57 for entry in entries.flatten() {
58 let file_name = entry.file_name();
59 let name = file_name.to_string_lossy();
60 let is_dir = entry.path().is_dir();
61 let display_name = if is_dir {
62 format!("{}/", name)
63 } else {
64 name.to_string()
65 };
66 let class = if is_dir { "dir" } else { "file" };
67
68 listing.push_str(&format!(
69 r#" <a href="{}" style="{}">{}</a>"#,
70 name, class, display_name
71 ));
72 listing.push('\n');
73 }
74
75 listing.push_str(include_str!("../templates/directory_listing_end.html"));
76 Ok(listing)
77 }
78}
79
80pub fn get_404_html() -> &'static str {
81 include_str!("../templates/404.html")
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87 use std::path::Path;
88
89 #[test]
90 fn test_default_file_handler_can_handle_any_file() {
91 let handler = DefaultFileHandler;
92 let path = Path::new("test.txt");
93 assert!(handler.can_handle(path));
94
95 let path = Path::new("some/random/file");
96 assert!(handler.can_handle(path));
97 }
98
99 #[test]
100 fn test_content_type_detection() {
101 let handler = DefaultFileHandler;
102
103 assert_eq!(handler.get_content_type(Path::new("index.html")), "text/html");
104 assert_eq!(handler.get_content_type(Path::new("style.css")), "text/css");
105 assert_eq!(handler.get_content_type(Path::new("script.js")), "application/javascript");
106 assert_eq!(handler.get_content_type(Path::new("data.json")), "application/json");
107
108 assert_eq!(handler.get_content_type(Path::new("image.png")), "image/png");
109 assert_eq!(handler.get_content_type(Path::new("photo.jpg")), "image/jpeg");
110 assert_eq!(handler.get_content_type(Path::new("photo.jpeg")), "image/jpeg");
111 assert_eq!(handler.get_content_type(Path::new("icon.ico")), "image/x-icon");
112 assert_eq!(handler.get_content_type(Path::new("vector.svg")), "image/svg+xml");
113
114 assert_eq!(handler.get_content_type(Path::new("readme.txt")), "text/plain");
115 assert_eq!(handler.get_content_type(Path::new("data.xml")), "application/xml");
116 assert_eq!(handler.get_content_type(Path::new("document.pdf")), "application/pdf");
117
118 assert_eq!(handler.get_content_type(Path::new("file.unknown")), "application/octet-stream");
119
120 assert_eq!(handler.get_content_type(Path::new("noextension")), "application/octet-stream");
121 }
122
123 #[test]
124 fn test_directory_handler_generates_valid_html() {
125 use std::fs;
126 use std::env;
127
128 let temp_dir = env::temp_dir().join("gurty_test");
129 let _ = fs::create_dir_all(&temp_dir);
130
131 let _ = fs::write(temp_dir.join("test.txt"), "test content");
132 let _ = fs::create_dir_all(temp_dir.join("subdir"));
133
134 let handler = DefaultDirectoryHandler;
135 let result = handler.handle_directory(&temp_dir, "/test/");
136
137 assert!(result.is_ok());
138 let html = result.unwrap();
139
140 assert!(html.contains("<!DOCTYPE html>"));
141 assert!(html.contains("<title>Directory Listing</title>"));
142 assert!(html.contains("← Parent Directory"));
143 assert!(html.contains("test.txt"));
144 assert!(html.contains("subdir/"));
145
146 let _ = fs::remove_dir_all(&temp_dir);
147 }
148
149 #[test]
150 fn test_directory_handler_root_path() {
151 use std::fs;
152 use std::env;
153
154 let temp_dir = env::temp_dir().join("gurty_test_root");
155 let _ = fs::create_dir_all(&temp_dir);
156
157 let handler = DefaultDirectoryHandler;
158 let result = handler.handle_directory(&temp_dir, "/");
159
160 assert!(result.is_ok());
161 let html = result.unwrap();
162
163 assert!(!html.contains("← Parent Directory"));
164
165 let _ = fs::remove_dir_all(&temp_dir);
166 }
167
168 #[test]
169 fn test_get_404_html_content() {
170 let html = get_404_html();
171
172 assert!(html.contains("<!DOCTYPE html>"));
173 assert!(html.contains("404 Page Not Found"));
174 assert!(html.contains("The requested path was not found"));
175 assert!(html.contains("Back to home"));
176 }
177
178 #[test]
179 fn test_directory_handler_with_empty_directory() {
180 use std::fs;
181 use std::env;
182
183 let temp_dir = env::temp_dir().join("gurty_test_empty");
184 let _ = fs::create_dir_all(&temp_dir);
185
186 let handler = DefaultDirectoryHandler;
187 let result = handler.handle_directory(&temp_dir, "/empty/");
188
189 assert!(result.is_ok());
190 let html = result.unwrap();
191
192 assert!(html.contains("<!DOCTYPE html>"));
193 assert!(html.contains("Directory Listing"));
194
195 let _ = fs::remove_dir_all(&temp_dir);
196 }
197}