mdblog 0.22.0

static site generator from markdown files.
Documentation
use std::path::PathBuf;

use axum::{
    extract::{Path, State},
    http::StatusCode,
    response::{IntoResponse, Redirect, Response},
    routing::get,
    Router,
};
use tower_http::trace::{self, TraceLayer};
use tracing::{debug, Level};

#[derive(Clone)]
struct StaticDir(PathBuf);

pub struct HttpServer {
    host: String,
    port: u16,
    root_dir: PathBuf,
}

impl HttpServer {
    pub fn new(host: String, port: u16, root_dir: PathBuf) -> Self {
        HttpServer { host, port, root_dir }
    }

    pub fn run(&self) {
        let host = self.host.clone();
        let port = self.port;
        let root_dir = self.root_dir.clone();

        let (server_tx, server_rx) = std::sync::mpsc::channel();
        std::thread::spawn(move || {
            let app = Router::new()
                .route("/", get(Redirect::permanent("/index.html")))
                .route("/*path", get(Self::handle_path))
                .layer(
                    TraceLayer::new_for_http()
                        .make_span_with(trace::DefaultMakeSpan::new().level(Level::INFO))
                        .on_response(trace::DefaultOnResponse::new().level(Level::INFO)),
                )
                .with_state(StaticDir(root_dir));
            let rt = tokio::runtime::Builder::new_current_thread()
                .enable_all()
                .build()
                .unwrap();
            rt.block_on(async move {
                let listener = tokio::net::TcpListener::bind((host, port)).await.unwrap();
                axum::serve(listener, app).await.unwrap();
                server_tx.send(()).unwrap();
            });
        });
        _ = server_rx.recv().unwrap();
    }

    async fn handle_path(Path(path): Path<String>, State(static_dir): State<StaticDir>) -> Response {
        debug!("...{}", path);
        let mut path = static_dir.0.join(path);
        match tokio::fs::metadata(&path).await {
            Err(err) => {
                if err.kind() == std::io::ErrorKind::NotFound {
                    return (StatusCode::NOT_FOUND, "not found").into_response();
                } else {
                    return (StatusCode::INTERNAL_SERVER_ERROR, "unhandled type").into_response();
                }
            }
            Ok(metadata) => {
                if metadata.is_dir() {
                    path.push("index.html");
                }
                if metadata.is_file() || metadata.is_symlink() {
                    let guess = mime_guess::from_path(&path).first();
                    let mime_type = guess.unwrap_or(mime_guess::mime::APPLICATION_OCTET_STREAM);
                    let bytes = tokio::fs::read(&path).await.unwrap();
                    return Response::builder()
                        .header(axum::http::header::CONTENT_TYPE, mime_type.to_string())
                        .status(StatusCode::OK)
                        .body(axum::body::Body::from(bytes))
                        .unwrap();
                } else {
                    return (StatusCode::INTERNAL_SERVER_ERROR, "unhandled type").into_response();
                }
            }
        }
    }
}