Changes to config format, move all global context into dedicated Context struct

main
Pantonshire 3 years ago
parent 054f21f664
commit 273ec529c0

@ -1,19 +1,21 @@
bind = "127.0.0.1:8080" bind = "127.0.0.1:8080"
concurrency_limit = 128 concurrency_limit = 128
fs_event_delay_millis = 2000
namespace_uuid = "00000000-0000-0000-0000-000000000000"
[site]
protocol = "http"
domain = "localhost:8080"
[content]
static_dir = "./content/static/" static_dir = "./content/static/"
favicon_dir = "./content/favicon/" favicon_dir = "./content/favicon/"
robots_path = "./content/robots.txt" robots_path = "./content/robots.txt"
posts_dir = "./articles/src/" posts_dir = "./articles/src/"
post_media_dir = "./articles/media/" post_media_dir = "./articles/media/"
fs_event_delay_millis = 2000 [github]
source_url = "https://github.com/pantonshire/blog_content/blob/main/src"
namespace_uuid = "00000000-0000-0000-0000-000000000000"
[self_ref]
protocol = "http"
domain = "localhost:8080"
[rss] [rss]
num_posts = 20 num_posts = 20
@ -23,3 +25,8 @@ ttl = 360
[atom] [atom]
num_posts = 20 num_posts = 20
title = "Pantonshire" title = "Pantonshire"
[[contact]]
name = "Twitter"
user = "@pantonshire"
url = "https://twitter.com/pantonshire"

@ -7,25 +7,36 @@ use serde::{Deserialize, Deserializer};
pub(crate) struct Config { pub(crate) struct Config {
pub bind: SocketAddr, pub bind: SocketAddr,
pub concurrency_limit: usize, pub concurrency_limit: usize,
pub static_dir: PathBuf,
pub favicon_dir: PathBuf,
pub robots_path: PathBuf,
pub posts_dir: PathBuf,
pub post_media_dir: PathBuf,
#[serde(rename = "fs_event_delay_millis", deserialize_with = "deserialize_millis")] #[serde(rename = "fs_event_delay_millis", deserialize_with = "deserialize_millis")]
pub fs_event_delay: Duration, pub fs_event_delay: Duration,
pub namespace_uuid: Uuid, pub namespace_uuid: Uuid,
pub self_ref: SelfRefConfig, pub content: ContentConfig,
pub github: GithubConfig,
pub site: SiteConfig,
pub rss: RssConfig, pub rss: RssConfig,
pub atom: AtomConfig, pub atom: AtomConfig,
} }
#[derive(Deserialize, Clone, Debug)] #[derive(Deserialize, Clone, Debug)]
pub(crate) struct SelfRefConfig { pub(crate) struct SiteConfig {
pub protocol: String, pub protocol: String,
pub domain: String, pub domain: String,
} }
#[derive(Deserialize, Clone, Debug)]
pub(crate) struct ContentConfig {
pub static_dir: PathBuf,
pub favicon_dir: PathBuf,
pub robots_path: PathBuf,
pub posts_dir: PathBuf,
pub post_media_dir: PathBuf,
}
#[derive(Deserialize, Clone, Debug)]
pub(crate) struct GithubConfig {
pub source_url: Option<String>,
}
#[derive(Deserialize, Clone, Debug)] #[derive(Deserialize, Clone, Debug)]
pub(crate) struct RssConfig { pub(crate) struct RssConfig {
pub num_posts: usize, pub num_posts: usize,

@ -0,0 +1,31 @@
use blog::db::ConcurrentPostsStore;
use crate::Config;
pub(crate) struct Context {
config: Config,
posts: ConcurrentPostsStore,
}
impl Context {
#[inline]
#[must_use]
pub(crate) fn new(config: Config, posts: ConcurrentPostsStore) -> Self {
Self {
config,
posts,
}
}
#[inline]
#[must_use]
pub(crate) fn config(&self) -> &Config {
&self.config
}
#[inline]
#[must_use]
pub(crate) fn posts(&self) -> &ConcurrentPostsStore {
&self.posts
}
}

@ -1,4 +1,5 @@
mod config; mod config;
mod context;
mod fs_watcher; mod fs_watcher;
mod render; mod render;
mod service; mod service;
@ -27,6 +28,7 @@ use blog::{
}; };
use config::Config; use config::Config;
use context::Context;
use render::Renderer; use render::Renderer;
fn main() { fn main() {
@ -42,7 +44,7 @@ fn run() -> Result<(), Error> {
// Load the configuration from the TOML config file specified by the first command-line // Load the configuration from the TOML config file specified by the first command-line
// argument. // argument.
let config = Arc::new({ let config = {
let config_path = env::args().nth(1) let config_path = env::args().nth(1)
.ok_or(Error::NoConfig)?; .ok_or(Error::NoConfig)?;
@ -53,27 +55,25 @@ fn run() -> Result<(), Error> {
contents.parse::<Config>() contents.parse::<Config>()
.map_err(Error::BadConfig)? .map_err(Error::BadConfig)?
}); };
// Create the data structure used to store the rendered posts. This uses an `Arc` internally, // Create the global context that will be used and modified throughout the program.
// so clones will point to the same underlying data. let context = Arc::new(Context::new(config, ConcurrentPostsStore::new()));
let posts_store = ConcurrentPostsStore::new();
let code_renderer = CodeBlockRenderer::new(); let code_renderer = CodeBlockRenderer::new();
// Create the post renderer and the mpsc channel that will be used to communicate with it. // Create the post renderer and the mpsc channel that will be used to communicate with it.
let (renderer, tx) = Renderer::new( let (renderer, tx) = Renderer::new(
config.clone(), context.clone(),
posts_store.clone(),
code_renderer, code_renderer,
config.posts_dir.clone() context.config().content.posts_dir.clone()
); );
// Dropping the watcher stops its thread, so keep it alive until the server has stopped. // Dropping the watcher stops its thread, so keep it alive until the server has stopped.
let watcher = fs_watcher::start_watching( let watcher = fs_watcher::start_watching(
tx, tx,
&config.posts_dir, &context.config().content.posts_dir,
config.fs_event_delay context.config().fs_event_delay
)?; )?;
let renderer_handle = thread::spawn(move || { let renderer_handle = thread::spawn(move || {
@ -88,7 +88,7 @@ fn run() -> Result<(), Error> {
.enable_all() .enable_all()
.build() .build()
.map_err(Error::TokioRuntime)? .map_err(Error::TokioRuntime)?
.block_on(run_server(config, posts_store))?; .block_on(run_server(context))?;
info!("Stopped server"); info!("Stopped server");
@ -105,17 +105,13 @@ fn run() -> Result<(), Error> {
Ok(()) Ok(())
} }
async fn run_server( async fn run_server(context: Arc<Context>) -> Result<(), Error> {
config: Arc<Config>, let service = service::site_service(context.clone());
posts_store: ConcurrentPostsStore,
) -> Result<(), Error>
{
let service = service::site_service(config.clone(), posts_store);
info!(address = %config.bind, "Starting server"); info!(address = %context.config().bind, "Starting server");
Server::try_bind(&config.bind) Server::try_bind(&context.config().bind)
.map_err(|err| Error::Bind(config.bind, err))? .map_err(|err| Error::Bind(context.config().bind, err))?
.serve(service.into_make_service()) .serve(service.into_make_service())
.with_graceful_shutdown(handle_interrupt()) .with_graceful_shutdown(handle_interrupt())
.await .await

@ -13,14 +13,12 @@ use tracing::{info, warn, error};
use blog::{ use blog::{
codeblock::CodeBlockRenderer, codeblock::CodeBlockRenderer,
post::{Error as ParseError, Post, Id}, post::{Error as ParseError, Post, Id},
db::ConcurrentPostsStore,
}; };
use crate::Config; use crate::Context;
pub(crate) struct Renderer { pub(crate) struct Renderer {
config: Arc<Config>, context: Arc<Context>,
posts: ConcurrentPostsStore,
code_renderer: CodeBlockRenderer, code_renderer: CodeBlockRenderer,
posts_dir_path: PathBuf, posts_dir_path: PathBuf,
rx: mpsc::Receiver<DebouncedEvent>, rx: mpsc::Receiver<DebouncedEvent>,
@ -28,8 +26,7 @@ pub(crate) struct Renderer {
impl Renderer { impl Renderer {
pub(crate) fn new( pub(crate) fn new(
config: Arc<Config>, context: Arc<Context>,
posts: ConcurrentPostsStore,
code_renderer: CodeBlockRenderer, code_renderer: CodeBlockRenderer,
posts_dir_path: PathBuf, posts_dir_path: PathBuf,
) -> (Self, mpsc::Sender<DebouncedEvent>) ) -> (Self, mpsc::Sender<DebouncedEvent>)
@ -42,8 +39,7 @@ impl Renderer {
tx.send(DebouncedEvent::Rescan).unwrap(); tx.send(DebouncedEvent::Rescan).unwrap();
(Self { (Self {
config, context,
posts,
code_renderer, code_renderer,
posts_dir_path, posts_dir_path,
rx, rx,
@ -106,7 +102,7 @@ impl Renderer {
fn update(&self, target: &EventTarget) { fn update(&self, target: &EventTarget) {
match self.parse_post_from_target(target) { match self.parse_post_from_target(target) {
Ok(post) => { Ok(post) => {
let mut guard = self.posts.write_blocking(); let mut guard = self.context.posts().write_blocking();
guard.insert(post); guard.insert(post);
}, },
Err(err) => { Err(err) => {
@ -118,7 +114,7 @@ impl Renderer {
#[tracing::instrument(skip(self))] #[tracing::instrument(skip(self))]
fn rename(&self, old_target: &EventTarget, new_target: &EventTarget) { fn rename(&self, old_target: &EventTarget, new_target: &EventTarget) {
let post_res = self.parse_post_from_target(new_target); let post_res = self.parse_post_from_target(new_target);
let mut guard = self.posts.write_blocking(); let mut guard = self.context.posts().write_blocking();
guard.remove(&old_target.id); guard.remove(&old_target.id);
match post_res { match post_res {
Ok(post) => { Ok(post) => {
@ -132,7 +128,7 @@ impl Renderer {
#[tracing::instrument(skip(self))] #[tracing::instrument(skip(self))]
fn remove(&self, target: &EventTarget) { fn remove(&self, target: &EventTarget) {
let mut guard = self.posts.write_blocking(); let mut guard = self.context.posts().write_blocking();
guard.remove(&target.id); guard.remove(&target.id);
} }
@ -168,7 +164,7 @@ impl Renderer {
} }
} }
let mut guard = self.posts.write_blocking(); let mut guard = self.context.posts().write_blocking();
guard.clear(); guard.clear();
for post in posts { for post in posts {
guard.insert(post); guard.insert(post);
@ -204,7 +200,7 @@ impl Renderer {
Post::new_from_str( Post::new_from_str(
&self.code_renderer, &self.code_renderer,
self.config.namespace_uuid, self.context.config().namespace_uuid,
target.id.clone(), target.id.clone(),
updated, updated,
&contents &contents

@ -3,22 +3,19 @@ use std::sync::Arc;
use atom_syndication as atom; use atom_syndication as atom;
use axum::{body::Bytes, extract::Extension}; use axum::{body::Bytes, extract::Extension};
use blog::{db::ConcurrentPostsStore, time::unix_epoch}; use blog::time::unix_epoch;
use crate::Config; use crate::Context;
use super::response::Atom; use super::response::Atom;
pub(super) async fn handle( pub(super) async fn handle(Extension(context): Extension<Arc<Context>>) -> Atom<Bytes> {
Extension(config): Extension<Arc<Config>>,
Extension(posts): Extension<ConcurrentPostsStore>,
) -> Atom<Bytes> {
let (atom_entries, updated) = { let (atom_entries, updated) = {
let guard = posts.read().await; let guard = context.posts().read().await;
let atom_entries = guard let atom_entries = guard
.iter_by_published() .iter_by_published()
.take(config.atom.num_posts) .take(context.config().atom.num_posts)
.map(|post| { .map(|post| {
atom::EntryBuilder::default() atom::EntryBuilder::default()
.id(format!("urn:uuid:{}", post.uuid())) .id(format!("urn:uuid:{}", post.uuid()))
@ -27,8 +24,8 @@ pub(super) async fn handle(
.links(vec![atom::LinkBuilder::default() .links(vec![atom::LinkBuilder::default()
.href(format!( .href(format!(
"{}://{}/articles/{}", "{}://{}/articles/{}",
config.self_ref.protocol, context.config().site.protocol,
config.self_ref.domain, context.config().site.domain,
post.id() post.id()
)) ))
.rel("alternate".to_owned()) .rel("alternate".to_owned())
@ -50,21 +47,21 @@ pub(super) async fn handle(
Atom( Atom(
atom::FeedBuilder::default() atom::FeedBuilder::default()
.id(format!("urn:uuid:{}", config.namespace_uuid)) .id(format!("urn:uuid:{}", context.config().namespace_uuid))
.title(config.atom.title.clone()) .title(context.config().atom.title.clone())
.updated(updated) .updated(updated)
.links(vec![ .links(vec![
atom::LinkBuilder::default() atom::LinkBuilder::default()
.href(format!( .href(format!(
"{}://{}/atom.xml", "{}://{}/atom.xml",
config.self_ref.protocol, config.self_ref.domain context.config().site.protocol, context.config().site.domain
)) ))
.rel("self".to_owned()) .rel("self".to_owned())
.build(), .build(),
atom::LinkBuilder::default() atom::LinkBuilder::default()
.href(format!( .href(format!(
"{}://{}/articles/", "{}://{}/articles/",
config.self_ref.protocol, config.self_ref.domain context.config().site.protocol, context.config().site.domain
)) ))
.rel("alternate".to_owned()) .rel("alternate".to_owned())
.mime_type(Some("text/html".to_owned())) .mime_type(Some("text/html".to_owned()))

@ -1,13 +1,13 @@
use std::sync::Arc;
use axum::extract::Extension; use axum::extract::Extension;
use maud::html; use maud::html;
use blog::db::ConcurrentPostsStore; use crate::{Context, template};
use crate::template;
use super::response::Html; use super::response::Html;
pub(super) async fn handle(Extension(posts): Extension<ConcurrentPostsStore>) -> Html { pub(super) async fn handle(Extension(context): Extension<Arc<Context>>) -> Html {
Html::new() Html::new()
.with_title_static("Pantonshire") .with_title_static("Pantonshire")
.with_crawler_permissive() .with_crawler_permissive()
@ -49,7 +49,7 @@ pub(super) async fn handle(Extension(posts): Extension<ConcurrentPostsStore>) ->
h2 { "Articles" } h2 { "Articles" }
p { "Some recent ones:" } p { "Some recent ones:" }
ul .articles_list { ul .articles_list {
@for post in posts.read().await.iter_by_published().rev().take(3) { @for post in context.posts().read().await.iter_by_published().rev().take(3) {
li { li {
h3 { a href={"/articles/" (post.id())} { (post.title()) } } h3 { a href={"/articles/" (post.id())} { (post.title()) } }
@if let Some(subtitle) = post.subtitle() { @if let Some(subtitle) = post.subtitle() {

@ -1,18 +1,18 @@
use std::sync::Arc;
use axum::extract::{Extension, Path}; use axum::extract::{Extension, Path};
use maud::html; use maud::html;
use blog::db::ConcurrentPostsStore; use crate::{Context, template};
use crate::template;
use super::response::{Error, Html}; use super::response::{Error, Html};
pub(super) async fn handle( pub(super) async fn handle(
Path(post_id): Path<String>, Path(post_id): Path<String>,
Extension(posts): Extension<ConcurrentPostsStore> Extension(context): Extension<Arc<Context>>,
) -> Result<Html, Error> ) -> Result<Html, Error>
{ {
let post = posts.get(&post_id) let post = context.posts().get(&post_id)
.await .await
.ok_or(Error::PostNotFound)?; .ok_or(Error::PostNotFound)?;

@ -1,13 +1,13 @@
use std::sync::Arc;
use axum::extract::Extension; use axum::extract::Extension;
use maud::html; use maud::html;
use blog::db::ConcurrentPostsStore; use crate::{Context, template};
use crate::template;
use super::response::Html; use super::response::Html;
pub(super) async fn handle(Extension(posts): Extension<ConcurrentPostsStore>) -> Html { pub(super) async fn handle(Extension(context): Extension<Arc<Context>>) -> Html {
Html::new() Html::new()
.with_title_static("Articles") .with_title_static("Articles")
.with_crawler_permissive() .with_crawler_permissive()
@ -23,7 +23,7 @@ pub(super) async fn handle(Extension(posts): Extension<ConcurrentPostsStore>) ->
"A collection of words I have written, against my better judgement." "A collection of words I have written, against my better judgement."
} }
ul .articles_list { ul .articles_list {
@for post in posts.read().await.iter_by_published().rev() { @for post in context.posts().read().await.iter_by_published().rev() {
li { li {
h3 { a href={"/articles/" (post.id())} { (post.title()) } } h3 { a href={"/articles/" (post.id())} { (post.title()) } }
@if let Some(subtitle) = post.subtitle() { @if let Some(subtitle) = post.subtitle() {

@ -5,24 +5,18 @@ use axum::{
extract::Extension, extract::Extension,
}; };
use blog::{ use blog::time::unix_epoch;
db::ConcurrentPostsStore,
time::unix_epoch,
};
use crate::Config; use crate::Context;
use super::response::Rss; use super::response::Rss;
pub(super) async fn handle( pub(super) async fn handle(Extension(context): Extension<Arc<Context>>) -> Rss<Bytes> {
Extension(config): Extension<Arc<Config>>,
Extension(posts): Extension<ConcurrentPostsStore>,
) -> Rss<Bytes> {
let (rss_items, updated) = { let (rss_items, updated) = {
let guard = posts.read().await; let guard = context.posts().read().await;
let rss_items = guard.iter_by_published() let rss_items = guard.iter_by_published()
.take(config.rss.num_posts) .take(context.config().rss.num_posts)
.map(|post| { .map(|post| {
rss::ItemBuilder::default() rss::ItemBuilder::default()
.title(Some(post.title().to_owned())) .title(Some(post.title().to_owned()))
@ -32,8 +26,8 @@ pub(super) async fn handle(
.build())) .build()))
.link(Some(format!( .link(Some(format!(
"{}://{}/articles/{}", "{}://{}/articles/{}",
config.self_ref.protocol, context.config().site.protocol,
config.self_ref.domain, context.config().site.domain,
post.id() post.id()
))) )))
.pub_date(Some(post.published().to_rfc2822())) .pub_date(Some(post.published().to_rfc2822()))
@ -48,12 +42,12 @@ pub(super) async fn handle(
}; };
Rss(rss::ChannelBuilder::default() Rss(rss::ChannelBuilder::default()
.title(config.rss.title.clone()) .title(context.config().rss.title.clone())
.link(format!( .link(format!(
"{}://{}", "{}://{}",
config.self_ref.protocol, config.self_ref.domain context.config().site.protocol, context.config().site.domain
)) ))
.ttl(Some(config.rss.ttl.to_string())) .ttl(Some(context.config().rss.ttl.to_string()))
.last_build_date(Some(updated.to_rfc2822())) .last_build_date(Some(updated.to_rfc2822()))
.items(rss_items) .items(rss_items)
.build() .build()

@ -11,9 +11,7 @@ use tower::limit::ConcurrencyLimitLayer;
use tower_http::trace::TraceLayer; use tower_http::trace::TraceLayer;
use tracing::info; use tracing::info;
use blog::db::ConcurrentPostsStore; use crate::Context;
use crate::Config;
use super::{ use super::{
atom, atom,
@ -26,11 +24,7 @@ use super::{
static_content, static_content,
}; };
pub(crate) fn service( pub(crate) fn service(context: Arc<Context>) -> Router {
config: Arc<Config>,
posts_store: ConcurrentPostsStore,
) -> Router
{
Router::new() Router::new()
.route("/", get(index::handle)) .route("/", get(index::handle))
.route("/contact", get(contact::handle)) .route("/contact", get(contact::handle))
@ -38,21 +32,20 @@ pub(crate) fn service(
.route("/rss.xml", get(rss::handle)) .route("/rss.xml", get(rss::handle))
.route("/atom.xml", get(atom::handle)) .route("/atom.xml", get(atom::handle))
.route("/articles/:post_id", get(post::handle)) .route("/articles/:post_id", get(post::handle))
.route("/robots.txt", static_content::file_service(&config.robots_path, None)) .route("/robots.txt", static_content::file_service(&context.config().content.robots_path, None))
.route("/favicon.ico", static_content::file_service(&config.favicon_dir.join("favicon.ico"), None)) .route("/favicon.ico", static_content::file_service(&context.config().content.favicon_dir.join("favicon.ico"), None))
.route("/favicon-16x16.png", static_content::file_service(&config.favicon_dir.join("favicon-16x16.png"), None)) .route("/favicon-16x16.png", static_content::file_service(&context.config().content.favicon_dir.join("favicon-16x16.png"), None))
.route("/favicon-32x32.png", static_content::file_service(&config.favicon_dir.join("favicon-32x32.png"), None)) .route("/favicon-32x32.png", static_content::file_service(&context.config().content.favicon_dir.join("favicon-32x32.png"), None))
.route("/apple-touch-icon.png", static_content::file_service(&config.favicon_dir.join("apple-touch-icon.png"), None)) .route("/apple-touch-icon.png", static_content::file_service(&context.config().content.favicon_dir.join("apple-touch-icon.png"), None))
.route("/android-chrome-192x192.png", static_content::file_service(&config.favicon_dir.join("android-chrome-192x192.png"), None)) .route("/android-chrome-192x192.png", static_content::file_service(&context.config().content.favicon_dir.join("android-chrome-192x192.png"), None))
.route("/android-chrome-512x512.png", static_content::file_service(&config.favicon_dir.join("android-chrome-512x512.png"), None)) .route("/android-chrome-512x512.png", static_content::file_service(&context.config().content.favicon_dir.join("android-chrome-512x512.png"), None))
.route("/site.webmanifest", static_content::file_service(&config.favicon_dir.join("site.webmanifest"), None)) .route("/site.webmanifest", static_content::file_service(&context.config().content.favicon_dir.join("site.webmanifest"), None))
.nest("/static", static_content::dir_service(&config.static_dir)) .nest("/static", static_content::dir_service(&context.config().content.static_dir))
.nest("/article_media", static_content::dir_service(&config.post_media_dir)) .nest("/article_media", static_content::dir_service(&context.config().content.post_media_dir))
.fallback(handle_fallback.into_service()) .fallback(handle_fallback.into_service())
.layer(ConcurrencyLimitLayer::new(config.concurrency_limit)) .layer(ConcurrencyLimitLayer::new(context.config().concurrency_limit))
.layer(TraceLayer::new_for_http()) .layer(TraceLayer::new_for_http())
.layer(Extension(config)) .layer(Extension(context))
.layer(Extension(posts_store))
} }
async fn handle_fallback(uri: Uri) -> Error { async fn handle_fallback(uri: Uri) -> Error {

@ -10,14 +10,13 @@ use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
use crate::post::{Post, Id}; use crate::post::{Post, Id};
#[derive(Clone)]
pub struct ConcurrentPostsStore { pub struct ConcurrentPostsStore {
inner: Arc<RwLock<PostsStore>>, inner: RwLock<PostsStore>,
} }
impl ConcurrentPostsStore { impl ConcurrentPostsStore {
pub fn new() -> Self { pub fn new() -> Self {
Self { inner: Arc::new(RwLock::new(PostsStore::new())) } Self { inner: RwLock::new(PostsStore::new()) }
} }
pub async fn read(&self) -> RwLockReadGuard<'_, PostsStore> { pub async fn read(&self) -> RwLockReadGuard<'_, PostsStore> {

Loading…
Cancel
Save