server 0.0.0

Local directory server
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()
}