use dotenv::dotenv;
use fluxio::service::{make_service_fn, service_fn};
use fluxio::{Body, Request, Response, Server, StatusCode};
use std::collections::HashMap;
use std::convert::Infallible;
use std::env;
use std::future::Future;
use std::net::SocketAddr;
use std::path::PathBuf;
use std::pin::Pin;
use std::sync::Arc;
use tokio::fs as async_fs;
use wtime::{
calc::get_day_name,
local::{format_local_ts, local_ts_sec},
};
use styledlog::Colorize;
use cans::mime::{set_mime_types, insert_mime_type, remove_mime_type};
pub type Req = fluxio::Request<fluxio::Body>;
pub type Reply = Pin<Box<dyn Future<Output = Result<fluxio::Response<fluxio::Body>, std::convert::Infallible>> + Send>>;
#[derive(Clone)]
pub struct Params {
pub dir: Arc<PathBuf>, pub extra: HashMap<String, String>, pub custom_404: Arc<String>,
}
#[derive(Clone)]
pub struct Route {
pub method: fluxio::Method, pub path: String, pub handler: fn(Request<Body>, Params) -> Reply, }
impl Route {
pub fn is_match(&self, req: &Req) -> Option<HashMap<String, String>> {
let path_segments: Vec<&str> = self.path.split('/').collect();
let request_segments: Vec<&str> = req.uri().path().split('/').collect();
if path_segments.len() != request_segments.len() {
return None; }
let mut params = HashMap::new();
for (route_segment, request_segment) in path_segments.iter().zip(request_segments.iter()) {
if route_segment.starts_with('<') && route_segment.ends_with('>') {
let key = route_segment
.trim_matches('<')
.trim_matches('>')
.to_string();
params.insert(key, request_segment.to_string());
} else if route_segment != request_segment {
return None; }
}
Some(params)
}
}
pub struct Fluxor {
pub params: Params, pub routes: Vec<Route>, pub mime_types: HashMap<String, String>, pub custom_404_closure: Option<Arc<dyn Fn(&str) -> String + Send + Sync>>, }
impl Fluxor {
pub fn new() -> Self {
let mime_types = set_mime_types(); Self {
params: Params {
dir: Arc::new(PathBuf::new()),
extra: HashMap::new(),
custom_404: Arc::new(r#"<html><body><h1>404 Not Found</h1><p>404 Page Not Found.</p></body></html>"#.to_string()),
},
routes: Vec::new(),
mime_types, custom_404_closure: None,
}
}
pub fn set_custom_404<F>(&mut self, closure: F)
where
F: Fn(&str) -> String + Send + Sync + 'static,
{
self.custom_404_closure = Some(Arc::new(closure));
}
pub fn set_dir(&mut self, dir: String) {
self.params.dir = Arc::new(PathBuf::from(dir));
}
pub fn route(
&mut self,
method: fluxio::Method,
path: &str,
handler: fn(Request<Body>, Params) -> Reply,
) {
self.routes.push(Route {
method,
path: path.to_string(),
handler,
});
}
pub fn include_mime_types(&mut self, new_types: HashMap<String, String>) {
for (ext, mime) in new_types {
insert_mime_type(&mut self.mime_types, &ext, &mime);
}
}
pub fn exclude_mime_types(&mut self, extensions: Vec<&str>) {
for ext in extensions {
remove_mime_type(&mut self.mime_types, ext);
}
}
pub async fn run(&self, host: &str, port: &str) {
let make_svc = make_service_fn(|_conn| {
let params = self.params.clone();
let routes = self.routes.clone();
let mime_types = self.mime_types.clone();
let custom_404_closure = self.custom_404_closure.clone();
async move {
Ok::<_, Infallible>(service_fn(move |req| {
let params = params.clone();
let routes = routes.clone();
handle_request(req, params.clone(), routes.clone(), mime_types.clone(), custom_404_closure.clone())
}))
}
});
let addr = format!("{}:{}", host, port);
let addr: SocketAddr = addr.parse().expect("Invalid address/port combination");
Server::bind(&addr).serve(make_svc);
let server = Server::bind(&addr).serve(make_svc);
let timestamp = format_local_ts();
let day_name = get_day_name(local_ts_sec());
let formated_addr = format!("{}", addr);
let address = format!(
"{}{}",
"http://".blue().bold(),
formated_addr.blue().italic().bold()
);
let project_name = env_var("PROJECT_NAME", "Fluxor");
let startup_message = format!(
"{} {}\n{}\n🌐 Server running {}: {} 📬\n{}\n🕔 {} {}\n{}\nPress Ctrl-C to shut down the server.",
project_name.bright_green().bold(), "has started.".bright_green().bold(), "[INFO]".cyan(), "on".magenta(), address.underline(), "[TIME]".cyan(), day_name, timestamp.blue(), "[SHUTDOWN]".cyan(),
);
let server_view = env_var("SERVER_VIEW", "show");
server_log(&server_view, &startup_message);
if let Err(e) = server.await {
println!(
"\n[ERROR]\n❌ Server error: '{:?}'\n[TIME]\n🕔 {} {}",
e, day_name, timestamp
);
}
}
}
async fn serve_static_file(req: &Req, params: &Params, mime_types: &HashMap<String, String>) -> Result<Response<Body>, Infallible> {
let path = params.dir.join(req.uri().path().trim_start_matches('/'));
match async_fs::read(&path).await {
Ok(content) => {
let extension = path
.extension()
.and_then(|s| s.to_str())
.unwrap_or_default();
let content_type = mime_types.get(extension).unwrap();
Ok(Response::builder()
.header("Content-Type", content_type)
.body(Body::from(content))
.unwrap())
}
_ => Ok(Response::builder()
.status(StatusCode::NOT_FOUND)
.body(Body::from("404 Not Found"))
.unwrap()),
}
}
fn not_found_response(req: &Req, _params: &Params, custom_closure: &Option<Arc<dyn Fn(&str) -> String + Send + Sync>>) -> Response<Body> {
let accept_header = req.headers()
.get("Accept")
.and_then(|h| h.to_str().ok())
.unwrap_or("");
let media_types: Vec<&str> = accept_header
.split(',')
.map(|s| s.trim()) .collect();
let content_type = if media_types.iter().any(|mt| *mt == "application/json" || *mt == "application/*" || *mt == "*/*") {
"application/json"
} else if media_types.iter().any(|mt| *mt == "text/html" || *mt == "text/*" || *mt == "*/*") {
"text/html"
} else {
"text/plain"
};
if let Some(closure) = custom_closure {
let content = closure(content_type);
Response::builder()
.status(StatusCode::NOT_FOUND)
.header("Content-Type", content_type)
.body(Body::from(content))
.unwrap()
} else {
let default_content = match content_type {
"application/json" => r#"{"error": {"code": 404, "message": "Not Found"}}"#,
"text/html" => "<html><body><h1>404 - Not Found</h1></body></html>",
_ => "404 Resource Not Found",
};
Response::builder()
.status(StatusCode::NOT_FOUND)
.header("Content-Type", content_type)
.body(Body::from(default_content))
.unwrap()
}
}
async fn handle_request(
req: Req,
params: Params,
routes: Vec<Route>,
mime_types: HashMap<String, String>, custom_closure: Option<Arc<dyn Fn(&str) -> String + Send + Sync>>,
) -> Result<Response<Body>, Infallible> {
for route in routes.iter() {
if route.method == *req.method() {
if let Some(captured_params) = route.is_match(&req) {
let mut new_params = params.clone();
new_params.extra.extend(captured_params);
if new_params.extra.values().any(|value| value.is_empty()) {
return Ok(Response::builder()
.status(StatusCode::NOT_FOUND)
.body(Body::from("Resource not found"))
.unwrap());
}
let future = (route.handler)(req, new_params);
return future.await;
}
}
}
let static_file_response = serve_static_file(&req, ¶ms, &mime_types).await?;
if static_file_response.status() == StatusCode::OK {
Ok(static_file_response)
} else {
Ok(not_found_response(&req, ¶ms, &custom_closure))
}
}
pub fn extract_query(req: &Req) -> HashMap<String, String> {
let mut query_params = HashMap::new();
if let Some(query) = req.uri().query() {
for (key, value) in url::form_urlencoded::parse(query.as_bytes()) {
query_params.insert(key.to_string(), value.to_string());
}
}
query_params
}
pub fn get_param(query_params: &HashMap<String, String>, key: &str) -> String {
query_params.get(key).cloned().unwrap_or_default()
}
pub fn boxed<F>(future: F) -> Reply
where
F: Future<Output = Result<Response<Body>, Infallible>> + Send + 'static,
{
Box::pin(future)
}
pub fn env_var(var_name: &str, default: &str) -> String {
env::var(var_name).unwrap_or_else(|_| default.to_string())
}
pub fn load_dotenv() {
dotenv().ok(); }
pub fn server_log(action: &str, text: &str) {
match action {
"show" => {
println!("{}", text);
}
"hide" => {
}
_ => {
}
}
}
use crate::client::HTTP_CLIENT;
pub fn serve_http_client(_req: Req, _params: Params) -> Reply {
boxed(async {
Ok(Response::builder()
.header("Content-Type", "text/html; charset=UTF-8")
.body(Body::from(HTTP_CLIENT))
.unwrap())
})
}