use std::{
collections::HashMap,
net::TcpListener,
path::PathBuf,
sync::{
Arc,
atomic::{AtomicBool, Ordering},
},
thread,
time::Duration,
};
use crate::prelude::*;
#[derive(Default, Clone)]
pub struct Server {
mappings: HashMap<(String, HttpMethod), RequestHandler>,
static_mounts: HashMap<String, String>,
}
impl Server {
pub fn new() -> Self {
Self::default()
}
pub fn map(&mut self, route: &str, method: HttpMethod, handler: RequestHandler) {
self.mappings.insert((route.to_string(), method), handler);
}
pub fn mount_static(&mut self, http_path: &str, fs_path: &str) {
self.static_mounts
.insert(http_path.to_string(), fs_path.to_string());
}
pub fn thread_start_at_port(
self,
port: usize,
stop_flag: Arc<AtomicBool>,
) -> thread::JoinHandle<()> {
let listener = TcpListener::bind(format!("127.0.0.1:{}", port)).unwrap();
listener.set_nonblocking(true).unwrap();
let handle = std::thread::spawn(move || {
while !stop_flag.load(Ordering::SeqCst) {
match listener.accept() {
Ok((mut stream, _)) => {
let req = Request::parse(&mut stream);
let res = match req {
Some(req) => self.handle_req(req).unwrap_or_else(Self::handler_404),
None => Self::handler_400(),
};
res.write_to_stream(&mut stream);
}
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
thread::sleep(Duration::from_millis(10))
}
Err(e) => {
eprintln!("{e}");
break;
}
}
}
});
thread::sleep(Duration::from_millis(100));
handle
}
fn handle_req(&self, req: Request) -> Option<Response> {
println!();
println!("{:?} Request recieved at '{}'", req.method, req.path);
match req.method {
HttpMethod::Get => {
if self.static_mounts.keys().any(|hp| req.path.starts_with(hp)) {
return self.static_handler(req);
}
self.mappings.get(&(req.path.clone(), HttpMethod::Get))?(req)
}
HttpMethod::Post => self.mappings.get(&(req.path.clone(), HttpMethod::Post))?(req),
}
}
fn static_handler(&self, req: Request) -> Option<Response> {
let http_path_root = self
.static_mounts
.keys()
.find(|http_path_root| req.path.starts_with(*http_path_root))
.unwrap();
let fs_path_root = self.static_mounts.get(http_path_root).unwrap();
let path = req
.path
.to_string()
.replacen(http_path_root, fs_path_root, 1);
println!(
"static map from http-path to fs-path: {} => {}",
req.path, path
);
let mut path_buf = PathBuf::from(path);
if !path_buf.exists() {
return None;
}
match path_buf.metadata() {
Ok(metadata) => {
if metadata.is_dir() {
path_buf.push("index.html");
}
eprintln!("File path: {}", path_buf.to_str().unwrap());
Some(
ResponseBuilder::new()
.status_ok()
.body_and_auto_content_headers(HttpBody::create_html(
path_buf.to_str().unwrap(),
))
.build(),
)
}
Err(_) => None,
}
}
fn handler_404() -> Response {
println!("returning generic 404...");
ResponseBuilder::new()
.status(404, "Not Found")
.body_and_auto_content_headers(HttpBody::create_html("static/generic/404.html"))
.build()
}
fn handler_400() -> Response {
println!("returning generic 400 ...");
ResponseBuilder::new()
.status(400, "Bad Request")
.body_and_auto_content_headers(HttpBody::create_html("static/generic/400.html"))
.build()
}
}
trait DebugNone {
fn debug_none(self, text: &str) -> Self;
}
impl<T> DebugNone for Option<T> {
fn debug_none(self, text: &str) -> Self {
if self.is_none() {
println!("{text}");
}
self
}
}