From cf102126b38572bff9cdb6f3b32866149a6a012b Mon Sep 17 00:00:00 2001 From: slonkazoid Date: Tue, 14 May 2024 12:26:43 +0300 Subject: [PATCH] move the rest of markdown-related stuff into it's own file --- README.md | 3 +- src/app.rs | 36 +++++------ src/config.rs | 4 +- src/post/markdown_posts.rs | 127 ++++++++++++++++++++++++++++--------- src/post/mod.rs | 50 ++++----------- 5 files changed, 128 insertions(+), 92 deletions(-) diff --git a/README.md b/README.md index de24d7f..afa78ac 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,8 @@ the default configuration with comments looks like this ```toml title = "bingus-blog" # title of the website description = "blazingly fast markdown blog software written in rust memory safe" # description of the website -raw_access = true # allow users to see the raw markdown of a post +markdown_access = true # allow users to see the raw markdown of a post + # endpoint: /posts/.md [rss] enable = false # serve an rss field under /feed.xml diff --git a/src/app.rs b/src/app.rs index 3e0d4be..72b8322 100644 --- a/src/app.rs +++ b/src/app.rs @@ -3,13 +3,13 @@ use std::time::Duration; use askama_axum::Template; use axum::extract::{Path, Query, State}; +use axum::http::header::CONTENT_TYPE; use axum::http::{header, Request}; use axum::response::{IntoResponse, Redirect, Response}; use axum::routing::get; use axum::{Json, Router}; use rss::{Category, ChannelBuilder, ItemBuilder}; use serde::Deserialize; -use tokio::io::AsyncReadExt; use tower_http::services::ServeDir; use tower_http::trace::TraceLayer; use tracing::{info, info_span, Span}; @@ -17,7 +17,7 @@ use tracing::{info, info_span, Span}; use crate::config::Config; use crate::error::{AppError, AppResult}; use crate::filters; -use crate::post::{MarkdownPosts, PostManager, PostMetadata, RenderStats}; +use crate::post::{MarkdownPosts, PostManager, PostMetadata, RenderStats, ReturnedPost}; #[derive(Clone)] pub struct AppState { @@ -138,26 +138,20 @@ async fn post( State(AppState { config, posts }): State, Path(name): Path, ) -> AppResult { - if name.ends_with(".md") && config.raw_access { - let mut file = tokio::fs::OpenOptions::new() - .read(true) - .open(config.dirs.posts.join(&name)) - .await?; + match posts.get_post(&name).await? { + ReturnedPost::Rendered(meta, rendered, rendered_in) => { + let page = PostTemplate { + meta, + rendered, + rendered_in, + markdown_access: config.markdown_access, + }; - let mut buf = Vec::new(); - file.read_to_end(&mut buf).await?; - - Ok(([("content-type", "text/plain")], buf).into_response()) - } else { - let post = posts.get_post(&name).await?; - let page = PostTemplate { - meta: post.0, - rendered: post.1, - rendered_in: post.2, - markdown_access: config.raw_access, - }; - - Ok(page.into_response()) + Ok(page.into_response()) + } + ReturnedPost::Raw(body, content_type) => { + Ok(([(CONTENT_TYPE, content_type)], body).into_response()) + } } } diff --git a/src/config.rs b/src/config.rs index c54e1db..23df5bf 100644 --- a/src/config.rs +++ b/src/config.rs @@ -64,7 +64,7 @@ pub struct RssConfig { pub struct Config { pub title: String, pub description: String, - pub raw_access: bool, + pub markdown_access: bool, pub num_posts: usize, pub rss: RssConfig, pub dirs: DirsConfig, @@ -78,7 +78,7 @@ impl Default for Config { Self { title: "bingus-blog".into(), description: "blazingly fast markdown blog software written in rust memory safe".into(), - raw_access: true, + markdown_access: true, num_posts: 5, // i have a love-hate relationship with serde // it was engimatic at first, but then i started actually using it diff --git a/src/post/markdown_posts.rs b/src/post/markdown_posts.rs index 1f73f33..e1ee1df 100644 --- a/src/post/markdown_posts.rs +++ b/src/post/markdown_posts.rs @@ -1,11 +1,16 @@ +use std::collections::BTreeSet; use std::io::{self, Write}; use std::ops::Deref; use std::path::Path; use std::time::Duration; use std::time::Instant; +use std::time::SystemTime; +use axum::http::HeaderValue; +use chrono::{DateTime, Utc}; use color_eyre::eyre::{self, Context}; use fronma::parser::{parse, ParsedData}; +use serde::Deserialize; use tokio::fs; use tokio::io::AsyncReadExt; use tracing::{error, info, warn}; @@ -13,9 +18,40 @@ use tracing::{error, info, warn}; use crate::config::Config; use crate::markdown_render::render; use crate::post::cache::{load_cache, Cache, CACHE_VERSION}; -use crate::post::{FrontMatter, PostError, PostManager, PostMetadata, RenderStats}; +use crate::post::{PostError, PostManager, PostMetadata, RenderStats, ReturnedPost}; use crate::systemtime_as_secs::as_secs; +#[derive(Deserialize)] +struct FrontMatter { + pub title: String, + pub description: String, + pub author: String, + pub icon: Option, + pub created_at: Option>, + pub modified_at: Option>, + #[serde(default)] + pub tags: BTreeSet, +} + +impl FrontMatter { + pub fn into_full( + self, + name: String, + created: Option, + modified: Option, + ) -> PostMetadata { + PostMetadata { + name, + title: self.title, + description: self.description, + author: self.author, + icon: self.icon, + created_at: self.created_at.or_else(|| created.map(|t| t.into())), + modified_at: self.modified_at.or_else(|| modified.map(|t| t.into())), + tags: self.tags.into_iter().collect(), + } + } +} pub struct MarkdownPosts where C: Deref, @@ -219,8 +255,10 @@ where .to_string(); let post = self.get_post(&name).await?; - if filter(&post.0, &post.1) { - posts.push(post); + if let ReturnedPost::Rendered(meta, content, stats) = post + && filter(&meta, &content) + { + posts.push((meta, content, stats)); } } } @@ -228,39 +266,66 @@ where Ok(posts) } - async fn get_post(&self, name: &str) -> Result<(PostMetadata, String, RenderStats), PostError> { - let start = Instant::now(); - let path = self.config.dirs.posts.join(name.to_owned() + ".md"); + async fn get_post(&self, name: &str) -> Result { + if self.config.markdown_access && name.ends_with(".md") { + let path = self.config.dirs.posts.join(name); - let stat = match tokio::fs::metadata(&path).await { - Ok(value) => value, - Err(err) => match err.kind() { - io::ErrorKind::NotFound => { - if let Some(cache) = self.cache.as_ref() { - cache.remove(name).await; + let mut file = match tokio::fs::OpenOptions::new().read(true).open(&path).await { + Ok(value) => value, + Err(err) => match err.kind() { + io::ErrorKind::NotFound => { + if let Some(cache) = self.cache.as_ref() { + cache.remove(name).await; + } + return Err(PostError::NotFound(name.to_string())); } - return Err(PostError::NotFound(name.to_string())); - } - _ => return Err(PostError::IoError(err)), - }, - }; - let mtime = as_secs(&stat.modified()?); + _ => return Err(PostError::IoError(err)), + }, + }; - if let Some(cache) = self.cache.as_ref() - && let Some(hit) = cache.lookup(name, mtime, &self.config.render).await - { - Ok(( - hit.metadata, - hit.rendered, - RenderStats::Cached(start.elapsed()), + let mut buf = Vec::with_capacity(4096); + + file.read_to_end(&mut buf).await?; + + Ok(ReturnedPost::Raw( + buf, + HeaderValue::from_static("text/plain"), )) } else { - let (metadata, rendered, stats) = self.parse_and_render(name.to_string(), path).await?; - Ok(( - metadata, - rendered, - RenderStats::ParsedAndRendered(start.elapsed(), stats.0, stats.1), - )) + let start = Instant::now(); + let path = self.config.dirs.posts.join(name.to_owned() + ".md"); + + let stat = match tokio::fs::metadata(&path).await { + Ok(value) => value, + Err(err) => match err.kind() { + io::ErrorKind::NotFound => { + if let Some(cache) = self.cache.as_ref() { + cache.remove(name).await; + } + return Err(PostError::NotFound(name.to_string())); + } + _ => return Err(PostError::IoError(err)), + }, + }; + let mtime = as_secs(&stat.modified()?); + + if let Some(cache) = self.cache.as_ref() + && let Some(hit) = cache.lookup(name, mtime, &self.config.render).await + { + Ok(ReturnedPost::Rendered( + hit.metadata, + hit.rendered, + RenderStats::Cached(start.elapsed()), + )) + } else { + let (metadata, rendered, stats) = + self.parse_and_render(name.to_string(), path).await?; + Ok(ReturnedPost::Rendered( + metadata, + rendered, + RenderStats::ParsedAndRendered(start.elapsed(), stats.0, stats.1), + )) + } } } diff --git a/src/post/mod.rs b/src/post/mod.rs index 2cf4e09..6dcb0b1 100644 --- a/src/post/mod.rs +++ b/src/post/mod.rs @@ -1,47 +1,15 @@ pub mod cache; pub mod markdown_posts; -use std::collections::BTreeSet; -use std::time::{Duration, SystemTime}; +use std::time::Duration; +use axum::http::HeaderValue; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use crate::error::PostError; pub use crate::post::markdown_posts::MarkdownPosts; -#[derive(Deserialize)] -struct FrontMatter { - pub title: String, - pub description: String, - pub author: String, - pub icon: Option, - pub created_at: Option>, - pub modified_at: Option>, - #[serde(default)] - pub tags: BTreeSet, -} - -impl FrontMatter { - pub fn into_full( - self, - name: String, - created: Option, - modified: Option, - ) -> PostMetadata { - PostMetadata { - name, - title: self.title, - description: self.description, - author: self.author, - icon: self.icon, - created_at: self.created_at.or_else(|| created.map(|t| t.into())), - modified_at: self.modified_at.or_else(|| modified.map(|t| t.into())), - tags: self.tags.into_iter().collect(), - } - } -} - #[derive(Serialize, Deserialize, Clone, Debug)] pub struct PostMetadata { pub name: String, @@ -54,13 +22,18 @@ pub struct PostMetadata { pub tags: Vec, } -#[allow(unused)] pub enum RenderStats { Cached(Duration), // format: Total, Parsed in, Rendered in ParsedAndRendered(Duration, Duration, Duration), } +#[allow(clippy::large_enum_variant)] // Raw will be returned very rarely +pub enum ReturnedPost { + Rendered(PostMetadata, String, RenderStats), + Raw(Vec, HeaderValue), +} + pub trait PostManager { async fn get_all_post_metadata( &self, @@ -97,10 +70,13 @@ pub trait PostManager { #[allow(unused)] async fn get_post_metadata(&self, name: &str) -> Result { - self.get_post(name).await.map(|(meta, ..)| meta) + match self.get_post(name).await? { + ReturnedPost::Rendered(metadata, ..) => Ok(metadata), + ReturnedPost::Raw(..) => Err(PostError::NotFound(name.to_string())), + } } - async fn get_post(&self, name: &str) -> Result<(PostMetadata, String, RenderStats), PostError>; + async fn get_post(&self, name: &str) -> Result; async fn cleanup(&self); }