You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

120 lines
3.4 KiB
Rust

mod codeblock;
mod fs_watcher;
mod post;
mod posts_store;
mod render;
mod service;
mod template;
use std::{env, fs, path::PathBuf, thread};
use axum::Server;
use miette::{IntoDiagnostic, Context};
use tracing::info;
use codeblock::CodeBlockRenderer;
use posts_store::ConcurrentPostsStore;
use render::Renderer;
#[derive(knuffel::Decode, Clone, Debug)]
pub struct Config {
#[knuffel(child, unwrap(argument))]
bind: String,
#[knuffel(child, unwrap(argument))]
concurrency_limit: usize,
#[knuffel(child, unwrap(argument))]
posts_dir: PathBuf,
#[knuffel(child, unwrap(argument))]
static_dir: PathBuf,
#[knuffel(child)]
rss: RssConfig,
}
#[derive(knuffel::Decode, Clone, Debug)]
pub struct RssConfig {
#[knuffel(child, unwrap(argument))]
num_posts: usize,
#[knuffel(child, unwrap(argument))]
title: String,
#[knuffel(child, unwrap(argument))]
ttl: u32,
#[knuffel(child, unwrap(argument))]
protocol: String,
#[knuffel(child, unwrap(argument))]
domain: String,
}
fn main() -> miette::Result<()> {
tracing_subscriber::fmt::init();
// Load the configuration from the KDL config file specified by the first command-line
// argument.
let config = {
let config_path = env::args().nth(1)
.ok_or_else(|| miette::Error::msg("No config file specified"))?;
info!(path = %config_path, "Loading config");
let contents = fs::read_to_string(&config_path)
.into_diagnostic()
.wrap_err_with(|| format!("Failed to read config file {}", config_path))?;
knuffel::parse::<Config>(&config_path, &contents)
.wrap_err_with(|| format!("Failed to parse config file {}", config_path))?
};
// Create the data structure used to store the rendered posts. This uses an `Arc` internally,
// so clones will point to the same underlying data.
let posts_store = ConcurrentPostsStore::new();
let code_renderer = CodeBlockRenderer::new();
// Create the post renderer and the mpsc channel that will be used to communicate with it.
let (renderer, tx) = Renderer::new(
posts_store.clone(),
code_renderer,
config.posts_dir.clone()
);
// Dropping the watcher stops its thread, so keep it alive until `main` returns.
let _watcher = fs_watcher::start_watching(tx, &config.posts_dir)?;
thread::spawn(move || {
renderer.handle_events();
});
info!("Started renderer thread");
// To run the web server, we need to be in an async context, so create a new Tokio runtime and
// pass control to it.
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.into_diagnostic()
.wrap_err("Failed to create async runtime")?
.block_on(run(config, posts_store))
}
async fn run(
config: Config,
posts_store: ConcurrentPostsStore,
) -> miette::Result<()>
{
let bind_address = &config.bind
.parse()
.into_diagnostic()
.wrap_err_with(|| format!("Failed to parse socket address \"{}\"", config.bind))?;
let service = service::site_service(config, posts_store);
info!(address = %bind_address, "Starting server");
Server::try_bind(bind_address)
.into_diagnostic()
.wrap_err_with(|| format!("Failed to bind {}", bind_address))?
.serve(service.into_make_service())
.await
.into_diagnostic()
.wrap_err("Fatal error while running the server")
}