use std::{
convert::Infallible,
fs::{self, File},
path::{Path, PathBuf},
str::FromStr,
};
use clap::Parser;
use touche::{Body, Request, Response, Server, StatusCode};
#[cfg(feature = "mime")]
mod mime;
#[cfg(feature = "mime")]
static MIME: std::sync::LazyLock<mime::Mime> = std::sync::LazyLock::new(mime::Mime::new);
#[derive(Parser)]
#[command(version, about, long_about = None)]
struct Cli {
#[arg(short, long, default_value_t = 5000)]
port: u16,
#[arg(long, default_value_t = String::from("0.0.0.0"))]
bind: String,
path: Option<PathBuf>,
}
fn main() -> std::io::Result<()> {
let Cli {
port,
bind,
path: base_path,
} = Cli::parse();
let base_path = base_path.unwrap_or_else(|| PathBuf::from_str("./").unwrap());
Server::bind((bind, port)).serve(move |req: Request<Body>| {
let path = base_path.join(req.uri().path().replacen('/', "", 1));
match fs::metadata(&path) {
Ok(meta) if meta.is_file() => Ok(serve_file(path)),
Ok(meta) if meta.is_dir() => {
let index = fs::read_dir(path).ok().and_then(|dir| {
dir.filter_map(|entry| entry.ok())
.filter(|entry| entry.path().file_stem().unwrap_or_default() == "index")
.find(|entry| {
entry
.path()
.extension()
.unwrap_or_default()
.to_str()
.unwrap_or_default()
.starts_with("htm")
})
});
match index {
Some(index) => Ok(serve_file(index.path())),
None => Ok(not_found()),
}
}
_ => Ok::<_, Infallible>(not_found()),
}
})
}
fn serve_file(path: impl AsRef<Path>) -> Response<Body> {
match File::open(&path) {
Ok(file) => {
#[allow(unused_mut)]
let mut res = Response::builder().status(StatusCode::OK);
#[cfg(feature = "mime")]
if let Some(content_type) = path
.as_ref()
.extension()
.and_then(|ext| ext.to_str())
.and_then(|ext| MIME.for_extension(ext))
{
res = res.header("content-type", content_type);
}
res.body(Body::try_from(file).unwrap()).unwrap()
}
Err(_) => not_found(),
}
}
fn not_found() -> Response<Body> {
Response::builder()
.status(StatusCode::NOT_FOUND)
.body("File not found".into())
.unwrap()
}