alloc optimization and enum refactors

This commit is contained in:
slonkazoid 2024-12-16 21:16:45 +03:00
parent ed81dcd223
commit bed8ae7849
Signed by: slonk
SSH key fingerprint: SHA256:tbZfJX4IOvZ0LGWOWu5Ijo8jfMPi78TU7x1VoEeCIjM
7 changed files with 212 additions and 179 deletions

View file

@ -47,7 +47,7 @@ mime_guess = "2.0.5"
notify-debouncer-full = { version = "0.3.1", default-features = false } notify-debouncer-full = { version = "0.3.1", default-features = false }
rss = "2.0.7" rss = "2.0.7"
scc = { version = "2.1.0", features = ["serde"] } scc = { version = "2.1.0", features = ["serde"] }
serde = { version = "1.0.197", features = ["derive"] } serde = { version = "1.0.197", features = ["derive", "rc"] }
serde-value = "0.7.0" serde-value = "0.7.0"
serde_json = { version = "1.0.124", features = ["preserve_order"] } serde_json = { version = "1.0.124", features = ["preserve_order"] }
syntect = "5.2.0" syntect = "5.2.0"

View file

@ -1,4 +1,3 @@
use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
@ -13,7 +12,6 @@ use include_dir::{include_dir, Dir};
use indexmap::IndexMap; use indexmap::IndexMap;
use rss::{Category, ChannelBuilder, ItemBuilder}; use rss::{Category, ChannelBuilder, ItemBuilder};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::Map;
use serde_value::Value; use serde_value::Value;
use tokio::sync::RwLock; use tokio::sync::RwLock;
use tower::service_fn; use tower::service_fn;
@ -57,7 +55,7 @@ struct IndexTemplate<'a> {
posts: Vec<PostMetadata>, posts: Vec<PostMetadata>,
rss: bool, rss: bool,
js: bool, js: bool,
tags: Map<String, serde_json::Value>, tags: IndexMap<Arc<str>, u64>,
joined_tags: String, joined_tags: String,
style: &'a StyleConfig, style: &'a StyleConfig,
} }
@ -66,13 +64,13 @@ struct IndexTemplate<'a> {
struct PostTemplate<'a> { struct PostTemplate<'a> {
bingus_info: &'a BingusInfo, bingus_info: &'a BingusInfo,
meta: &'a PostMetadata, meta: &'a PostMetadata,
rendered: String, rendered: Arc<str>,
rendered_in: RenderStats, rendered_in: RenderStats,
js: bool, js: bool,
color: Option<&'a str>, color: Option<&'a str>,
joined_tags: String, joined_tags: String,
style: &'a StyleConfig, style: &'a StyleConfig,
raw_name: Option<&'a str>, raw_name: Option<String>,
} }
#[derive(Deserialize)] #[derive(Deserialize)]
@ -84,12 +82,12 @@ struct QueryParams {
other: IndexMap<String, Value>, other: IndexMap<String, Value>,
} }
fn collect_tags(posts: &Vec<PostMetadata>) -> Map<String, serde_json::Value> { fn collect_tags(posts: &Vec<PostMetadata>) -> IndexMap<Arc<str>, u64> {
let mut tags = HashMap::new(); let mut tags = IndexMap::new();
for post in posts { for post in posts {
for tag in &post.tags { for tag in &post.tags {
if let Some((existing_tag, count)) = tags.remove_entry(tag) { if let Some((existing_tag, count)) = tags.swap_remove_entry(tag) {
tags.insert(existing_tag, count + 1); tags.insert(existing_tag, count + 1);
} else { } else {
tags.insert(tag.clone(), 1); tags.insert(tag.clone(), 1);
@ -97,21 +95,13 @@ fn collect_tags(posts: &Vec<PostMetadata>) -> Map<String, serde_json::Value> {
} }
} }
let mut tags: Vec<(String, u64)> = tags.into_iter().collect(); tags.sort_unstable_by(|k1, _v1, k2, _v2| k1.cmp(k2));
tags.sort_by(|_k1, v1, _k2, v2| v1.cmp(v2));
tags.sort_unstable_by_key(|(v, _)| v.clone()); tags
tags.sort_by_key(|(_, v)| -(*v as i64));
let mut map = Map::new();
for tag in tags.into_iter() {
map.insert(tag.0, tag.1.into());
}
map
} }
fn join_tags_for_meta(tags: &Map<String, serde_json::Value>, delim: &str) -> String { fn join_tags_for_meta(tags: &IndexMap<Arc<str>, u64>, delim: &str) -> String {
let mut s = String::new(); let mut s = String::new();
let tags = tags.keys().enumerate(); let tags = tags.keys().enumerate();
let len = tags.len(); let len = tags.len();
@ -207,21 +197,21 @@ async fn rss(
for (metadata, content, _) in posts { for (metadata, content, _) in posts {
channel.item( channel.item(
ItemBuilder::default() ItemBuilder::default()
.title(metadata.title) .title(metadata.title.to_string())
.description(metadata.description) .description(metadata.description.to_string())
.author(metadata.author) .author(metadata.author.to_string())
.categories( .categories(
metadata metadata
.tags .tags
.into_iter() .into_iter()
.map(|tag| Category { .map(|tag| Category {
name: tag, name: tag.to_string(),
domain: None, domain: None,
}) })
.collect::<Vec<Category>>(), .collect::<Vec<Category>>(),
) )
.pub_date(metadata.created_at.map(|date| date.to_rfc2822())) .pub_date(metadata.created_at.map(|date| date.to_rfc2822()))
.content(content) .content(content.to_string())
.link( .link(
config config
.rss .rss
@ -246,15 +236,18 @@ async fn post(
templates: reg, templates: reg,
.. ..
}): State<AppState>, }): State<AppState>,
Path(name): Path<String>, Path(name): Path<Arc<str>>,
Query(query): Query<QueryParams>, Query(query): Query<QueryParams>,
) -> AppResult<impl IntoResponse> { ) -> AppResult<impl IntoResponse> {
match posts.get_post(&name, &query.other).await? { match posts.get_post(name.clone(), &query.other).await? {
ReturnedPost::Rendered(ref meta, rendered, rendered_in) => { ReturnedPost::Rendered {
ref meta,
body: rendered,
perf: rendered_in,
} => {
let joined_tags = meta.tags.join(", "); let joined_tags = meta.tags.join(", ");
let reg = reg.read().await; let reg = reg.read().await;
let raw_name;
let rendered = reg.render( let rendered = reg.render(
"post", "post",
&PostTemplate { &PostTemplate {
@ -269,20 +262,19 @@ async fn post(
.or(config.style.default_color.as_deref()), .or(config.style.default_color.as_deref()),
joined_tags, joined_tags,
style: &config.style, style: &config.style,
raw_name: if config.markdown_access { raw_name: config
raw_name = posts.as_raw(&meta.name).await?; .markdown_access
raw_name.as_deref() .then(|| posts.as_raw(&meta.name))
} else { .unwrap_or(None),
None
},
}, },
); );
drop(reg); drop(reg);
Ok(Html(rendered?).into_response()) Ok(Html(rendered?).into_response())
} }
ReturnedPost::Raw(body, content_type) => { ReturnedPost::Raw {
Ok(([(CONTENT_TYPE, content_type)], body).into_response()) buffer,
} content_type,
} => Ok(([(CONTENT_TYPE, content_type)], buffer).into_response()),
} }
} }

View file

@ -1,3 +1,5 @@
use std::sync::Arc;
use askama_axum::Template; use askama_axum::Template;
use axum::http::StatusCode; use axum::http::StatusCode;
use axum::response::{IntoResponse, Response}; use axum::response::{IntoResponse, Response};
@ -14,7 +16,7 @@ pub enum PostError {
#[error("failed to render post: {0}")] #[error("failed to render post: {0}")]
RenderError(String), RenderError(String),
#[error("post {0:?} not found")] #[error("post {0:?} not found")]
NotFound(String), NotFound(Arc<str>),
} }
impl From<fronma::error::Error> for PostError { impl From<fronma::error::Error> for PostError {

View file

@ -27,21 +27,21 @@ use super::{ApplyFilters, PostManager, PostMetadata, RenderStats, ReturnedPost};
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
struct BlagMetadata { struct BlagMetadata {
pub title: String, pub title: Arc<str>,
pub description: String, pub description: Arc<str>,
pub author: String, pub author: Arc<str>,
pub icon: Option<String>, pub icon: Option<Arc<str>>,
pub icon_alt: Option<String>, pub icon_alt: Option<Arc<str>>,
pub color: Option<String>, pub color: Option<Arc<str>>,
pub created_at: Option<DateTime<Utc>>, pub created_at: Option<DateTime<Utc>>,
pub modified_at: Option<DateTime<Utc>>, pub modified_at: Option<DateTime<Utc>>,
#[serde(default)] #[serde(default)]
pub tags: BTreeSet<String>, pub tags: BTreeSet<Arc<str>>,
pub dont_cache: bool, pub dont_cache: bool,
} }
impl BlagMetadata { impl BlagMetadata {
pub fn into_full(self, name: String) -> (PostMetadata, bool) { pub fn into_full(self, name: Arc<str>) -> (PostMetadata, bool) {
( (
PostMetadata { PostMetadata {
name, name,
@ -79,7 +79,7 @@ impl Blag {
async fn render( async fn render(
&self, &self,
name: &str, name: Arc<str>,
path: impl AsRef<Path>, path: impl AsRef<Path>,
query_json: String, query_json: String,
) -> Result<(PostMetadata, String, (Duration, Duration), bool), PostError> { ) -> Result<(PostMetadata, String, (Duration, Duration), bool), PostError> {
@ -105,7 +105,7 @@ impl Blag {
let blag_meta: BlagMetadata = serde_json::from_str(&buf)?; let blag_meta: BlagMetadata = serde_json::from_str(&buf)?;
debug!("blag meta: {blag_meta:?}"); debug!("blag meta: {blag_meta:?}");
let (meta, dont_cache) = blag_meta.into_full(name.to_string()); let (meta, dont_cache) = blag_meta.into_full(name);
let parsed = start.elapsed(); let parsed = start.elapsed();
let rendering = Instant::now(); let rendering = Instant::now();
@ -132,7 +132,7 @@ impl PostManager for Blag {
&self, &self,
filters: &[Filter<'_>], filters: &[Filter<'_>],
query: &IndexMap<String, Value>, query: &IndexMap<String, Value>,
) -> Result<Vec<(PostMetadata, String, RenderStats)>, PostError> { ) -> Result<Vec<(PostMetadata, Arc<str>, RenderStats)>, PostError> {
let mut set = FuturesUnordered::new(); let mut set = FuturesUnordered::new();
let mut posts = Vec::new(); let mut posts = Vec::new();
let mut files = tokio::fs::read_dir(&self.root).await?; let mut files = tokio::fs::read_dir(&self.root).await?;
@ -149,17 +149,16 @@ impl PostManager for Blag {
let file_type = entry.file_type().await?; let file_type = entry.file_type().await?;
if file_type.is_file() { if file_type.is_file() {
let name = match entry.file_name().into_string() { let mut name = match entry.file_name().into_string() {
Ok(v) => v, Ok(v) => v,
Err(_) => { Err(_) => {
continue; continue;
} }
}; };
if name.ends_with(".sh") { if self.is_raw(&name) {
set.push( name.truncate(3);
async move { self.get_post(name.trim_end_matches(".sh"), query).await }, set.push(self.get_post(name.into(), query));
);
} }
} }
} }
@ -167,8 +166,8 @@ impl PostManager for Blag {
while let Some(result) = set.next().await { while let Some(result) = set.next().await {
let post = match result { let post = match result {
Ok(v) => match v { Ok(v) => match v {
ReturnedPost::Rendered(meta, content, stats) => (meta, content, stats), ReturnedPost::Rendered { meta, body, perf } => (meta, body, perf),
ReturnedPost::Raw(..) => unreachable!(), ReturnedPost::Raw { .. } => unreachable!(),
}, },
Err(err) => { Err(err) => {
error!("error while rendering blagpost: {err}"); error!("error while rendering blagpost: {err}");
@ -189,29 +188,29 @@ impl PostManager for Blag {
#[instrument(level = "info", skip(self))] #[instrument(level = "info", skip(self))]
async fn get_post( async fn get_post(
&self, &self,
name: &str, name: Arc<str>,
query: &IndexMap<String, Value>, query: &IndexMap<String, Value>,
) -> Result<ReturnedPost, PostError> { ) -> Result<ReturnedPost, PostError> {
let start = Instant::now(); let start = Instant::now();
let mut path = self.root.join(name); let mut path = self.root.join(&*name);
if name.ends_with(".sh") { if self.is_raw(&name) {
let mut buf = Vec::new(); let mut buffer = Vec::new();
let mut file = let mut file =
OpenOptions::new() OpenOptions::new()
.read(true) .read(true)
.open(&path) .open(&path)
.await .await
.map_err(|err| match err.kind() { .map_err(|err| match err.kind() {
std::io::ErrorKind::NotFound => PostError::NotFound(name.to_string()), std::io::ErrorKind::NotFound => PostError::NotFound(name),
_ => PostError::IoError(err), _ => PostError::IoError(err),
})?; })?;
file.read_to_end(&mut buf).await?; file.read_to_end(&mut buffer).await?;
return Ok(ReturnedPost::Raw( return Ok(ReturnedPost::Raw {
buf, buffer,
HeaderValue::from_static("text/x-shellscript"), content_type: HeaderValue::from_static("text/x-shellscript"),
)); });
} else { } else {
path.add_extension("sh"); path.add_extension("sh");
} }
@ -219,12 +218,12 @@ impl PostManager for Blag {
let stat = tokio::fs::metadata(&path) let stat = tokio::fs::metadata(&path)
.await .await
.map_err(|err| match err.kind() { .map_err(|err| match err.kind() {
std::io::ErrorKind::NotFound => PostError::NotFound(name.to_string()), std::io::ErrorKind::NotFound => PostError::NotFound(name.clone()),
_ => PostError::IoError(err), _ => PostError::IoError(err),
})?; })?;
if !stat.is_file() { if !stat.is_file() {
return Err(PostError::NotFound(name.to_string())); return Err(PostError::NotFound(name));
} }
let mtime = as_secs(&stat.modified()?); let mtime = as_secs(&stat.modified()?);
@ -235,67 +234,69 @@ impl PostManager for Blag {
let query_hash = hasher.finish(); let query_hash = hasher.finish();
let post = if let Some(cache) = &self.cache { let post = if let Some(cache) = &self.cache {
if let Some(CacheValue { if let Some(CacheValue { meta, body, .. }) =
metadata, rendered, .. cache.lookup(&name, mtime, query_hash).await
}) = cache.lookup(name, mtime, query_hash).await
{ {
ReturnedPost::Rendered(metadata, rendered, RenderStats::Cached(start.elapsed())) ReturnedPost::Rendered {
meta,
body,
perf: RenderStats::Cached(start.elapsed()),
}
} else { } else {
let (meta, content, (parsed, rendered), dont_cache) = let (meta, content, (parsed, rendered), dont_cache) =
self.render(name, path, query_json).await?; self.render(name.clone(), path, query_json).await?;
let body = content.into();
if !dont_cache { if !dont_cache {
cache cache
.insert( .insert(name, meta.clone(), mtime, Arc::clone(&body), query_hash)
name.to_string(),
meta.clone(),
mtime,
content.clone(),
query_hash,
)
.await .await
.unwrap_or_else(|err| warn!("failed to insert {:?} into cache", err.0)); .unwrap_or_else(|err| warn!("failed to insert {:?} into cache", err.0));
} }
let total = start.elapsed(); let total = start.elapsed();
ReturnedPost::Rendered( ReturnedPost::Rendered {
meta, meta,
content, body,
RenderStats::Rendered { perf: RenderStats::Rendered {
total, total,
parsed, parsed,
rendered, rendered,
}, },
) }
} }
} else { } else {
let (meta, content, (parsed, rendered), ..) = let (meta, content, (parsed, rendered), ..) =
self.render(name, path, query_json).await?; self.render(name, path, query_json).await?;
let total = start.elapsed(); let total = start.elapsed();
ReturnedPost::Rendered( ReturnedPost::Rendered {
meta, meta,
content, body: content.into(),
RenderStats::Rendered { perf: RenderStats::Rendered {
total, total,
parsed, parsed,
rendered, rendered,
}, },
) }
}; };
if let ReturnedPost::Rendered(.., stats) = &post { if let ReturnedPost::Rendered { perf, .. } = &post {
info!("rendered blagpost in {:?}", stats); info!("rendered blagpost in {:?}", perf);
} }
Ok(post) Ok(post)
} }
async fn as_raw(&self, name: &str) -> Result<Option<String>, PostError> { fn is_raw(&self, name: &str) -> bool {
name.ends_with(".sh")
}
fn as_raw(&self, name: &str) -> Option<String> {
let mut buf = String::with_capacity(name.len() + 3); let mut buf = String::with_capacity(name.len() + 3);
buf += name; buf += name;
buf += ".sh"; buf += ".sh";
Ok(Some(buf)) Some(buf)
} }
} }

View file

@ -1,5 +1,6 @@
use std::io::{Read, Write}; use std::io::{Read, Write};
use std::ops::Deref; use std::ops::Deref;
use std::sync::Arc;
use crate::config::CacheConfig; use crate::config::CacheConfig;
use crate::post::PostMetadata; use crate::post::PostMetadata;
@ -10,18 +11,18 @@ use tokio::io::AsyncReadExt;
use tracing::{debug, info, instrument}; use tracing::{debug, info, instrument};
/// do not persist cache if this version number changed /// do not persist cache if this version number changed
pub const CACHE_VERSION: u16 = 2; pub const CACHE_VERSION: u16 = 3;
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone)]
pub struct CacheValue { pub struct CacheValue {
pub metadata: PostMetadata, pub meta: PostMetadata,
pub rendered: String, pub body: Arc<str>,
pub mtime: u64, pub mtime: u64,
pub extra: u64, pub extra: u64,
} }
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone)]
pub struct FileCache(HashMap<String, CacheValue>, u16); pub struct FileCache(HashMap<Arc<str>, CacheValue>, u16);
impl Default for FileCache { impl Default for FileCache {
fn default() -> Self { fn default() -> Self {
@ -50,7 +51,7 @@ impl FileCache {
Some(entry) => { Some(entry) => {
let cached = entry.get(); let cached = entry.get();
if mtime <= cached.mtime { if mtime <= cached.mtime {
Some(cached.metadata.clone()) Some(cached.meta.clone())
} else { } else {
let _ = entry.remove(); let _ = entry.remove();
None None
@ -62,15 +63,15 @@ impl FileCache {
pub async fn insert( pub async fn insert(
&self, &self,
name: String, name: Arc<str>,
metadata: PostMetadata, metadata: PostMetadata,
mtime: u64, mtime: u64,
rendered: String, rendered: Arc<str>,
extra: u64, extra: u64,
) -> Result<(), (String, (PostMetadata, String))> { ) -> Result<(), (Arc<str>, (PostMetadata, Arc<str>))> {
let value = CacheValue { let value = CacheValue {
metadata, meta: metadata,
rendered, body: rendered,
mtime, mtime,
extra, extra,
}; };
@ -84,13 +85,13 @@ impl FileCache {
self.0 self.0
.insert_async(name, value) .insert_async(name, value)
.await .await
.map_err(|x| (x.0, (x.1.metadata, x.1.rendered))) .map_err(|x| (x.0, (x.1.meta, x.1.body)))
} else { } else {
Ok(()) Ok(())
} }
} }
pub async fn remove(&self, name: &str) -> Option<(String, CacheValue)> { pub async fn remove(&self, name: &str) -> Option<(Arc<str>, CacheValue)> {
self.0.remove_async(name).await self.0.remove_async(name).await
} }

View file

@ -24,29 +24,29 @@ use crate::config::Config;
use crate::markdown_render::{build_syntect, render}; use crate::markdown_render::{build_syntect, render};
use crate::systemtime_as_secs::as_secs; use crate::systemtime_as_secs::as_secs;
use super::cache::CacheGuard; use super::cache::{CacheGuard, CacheValue};
use super::{ use super::{
ApplyFilters, Filter, PostError, PostManager, PostMetadata, RenderStats, ReturnedPost, ApplyFilters, Filter, PostError, PostManager, PostMetadata, RenderStats, ReturnedPost,
}; };
#[derive(Deserialize)] #[derive(Deserialize)]
struct FrontMatter { struct FrontMatter {
pub title: String, pub title: Arc<str>,
pub description: String, pub description: Arc<str>,
pub author: String, pub author: Arc<str>,
pub icon: Option<String>, pub icon: Option<Arc<str>>,
pub icon_alt: Option<String>, pub icon_alt: Option<Arc<str>>,
pub color: Option<String>, pub color: Option<Arc<str>>,
pub created_at: Option<DateTime<Utc>>, pub created_at: Option<DateTime<Utc>>,
pub modified_at: Option<DateTime<Utc>>, pub modified_at: Option<DateTime<Utc>>,
#[serde(default)] #[serde(default)]
pub tags: BTreeSet<String>, pub tags: BTreeSet<Arc<str>>,
} }
impl FrontMatter { impl FrontMatter {
pub fn into_full( pub fn into_full(
self, self,
name: String, name: Arc<str>,
created: Option<SystemTime>, created: Option<SystemTime>,
modified: Option<SystemTime>, modified: Option<SystemTime>,
) -> PostMetadata { ) -> PostMetadata {
@ -94,9 +94,9 @@ impl MarkdownPosts {
async fn parse_and_render( async fn parse_and_render(
&self, &self,
name: String, name: Arc<str>,
path: impl AsRef<Path>, path: impl AsRef<Path>,
) -> Result<(PostMetadata, String, (Duration, Duration)), PostError> { ) -> Result<(PostMetadata, Arc<str>, (Duration, Duration)), PostError> {
let parsing_start = Instant::now(); let parsing_start = Instant::now();
let mut file = match tokio::fs::OpenOptions::new().read(true).open(&path).await { let mut file = match tokio::fs::OpenOptions::new().read(true).open(&path).await {
Ok(val) => val, Ok(val) => val,
@ -117,16 +117,16 @@ impl MarkdownPosts {
let parsing = parsing_start.elapsed(); let parsing = parsing_start.elapsed();
let before_render = Instant::now(); let before_render = Instant::now();
let post = render(body, Some(&self.syntect)); let post = render(body, Some(&self.syntect)).into();
let rendering = before_render.elapsed(); let rendering = before_render.elapsed();
if let Some(cache) = &self.cache { if let Some(cache) = &self.cache {
cache cache
.insert( .insert(
name.to_string(), name.clone(),
metadata.clone(), metadata.clone(),
as_secs(&modified), as_secs(&modified),
post.clone(), Arc::clone(&post),
self.render_hash, self.render_hash,
) )
.await .await
@ -143,7 +143,7 @@ impl PostManager for MarkdownPosts {
&self, &self,
filters: &[Filter<'_>], filters: &[Filter<'_>],
query: &IndexMap<String, Value>, query: &IndexMap<String, Value>,
) -> Result<Vec<(PostMetadata, String, RenderStats)>, PostError> { ) -> Result<Vec<(PostMetadata, Arc<str>, RenderStats)>, PostError> {
let mut posts = Vec::new(); let mut posts = Vec::new();
let mut read_dir = fs::read_dir(&self.config.dirs.posts).await?; let mut read_dir = fs::read_dir(&self.config.dirs.posts).await?;
@ -157,13 +157,14 @@ impl PostManager for MarkdownPosts {
.file_stem() .file_stem()
.unwrap() .unwrap()
.to_string_lossy() .to_string_lossy()
.to_string(); .to_string()
.into();
let post = self.get_post(&name, query).await?; let post = self.get_post(Arc::clone(&name), query).await?;
if let ReturnedPost::Rendered(meta, content, stats) = post if let ReturnedPost::Rendered { meta, body, perf } = post
&& meta.apply_filters(filters) && meta.apply_filters(filters)
{ {
posts.push((meta, content, stats)); posts.push((meta, body, perf));
} }
} }
} }
@ -185,7 +186,8 @@ impl PostManager for MarkdownPosts {
if stat.is_file() && path.extension().is_some_and(|ext| ext == "md") { if stat.is_file() && path.extension().is_some_and(|ext| ext == "md") {
let mtime = as_secs(&stat.modified()?); let mtime = as_secs(&stat.modified()?);
let name = String::from(path.file_stem().unwrap().to_string_lossy()); let name: Arc<str> =
String::from(path.file_stem().unwrap().to_string_lossy()).into();
if let Some(cache) = &self.cache if let Some(cache) = &self.cache
&& let Some(hit) = cache.lookup_metadata(&name, mtime).await && let Some(hit) = cache.lookup_metadata(&name, mtime).await
@ -218,42 +220,49 @@ impl PostManager for MarkdownPosts {
#[instrument(level = "info", skip(self))] #[instrument(level = "info", skip(self))]
async fn get_post( async fn get_post(
&self, &self,
name: &str, name: Arc<str>,
_query: &IndexMap<String, Value>, _query: &IndexMap<String, Value>,
) -> Result<ReturnedPost, PostError> { ) -> Result<ReturnedPost, PostError> {
let post = if self.config.markdown_access && name.ends_with(".md") { let post = if self.config.markdown_access && self.is_raw(&name) {
let path = self.config.dirs.posts.join(name); let path = self.config.dirs.posts.join(&*name);
let mut file = match tokio::fs::OpenOptions::new().read(true).open(&path).await { let mut file = match tokio::fs::OpenOptions::new().read(true).open(&path).await {
Ok(value) => value, Ok(value) => value,
Err(err) => match err.kind() { Err(err) => match err.kind() {
io::ErrorKind::NotFound => { io::ErrorKind::NotFound => {
if let Some(cache) = &self.cache { if let Some(cache) = &self.cache {
cache.remove(name).await; cache.remove(&name).await;
} }
return Err(PostError::NotFound(name.to_string())); return Err(PostError::NotFound(name));
} }
_ => return Err(PostError::IoError(err)), _ => return Err(PostError::IoError(err)),
}, },
}; };
let mut buf = Vec::with_capacity(4096); let mut buffer = Vec::with_capacity(4096);
file.read_to_end(&mut buf).await?; file.read_to_end(&mut buffer).await?;
ReturnedPost::Raw(buf, HeaderValue::from_static("text/plain")) ReturnedPost::Raw {
buffer,
content_type: HeaderValue::from_static("text/plain"),
}
} else { } else {
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(self.as_raw(&name).unwrap_or_else(|| unreachable!()));
let stat = match tokio::fs::metadata(&path).await { let stat = match tokio::fs::metadata(&path).await {
Ok(value) => value, Ok(value) => value,
Err(err) => match err.kind() { Err(err) => match err.kind() {
io::ErrorKind::NotFound => { io::ErrorKind::NotFound => {
if let Some(cache) = &self.cache { if let Some(cache) = &self.cache {
cache.remove(name).await; cache.remove(&name).await;
} }
return Err(PostError::NotFound(name.to_string())); return Err(PostError::NotFound(name));
} }
_ => return Err(PostError::IoError(err)), _ => return Err(PostError::IoError(err)),
}, },
@ -261,30 +270,30 @@ impl PostManager for MarkdownPosts {
let mtime = as_secs(&stat.modified()?); let mtime = as_secs(&stat.modified()?);
if let Some(cache) = &self.cache if let Some(cache) = &self.cache
&& let Some(hit) = cache.lookup(name, mtime, self.render_hash).await && let Some(CacheValue { meta, body, .. }) =
cache.lookup(&name, mtime, self.render_hash).await
{ {
ReturnedPost::Rendered( ReturnedPost::Rendered {
hit.metadata, meta,
hit.rendered, body,
RenderStats::Cached(start.elapsed()), perf: RenderStats::Cached(start.elapsed()),
) }
} else { } else {
let (metadata, rendered, stats) = let (meta, body, stats) = self.parse_and_render(name, path).await?;
self.parse_and_render(name.to_string(), path).await?; ReturnedPost::Rendered {
ReturnedPost::Rendered( meta,
metadata, body,
rendered, perf: RenderStats::Rendered {
RenderStats::Rendered {
total: start.elapsed(), total: start.elapsed(),
parsed: stats.0, parsed: stats.0,
rendered: stats.1, rendered: stats.1,
}, },
) }
} }
}; };
if let ReturnedPost::Rendered(.., stats) = &post { if let ReturnedPost::Rendered { perf, .. } = &post {
info!("rendered post in {:?}", stats); info!("rendered post in {:?}", perf);
} }
Ok(post) Ok(post)
@ -294,20 +303,29 @@ impl PostManager for MarkdownPosts {
if let Some(cache) = &self.cache { if let Some(cache) = &self.cache {
cache cache
.cleanup(|name| { .cleanup(|name| {
std::fs::metadata(self.config.dirs.posts.join(name.to_owned() + ".md")) std::fs::metadata(
.ok() self.config
.and_then(|metadata| metadata.modified().ok()) .dirs
.map(|mtime| as_secs(&mtime)) .posts
.join(self.as_raw(name).unwrap_or_else(|| unreachable!())),
)
.ok()
.and_then(|metadata| metadata.modified().ok())
.map(|mtime| as_secs(&mtime))
}) })
.await .await
} }
} }
async fn as_raw(&self, name: &str) -> Result<Option<String>, PostError> { fn is_raw(&self, name: &str) -> bool {
name.ends_with(".md")
}
fn as_raw(&self, name: &str) -> Option<String> {
let mut buf = String::with_capacity(name.len() + 3); let mut buf = String::with_capacity(name.len() + 3);
buf += name; buf += name;
buf += ".md"; buf += ".md";
Ok(Some(buf)) Some(buf)
} }
} }

View file

@ -2,9 +2,11 @@ pub mod blag;
pub mod cache; pub mod cache;
pub mod markdown_posts; pub mod markdown_posts;
use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use axum::{async_trait, http::HeaderValue}; use axum::async_trait;
use axum::http::HeaderValue;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use indexmap::IndexMap; use indexmap::IndexMap;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -17,19 +19,19 @@ pub use markdown_posts::MarkdownPosts;
// TODO: replace String with Arc<str> // TODO: replace String with Arc<str>
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug)]
pub struct PostMetadata { pub struct PostMetadata {
pub name: String, pub name: Arc<str>,
pub title: String, pub title: Arc<str>,
pub description: String, pub description: Arc<str>,
pub author: String, pub author: Arc<str>,
pub icon: Option<String>, pub icon: Option<Arc<str>>,
pub icon_alt: Option<String>, pub icon_alt: Option<Arc<str>>,
pub color: Option<String>, pub color: Option<Arc<str>>,
pub created_at: Option<DateTime<Utc>>, pub created_at: Option<DateTime<Utc>>,
pub modified_at: Option<DateTime<Utc>>, pub modified_at: Option<DateTime<Utc>>,
pub tags: Vec<String>, pub tags: Vec<Arc<str>>,
} }
#[derive(Serialize, Debug)] #[derive(Serialize, Debug, Clone)]
#[allow(unused)] #[allow(unused)]
pub enum RenderStats { pub enum RenderStats {
Cached(Duration), Cached(Duration),
@ -39,13 +41,25 @@ pub enum RenderStats {
rendered: Duration, rendered: Duration,
}, },
Fetched(Duration), Fetched(Duration),
Other {
verb: Arc<str>,
time: Duration,
},
Unknown, Unknown,
} }
#[allow(clippy::large_enum_variant)] // Raw will be returned very rarely #[allow(clippy::large_enum_variant)] // Raw will be returned very rarely
#[derive(Debug, Clone)]
pub enum ReturnedPost { pub enum ReturnedPost {
Rendered(PostMetadata, String, RenderStats), Rendered {
Raw(Vec<u8>, HeaderValue), meta: PostMetadata,
body: Arc<str>,
perf: RenderStats,
},
Raw {
buffer: Vec<u8>,
content_type: HeaderValue,
},
} }
pub enum Filter<'a> { pub enum Filter<'a> {
@ -57,7 +71,7 @@ impl Filter<'_> {
match self { match self {
Filter::Tags(tags) => tags Filter::Tags(tags) => tags
.iter() .iter()
.any(|tag| meta.tags.iter().any(|meta_tag| meta_tag == tag)), .any(|tag| meta.tags.iter().any(|meta_tag| &**meta_tag == *tag)),
} }
} }
} }
@ -93,7 +107,7 @@ pub trait PostManager {
&self, &self,
filters: &[Filter<'_>], filters: &[Filter<'_>],
query: &IndexMap<String, Value>, query: &IndexMap<String, Value>,
) -> Result<Vec<(PostMetadata, String, RenderStats)>, PostError>; ) -> Result<Vec<(PostMetadata, Arc<str>, RenderStats)>, PostError>;
async fn get_max_n_post_metadata_with_optional_tag_sorted( async fn get_max_n_post_metadata_with_optional_tag_sorted(
&self, &self,
@ -119,25 +133,30 @@ pub trait PostManager {
#[allow(unused)] #[allow(unused)]
async fn get_post_metadata( async fn get_post_metadata(
&self, &self,
name: &str, name: Arc<str>,
query: &IndexMap<String, Value>, query: &IndexMap<String, Value>,
) -> Result<PostMetadata, PostError> { ) -> Result<PostMetadata, PostError> {
match self.get_post(name, query).await? { match self.get_post(name.clone(), query).await? {
ReturnedPost::Rendered(metadata, ..) => Ok(metadata), ReturnedPost::Rendered { meta, .. } => Ok(meta),
ReturnedPost::Raw(..) => Err(PostError::NotFound(name.to_string())), ReturnedPost::Raw { .. } => Err(PostError::NotFound(name)),
} }
} }
async fn get_post( async fn get_post(
&self, &self,
name: &str, name: Arc<str>,
query: &IndexMap<String, Value>, query: &IndexMap<String, Value>,
) -> Result<ReturnedPost, PostError>; ) -> Result<ReturnedPost, PostError>;
async fn cleanup(&self) {} async fn cleanup(&self) {}
#[allow(unused)] #[allow(unused)]
async fn as_raw(&self, name: &str) -> Result<Option<String>, PostError> { fn is_raw(&self, name: &str) -> bool {
Ok(None) false
}
#[allow(unused)]
fn as_raw(&self, name: &str) -> Option<String> {
None
} }
} }