refactor part 2: create PostManager trait

This commit is contained in:
slonkazoid 2024-05-14 10:11:41 +03:00
parent a7b5472fc6
commit cc41ba9421
Signed by: slonk
SSH key fingerprint: SHA256:tbZfJX4IOvZ0LGWOWu5Ijo8jfMPi78TU7x1VoEeCIjM
4 changed files with 113 additions and 81 deletions

View file

@ -17,12 +17,12 @@ use tracing::{info, info_span, Span};
use crate::config::Config; use crate::config::Config;
use crate::error::{AppError, AppResult}; use crate::error::{AppError, AppResult};
use crate::filters; use crate::filters;
use crate::post::{PostManager, PostMetadata, RenderStats}; use crate::post::{MarkdownPosts, PostManager, PostMetadata, RenderStats};
#[derive(Clone)] #[derive(Clone)]
pub struct AppState { pub struct AppState {
pub config: Arc<Config>, pub config: Arc<Config>,
pub posts: Arc<PostManager<Arc<Config>>>, pub posts: Arc<MarkdownPosts<Arc<Config>>>,
} }
#[derive(Template)] #[derive(Template)]
@ -84,7 +84,7 @@ async fn rss(
} }
let posts = posts let posts = posts
.get_all_posts_filtered(|metadata, _| { .get_all_posts(|metadata, _| {
!query !query
.tag .tag
.as_ref() .as_ref()
@ -161,7 +161,7 @@ async fn post(
} }
} }
pub fn new() -> Router<AppState> { pub fn new(config: &Config) -> Router<AppState> {
Router::new() Router::new()
.route("/", get(index)) .route("/", get(index))
.route( .route(
@ -173,8 +173,11 @@ pub fn new() -> Router<AppState> {
.route("/posts/:name", get(post)) .route("/posts/:name", get(post))
.route("/posts", get(all_posts)) .route("/posts", get(all_posts))
.route("/feed.xml", get(rss)) .route("/feed.xml", get(rss))
.nest_service("/static", ServeDir::new("static").precompressed_gzip()) .nest_service(
.nest_service("/media", ServeDir::new("media")) "/static",
ServeDir::new(&config.dirs._static).precompressed_gzip(),
)
.nest_service("/media", ServeDir::new(&config.dirs.media))
.layer( .layer(
TraceLayer::new_for_http() TraceLayer::new_for_http()
.make_span_with(|request: &Request<_>| { .make_span_with(|request: &Request<_>| {

View file

@ -5,7 +5,7 @@ use std::path::PathBuf;
use color_eyre::eyre::{bail, Context, Result}; use color_eyre::eyre::{bail, Context, Result};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tracing::{error, info}; use tracing::{error, info, instrument};
use url::Url; use url::Url;
use crate::ranged_i128_visitor::RangedI128Visitor; use crate::ranged_i128_visitor::RangedI128Visitor;
@ -49,6 +49,8 @@ pub struct HttpConfig {
pub struct DirsConfig { pub struct DirsConfig {
pub posts: PathBuf, pub posts: PathBuf,
pub media: PathBuf, pub media: PathBuf,
#[serde(rename = "static")]
pub _static: PathBuf,
} }
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
@ -101,6 +103,7 @@ impl Default for DirsConfig {
Self { Self {
posts: "posts".into(), posts: "posts".into(),
media: "media".into(), media: "media".into(),
_static: "static".into(),
} }
} }
} }
@ -138,6 +141,7 @@ impl Default for CacheConfig {
} }
} }
#[instrument(name = "config")]
pub async fn load() -> Result<Config> { pub async fn load() -> Result<Config> {
let config_file = env::var(format!( let config_file = env::var(format!(
"{}_CONFIG", "{}_CONFIG",

View file

@ -27,7 +27,7 @@ use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::{util::SubscriberInitExt, EnvFilter}; use tracing_subscriber::{util::SubscriberInitExt, EnvFilter};
use crate::app::AppState; use crate::app::AppState;
use crate::post::PostManager; use crate::post::{MarkdownPosts, PostManager};
#[tokio::main] #[tokio::main]
async fn main() -> eyre::Result<()> { async fn main() -> eyre::Result<()> {
@ -55,7 +55,7 @@ async fn main() -> eyre::Result<()> {
let mut tasks = JoinSet::new(); let mut tasks = JoinSet::new();
let cancellation_token = CancellationToken::new(); let cancellation_token = CancellationToken::new();
let posts = Arc::new(PostManager::new(Arc::clone(&config)).await?); let posts = Arc::new(MarkdownPosts::new(Arc::clone(&config)).await?);
let state = AppState { let state = AppState {
config: Arc::clone(&config), config: Arc::clone(&config),
posts: Arc::clone(&posts), posts: Arc::clone(&posts),
@ -82,7 +82,7 @@ async fn main() -> eyre::Result<()> {
} }
} }
let app = app::new().with_state(state.clone()); let app = app::new(&config).with_state(state.clone());
let listener = TcpListener::bind(socket_addr) let listener = TcpListener::bind(socket_addr)
.await .await

View file

@ -97,7 +97,51 @@ async fn load_cache(config: &Config) -> Result<Cache, eyre::Report> {
bitcode::deserialize(serialized.as_slice()).context("failed to parse cache") bitcode::deserialize(serialized.as_slice()).context("failed to parse cache")
} }
pub struct PostManager<C> pub trait PostManager {
async fn get_all_post_metadata(
&self,
filter: impl Fn(&PostMetadata) -> bool,
) -> Result<Vec<PostMetadata>, PostError> {
self.get_all_posts(|m, _| filter(m))
.await
.map(|vec| vec.into_iter().map(|(meta, ..)| meta).collect())
}
async fn get_all_posts(
&self,
filter: impl Fn(&PostMetadata, &str) -> bool,
) -> Result<Vec<(PostMetadata, String, RenderStats)>, PostError>;
async fn get_max_n_post_metadata_with_optional_tag_sorted(
&self,
n: Option<usize>,
tag: Option<&String>,
) -> Result<Vec<PostMetadata>, PostError> {
let mut posts = self
.get_all_post_metadata(|metadata| !tag.is_some_and(|tag| !metadata.tags.contains(tag)))
.await?;
// we still want some semblance of order if created_at is None so sort by mtime as well
posts.sort_unstable_by_key(|metadata| metadata.modified_at.unwrap_or_default());
posts.sort_by_key(|metadata| metadata.created_at.unwrap_or_default());
posts.reverse();
if let Some(n) = n {
posts.truncate(n);
}
Ok(posts)
}
#[allow(unused)]
async fn get_post_metadata(&self, name: &str) -> Result<PostMetadata, PostError> {
self.get_post(name).await.map(|(meta, ..)| meta)
}
async fn get_post(&self, name: &str) -> Result<(PostMetadata, String, RenderStats), PostError>;
async fn cleanup(&self);
}
pub struct MarkdownPosts<C>
where where
C: Deref<Target = Config>, C: Deref<Target = Config>,
{ {
@ -105,11 +149,11 @@ where
config: C, config: C,
} }
impl<C> PostManager<C> impl<C> MarkdownPosts<C>
where where
C: Deref<Target = Config>, C: Deref<Target = Config>,
{ {
pub async fn new(config: C) -> eyre::Result<PostManager<C>> { pub async fn new(config: C) -> eyre::Result<MarkdownPosts<C>> {
if config.cache.enable { if config.cache.enable {
if config.cache.persistence && tokio::fs::try_exists(&config.cache.file).await? { if config.cache.persistence && tokio::fs::try_exists(&config.cache.file).await? {
info!("loading cache from file"); info!("loading cache from file");
@ -186,7 +230,52 @@ where
Ok((metadata, post, (parsing, rendering))) Ok((metadata, post, (parsing, rendering)))
} }
pub async fn get_all_post_metadata_filtered( fn cache(&self) -> Option<&Cache> {
self.cache.as_ref()
}
fn try_drop(&mut self) -> Result<(), eyre::Report> {
// write cache to file
let config = &self.config.cache;
if config.enable
&& config.persistence
&& let Some(cache) = self.cache()
{
let path = &config.file;
let serialized = bitcode::serialize(cache).context("failed to serialize cache")?;
let mut cache_file = std::fs::File::create(path)
.with_context(|| format!("failed to open cache at {}", path.display()))?;
let compression_level = config.compression_level;
if config.compress {
std::io::Write::write_all(
&mut zstd::stream::write::Encoder::new(cache_file, compression_level)?
.auto_finish(),
&serialized,
)
} else {
cache_file.write_all(&serialized)
}
.context("failed to write cache to file")?;
info!("wrote cache to {}", path.display());
}
Ok(())
}
}
impl<C> Drop for MarkdownPosts<C>
where
C: Deref<Target = Config>,
{
fn drop(&mut self) {
self.try_drop().unwrap()
}
}
impl<C> PostManager for MarkdownPosts<C>
where
C: Deref<Target = Config>,
{
async fn get_all_post_metadata(
&self, &self,
filter: impl Fn(&PostMetadata) -> bool, filter: impl Fn(&PostMetadata) -> bool,
) -> Result<Vec<PostMetadata>, PostError> { ) -> Result<Vec<PostMetadata>, PostError> {
@ -235,7 +324,7 @@ where
Ok(posts) Ok(posts)
} }
pub async fn get_all_posts_filtered( async fn get_all_posts(
&self, &self,
filter: impl Fn(&PostMetadata, &str) -> bool, filter: impl Fn(&PostMetadata, &str) -> bool,
) -> Result<Vec<(PostMetadata, String, RenderStats)>, PostError> { ) -> Result<Vec<(PostMetadata, String, RenderStats)>, PostError> {
@ -264,31 +353,7 @@ where
Ok(posts) Ok(posts)
} }
pub async fn get_max_n_post_metadata_with_optional_tag_sorted( async fn get_post(&self, name: &str) -> Result<(PostMetadata, String, RenderStats), PostError> {
&self,
n: Option<usize>,
tag: Option<&String>,
) -> Result<Vec<PostMetadata>, PostError> {
let mut posts = self
.get_all_post_metadata_filtered(|metadata| {
!tag.is_some_and(|tag| !metadata.tags.contains(tag))
})
.await?;
// we still want some semblance of order if created_at is None so sort by mtime as well
posts.sort_unstable_by_key(|metadata| metadata.modified_at.unwrap_or_default());
posts.sort_by_key(|metadata| metadata.created_at.unwrap_or_default());
posts.reverse();
if let Some(n) = n {
posts.truncate(n);
}
Ok(posts)
}
pub async fn get_post(
&self,
name: &str,
) -> Result<(PostMetadata, String, RenderStats), PostError> {
let start = Instant::now(); let start = Instant::now();
let path = self.config.dirs.posts.join(name.to_owned() + ".md"); let path = self.config.dirs.posts.join(name.to_owned() + ".md");
@ -324,11 +389,7 @@ where
} }
} }
pub fn cache(&self) -> Option<&Cache> { async fn cleanup(&self) {
self.cache.as_ref()
}
pub async fn cleanup(&self) {
if let Some(cache) = self.cache.as_ref() { if let Some(cache) = self.cache.as_ref() {
cache cache
.cleanup(|name| { .cleanup(|name| {
@ -340,40 +401,4 @@ where
.await .await
} }
} }
fn try_drop(&mut self) -> Result<(), eyre::Report> {
// write cache to file
let config = &self.config.cache;
if config.enable
&& config.persistence
&& let Some(cache) = self.cache()
{
let path = &config.file;
let serialized = bitcode::serialize(cache).context("failed to serialize cache")?;
let mut cache_file = std::fs::File::create(path)
.with_context(|| format!("failed to open cache at {}", path.display()))?;
let compression_level = config.compression_level;
if config.compress {
std::io::Write::write_all(
&mut zstd::stream::write::Encoder::new(cache_file, compression_level)?
.auto_finish(),
&serialized,
)
} else {
cache_file.write_all(&serialized)
}
.context("failed to write cache to file")?;
info!("wrote cache to {}", path.display());
}
Ok(())
}
}
impl<C> Drop for PostManager<C>
where
C: Deref<Target = Config>,
{
fn drop(&mut self) {
self.try_drop().unwrap()
}
} }