alloc optimization and enum refactors
This commit is contained in:
parent
ed81dcd223
commit
bed8ae7849
7 changed files with 212 additions and 179 deletions
|
@ -47,7 +47,7 @@ mime_guess = "2.0.5"
|
|||
notify-debouncer-full = { version = "0.3.1", default-features = false }
|
||||
rss = "2.0.7"
|
||||
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_json = { version = "1.0.124", features = ["preserve_order"] }
|
||||
syntect = "5.2.0"
|
||||
|
|
68
src/app.rs
68
src/app.rs
|
@ -1,4 +1,3 @@
|
|||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
|
@ -13,7 +12,6 @@ use include_dir::{include_dir, Dir};
|
|||
use indexmap::IndexMap;
|
||||
use rss::{Category, ChannelBuilder, ItemBuilder};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Map;
|
||||
use serde_value::Value;
|
||||
use tokio::sync::RwLock;
|
||||
use tower::service_fn;
|
||||
|
@ -57,7 +55,7 @@ struct IndexTemplate<'a> {
|
|||
posts: Vec<PostMetadata>,
|
||||
rss: bool,
|
||||
js: bool,
|
||||
tags: Map<String, serde_json::Value>,
|
||||
tags: IndexMap<Arc<str>, u64>,
|
||||
joined_tags: String,
|
||||
style: &'a StyleConfig,
|
||||
}
|
||||
|
@ -66,13 +64,13 @@ struct IndexTemplate<'a> {
|
|||
struct PostTemplate<'a> {
|
||||
bingus_info: &'a BingusInfo,
|
||||
meta: &'a PostMetadata,
|
||||
rendered: String,
|
||||
rendered: Arc<str>,
|
||||
rendered_in: RenderStats,
|
||||
js: bool,
|
||||
color: Option<&'a str>,
|
||||
joined_tags: String,
|
||||
style: &'a StyleConfig,
|
||||
raw_name: Option<&'a str>,
|
||||
raw_name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
@ -84,12 +82,12 @@ struct QueryParams {
|
|||
other: IndexMap<String, Value>,
|
||||
}
|
||||
|
||||
fn collect_tags(posts: &Vec<PostMetadata>) -> Map<String, serde_json::Value> {
|
||||
let mut tags = HashMap::new();
|
||||
fn collect_tags(posts: &Vec<PostMetadata>) -> IndexMap<Arc<str>, u64> {
|
||||
let mut tags = IndexMap::new();
|
||||
|
||||
for post in posts {
|
||||
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);
|
||||
} else {
|
||||
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.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
|
||||
tags
|
||||
}
|
||||
|
||||
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 tags = tags.keys().enumerate();
|
||||
let len = tags.len();
|
||||
|
@ -207,21 +197,21 @@ async fn rss(
|
|||
for (metadata, content, _) in posts {
|
||||
channel.item(
|
||||
ItemBuilder::default()
|
||||
.title(metadata.title)
|
||||
.description(metadata.description)
|
||||
.author(metadata.author)
|
||||
.title(metadata.title.to_string())
|
||||
.description(metadata.description.to_string())
|
||||
.author(metadata.author.to_string())
|
||||
.categories(
|
||||
metadata
|
||||
.tags
|
||||
.into_iter()
|
||||
.map(|tag| Category {
|
||||
name: tag,
|
||||
name: tag.to_string(),
|
||||
domain: None,
|
||||
})
|
||||
.collect::<Vec<Category>>(),
|
||||
)
|
||||
.pub_date(metadata.created_at.map(|date| date.to_rfc2822()))
|
||||
.content(content)
|
||||
.content(content.to_string())
|
||||
.link(
|
||||
config
|
||||
.rss
|
||||
|
@ -246,15 +236,18 @@ async fn post(
|
|||
templates: reg,
|
||||
..
|
||||
}): State<AppState>,
|
||||
Path(name): Path<String>,
|
||||
Path(name): Path<Arc<str>>,
|
||||
Query(query): Query<QueryParams>,
|
||||
) -> AppResult<impl IntoResponse> {
|
||||
match posts.get_post(&name, &query.other).await? {
|
||||
ReturnedPost::Rendered(ref meta, rendered, rendered_in) => {
|
||||
match posts.get_post(name.clone(), &query.other).await? {
|
||||
ReturnedPost::Rendered {
|
||||
ref meta,
|
||||
body: rendered,
|
||||
perf: rendered_in,
|
||||
} => {
|
||||
let joined_tags = meta.tags.join(", ");
|
||||
|
||||
let reg = reg.read().await;
|
||||
let raw_name;
|
||||
let rendered = reg.render(
|
||||
"post",
|
||||
&PostTemplate {
|
||||
|
@ -269,20 +262,19 @@ async fn post(
|
|||
.or(config.style.default_color.as_deref()),
|
||||
joined_tags,
|
||||
style: &config.style,
|
||||
raw_name: if config.markdown_access {
|
||||
raw_name = posts.as_raw(&meta.name).await?;
|
||||
raw_name.as_deref()
|
||||
} else {
|
||||
None
|
||||
},
|
||||
raw_name: config
|
||||
.markdown_access
|
||||
.then(|| posts.as_raw(&meta.name))
|
||||
.unwrap_or(None),
|
||||
},
|
||||
);
|
||||
drop(reg);
|
||||
Ok(Html(rendered?).into_response())
|
||||
}
|
||||
ReturnedPost::Raw(body, content_type) => {
|
||||
Ok(([(CONTENT_TYPE, content_type)], body).into_response())
|
||||
}
|
||||
ReturnedPost::Raw {
|
||||
buffer,
|
||||
content_type,
|
||||
} => Ok(([(CONTENT_TYPE, content_type)], buffer).into_response()),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use askama_axum::Template;
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::{IntoResponse, Response};
|
||||
|
@ -14,7 +16,7 @@ pub enum PostError {
|
|||
#[error("failed to render post: {0}")]
|
||||
RenderError(String),
|
||||
#[error("post {0:?} not found")]
|
||||
NotFound(String),
|
||||
NotFound(Arc<str>),
|
||||
}
|
||||
|
||||
impl From<fronma::error::Error> for PostError {
|
||||
|
|
109
src/post/blag.rs
109
src/post/blag.rs
|
@ -27,21 +27,21 @@ use super::{ApplyFilters, PostManager, PostMetadata, RenderStats, ReturnedPost};
|
|||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct BlagMetadata {
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
pub author: String,
|
||||
pub icon: Option<String>,
|
||||
pub icon_alt: Option<String>,
|
||||
pub color: Option<String>,
|
||||
pub title: Arc<str>,
|
||||
pub description: Arc<str>,
|
||||
pub author: Arc<str>,
|
||||
pub icon: Option<Arc<str>>,
|
||||
pub icon_alt: Option<Arc<str>>,
|
||||
pub color: Option<Arc<str>>,
|
||||
pub created_at: Option<DateTime<Utc>>,
|
||||
pub modified_at: Option<DateTime<Utc>>,
|
||||
#[serde(default)]
|
||||
pub tags: BTreeSet<String>,
|
||||
pub tags: BTreeSet<Arc<str>>,
|
||||
pub dont_cache: bool,
|
||||
}
|
||||
|
||||
impl BlagMetadata {
|
||||
pub fn into_full(self, name: String) -> (PostMetadata, bool) {
|
||||
pub fn into_full(self, name: Arc<str>) -> (PostMetadata, bool) {
|
||||
(
|
||||
PostMetadata {
|
||||
name,
|
||||
|
@ -79,7 +79,7 @@ impl Blag {
|
|||
|
||||
async fn render(
|
||||
&self,
|
||||
name: &str,
|
||||
name: Arc<str>,
|
||||
path: impl AsRef<Path>,
|
||||
query_json: String,
|
||||
) -> Result<(PostMetadata, String, (Duration, Duration), bool), PostError> {
|
||||
|
@ -105,7 +105,7 @@ impl Blag {
|
|||
|
||||
let blag_meta: BlagMetadata = serde_json::from_str(&buf)?;
|
||||
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 rendering = Instant::now();
|
||||
|
@ -132,7 +132,7 @@ impl PostManager for Blag {
|
|||
&self,
|
||||
filters: &[Filter<'_>],
|
||||
query: &IndexMap<String, Value>,
|
||||
) -> Result<Vec<(PostMetadata, String, RenderStats)>, PostError> {
|
||||
) -> Result<Vec<(PostMetadata, Arc<str>, RenderStats)>, PostError> {
|
||||
let mut set = FuturesUnordered::new();
|
||||
let mut posts = Vec::new();
|
||||
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?;
|
||||
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,
|
||||
Err(_) => {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
if name.ends_with(".sh") {
|
||||
set.push(
|
||||
async move { self.get_post(name.trim_end_matches(".sh"), query).await },
|
||||
);
|
||||
if self.is_raw(&name) {
|
||||
name.truncate(3);
|
||||
set.push(self.get_post(name.into(), query));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -167,8 +166,8 @@ impl PostManager for Blag {
|
|||
while let Some(result) = set.next().await {
|
||||
let post = match result {
|
||||
Ok(v) => match v {
|
||||
ReturnedPost::Rendered(meta, content, stats) => (meta, content, stats),
|
||||
ReturnedPost::Raw(..) => unreachable!(),
|
||||
ReturnedPost::Rendered { meta, body, perf } => (meta, body, perf),
|
||||
ReturnedPost::Raw { .. } => unreachable!(),
|
||||
},
|
||||
Err(err) => {
|
||||
error!("error while rendering blagpost: {err}");
|
||||
|
@ -189,29 +188,29 @@ impl PostManager for Blag {
|
|||
#[instrument(level = "info", skip(self))]
|
||||
async fn get_post(
|
||||
&self,
|
||||
name: &str,
|
||||
name: Arc<str>,
|
||||
query: &IndexMap<String, Value>,
|
||||
) -> Result<ReturnedPost, PostError> {
|
||||
let start = Instant::now();
|
||||
let mut path = self.root.join(name);
|
||||
let mut path = self.root.join(&*name);
|
||||
|
||||
if name.ends_with(".sh") {
|
||||
let mut buf = Vec::new();
|
||||
if self.is_raw(&name) {
|
||||
let mut buffer = Vec::new();
|
||||
let mut file =
|
||||
OpenOptions::new()
|
||||
.read(true)
|
||||
.open(&path)
|
||||
.await
|
||||
.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),
|
||||
})?;
|
||||
file.read_to_end(&mut buf).await?;
|
||||
file.read_to_end(&mut buffer).await?;
|
||||
|
||||
return Ok(ReturnedPost::Raw(
|
||||
buf,
|
||||
HeaderValue::from_static("text/x-shellscript"),
|
||||
));
|
||||
return Ok(ReturnedPost::Raw {
|
||||
buffer,
|
||||
content_type: HeaderValue::from_static("text/x-shellscript"),
|
||||
});
|
||||
} else {
|
||||
path.add_extension("sh");
|
||||
}
|
||||
|
@ -219,12 +218,12 @@ impl PostManager for Blag {
|
|||
let stat = tokio::fs::metadata(&path)
|
||||
.await
|
||||
.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),
|
||||
})?;
|
||||
|
||||
if !stat.is_file() {
|
||||
return Err(PostError::NotFound(name.to_string()));
|
||||
return Err(PostError::NotFound(name));
|
||||
}
|
||||
|
||||
let mtime = as_secs(&stat.modified()?);
|
||||
|
@ -235,67 +234,69 @@ impl PostManager for Blag {
|
|||
let query_hash = hasher.finish();
|
||||
|
||||
let post = if let Some(cache) = &self.cache {
|
||||
if let Some(CacheValue {
|
||||
metadata, rendered, ..
|
||||
}) = cache.lookup(name, mtime, query_hash).await
|
||||
if let Some(CacheValue { meta, body, .. }) =
|
||||
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 {
|
||||
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 {
|
||||
cache
|
||||
.insert(
|
||||
name.to_string(),
|
||||
meta.clone(),
|
||||
mtime,
|
||||
content.clone(),
|
||||
query_hash,
|
||||
)
|
||||
.insert(name, meta.clone(), mtime, Arc::clone(&body), query_hash)
|
||||
.await
|
||||
.unwrap_or_else(|err| warn!("failed to insert {:?} into cache", err.0));
|
||||
}
|
||||
|
||||
let total = start.elapsed();
|
||||
ReturnedPost::Rendered(
|
||||
ReturnedPost::Rendered {
|
||||
meta,
|
||||
content,
|
||||
RenderStats::Rendered {
|
||||
body,
|
||||
perf: RenderStats::Rendered {
|
||||
total,
|
||||
parsed,
|
||||
rendered,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let (meta, content, (parsed, rendered), ..) =
|
||||
self.render(name, path, query_json).await?;
|
||||
|
||||
let total = start.elapsed();
|
||||
ReturnedPost::Rendered(
|
||||
ReturnedPost::Rendered {
|
||||
meta,
|
||||
content,
|
||||
RenderStats::Rendered {
|
||||
body: content.into(),
|
||||
perf: RenderStats::Rendered {
|
||||
total,
|
||||
parsed,
|
||||
rendered,
|
||||
},
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
if let ReturnedPost::Rendered(.., stats) = &post {
|
||||
info!("rendered blagpost in {:?}", stats);
|
||||
if let ReturnedPost::Rendered { perf, .. } = &post {
|
||||
info!("rendered blagpost in {:?}", perf);
|
||||
}
|
||||
|
||||
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);
|
||||
buf += name;
|
||||
buf += ".sh";
|
||||
|
||||
Ok(Some(buf))
|
||||
Some(buf)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use std::io::{Read, Write};
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::config::CacheConfig;
|
||||
use crate::post::PostMetadata;
|
||||
|
@ -10,18 +11,18 @@ use tokio::io::AsyncReadExt;
|
|||
use tracing::{debug, info, instrument};
|
||||
|
||||
/// 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)]
|
||||
pub struct CacheValue {
|
||||
pub metadata: PostMetadata,
|
||||
pub rendered: String,
|
||||
pub meta: PostMetadata,
|
||||
pub body: Arc<str>,
|
||||
pub mtime: u64,
|
||||
pub extra: u64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct FileCache(HashMap<String, CacheValue>, u16);
|
||||
pub struct FileCache(HashMap<Arc<str>, CacheValue>, u16);
|
||||
|
||||
impl Default for FileCache {
|
||||
fn default() -> Self {
|
||||
|
@ -50,7 +51,7 @@ impl FileCache {
|
|||
Some(entry) => {
|
||||
let cached = entry.get();
|
||||
if mtime <= cached.mtime {
|
||||
Some(cached.metadata.clone())
|
||||
Some(cached.meta.clone())
|
||||
} else {
|
||||
let _ = entry.remove();
|
||||
None
|
||||
|
@ -62,15 +63,15 @@ impl FileCache {
|
|||
|
||||
pub async fn insert(
|
||||
&self,
|
||||
name: String,
|
||||
name: Arc<str>,
|
||||
metadata: PostMetadata,
|
||||
mtime: u64,
|
||||
rendered: String,
|
||||
rendered: Arc<str>,
|
||||
extra: u64,
|
||||
) -> Result<(), (String, (PostMetadata, String))> {
|
||||
) -> Result<(), (Arc<str>, (PostMetadata, Arc<str>))> {
|
||||
let value = CacheValue {
|
||||
metadata,
|
||||
rendered,
|
||||
meta: metadata,
|
||||
body: rendered,
|
||||
mtime,
|
||||
extra,
|
||||
};
|
||||
|
@ -84,13 +85,13 @@ impl FileCache {
|
|||
self.0
|
||||
.insert_async(name, value)
|
||||
.await
|
||||
.map_err(|x| (x.0, (x.1.metadata, x.1.rendered)))
|
||||
.map_err(|x| (x.0, (x.1.meta, x.1.body)))
|
||||
} else {
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -24,29 +24,29 @@ use crate::config::Config;
|
|||
use crate::markdown_render::{build_syntect, render};
|
||||
use crate::systemtime_as_secs::as_secs;
|
||||
|
||||
use super::cache::CacheGuard;
|
||||
use super::cache::{CacheGuard, CacheValue};
|
||||
use super::{
|
||||
ApplyFilters, Filter, PostError, PostManager, PostMetadata, RenderStats, ReturnedPost,
|
||||
};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct FrontMatter {
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
pub author: String,
|
||||
pub icon: Option<String>,
|
||||
pub icon_alt: Option<String>,
|
||||
pub color: Option<String>,
|
||||
pub title: Arc<str>,
|
||||
pub description: Arc<str>,
|
||||
pub author: Arc<str>,
|
||||
pub icon: Option<Arc<str>>,
|
||||
pub icon_alt: Option<Arc<str>>,
|
||||
pub color: Option<Arc<str>>,
|
||||
pub created_at: Option<DateTime<Utc>>,
|
||||
pub modified_at: Option<DateTime<Utc>>,
|
||||
#[serde(default)]
|
||||
pub tags: BTreeSet<String>,
|
||||
pub tags: BTreeSet<Arc<str>>,
|
||||
}
|
||||
|
||||
impl FrontMatter {
|
||||
pub fn into_full(
|
||||
self,
|
||||
name: String,
|
||||
name: Arc<str>,
|
||||
created: Option<SystemTime>,
|
||||
modified: Option<SystemTime>,
|
||||
) -> PostMetadata {
|
||||
|
@ -94,9 +94,9 @@ impl MarkdownPosts {
|
|||
|
||||
async fn parse_and_render(
|
||||
&self,
|
||||
name: String,
|
||||
name: Arc<str>,
|
||||
path: impl AsRef<Path>,
|
||||
) -> Result<(PostMetadata, String, (Duration, Duration)), PostError> {
|
||||
) -> Result<(PostMetadata, Arc<str>, (Duration, Duration)), PostError> {
|
||||
let parsing_start = Instant::now();
|
||||
let mut file = match tokio::fs::OpenOptions::new().read(true).open(&path).await {
|
||||
Ok(val) => val,
|
||||
|
@ -117,16 +117,16 @@ impl MarkdownPosts {
|
|||
let parsing = parsing_start.elapsed();
|
||||
|
||||
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();
|
||||
|
||||
if let Some(cache) = &self.cache {
|
||||
cache
|
||||
.insert(
|
||||
name.to_string(),
|
||||
name.clone(),
|
||||
metadata.clone(),
|
||||
as_secs(&modified),
|
||||
post.clone(),
|
||||
Arc::clone(&post),
|
||||
self.render_hash,
|
||||
)
|
||||
.await
|
||||
|
@ -143,7 +143,7 @@ impl PostManager for MarkdownPosts {
|
|||
&self,
|
||||
filters: &[Filter<'_>],
|
||||
query: &IndexMap<String, Value>,
|
||||
) -> Result<Vec<(PostMetadata, String, RenderStats)>, PostError> {
|
||||
) -> Result<Vec<(PostMetadata, Arc<str>, RenderStats)>, PostError> {
|
||||
let mut posts = Vec::new();
|
||||
|
||||
let mut read_dir = fs::read_dir(&self.config.dirs.posts).await?;
|
||||
|
@ -157,13 +157,14 @@ impl PostManager for MarkdownPosts {
|
|||
.file_stem()
|
||||
.unwrap()
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
.to_string()
|
||||
.into();
|
||||
|
||||
let post = self.get_post(&name, query).await?;
|
||||
if let ReturnedPost::Rendered(meta, content, stats) = post
|
||||
let post = self.get_post(Arc::clone(&name), query).await?;
|
||||
if let ReturnedPost::Rendered { meta, body, perf } = post
|
||||
&& 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") {
|
||||
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
|
||||
&& let Some(hit) = cache.lookup_metadata(&name, mtime).await
|
||||
|
@ -218,42 +220,49 @@ impl PostManager for MarkdownPosts {
|
|||
#[instrument(level = "info", skip(self))]
|
||||
async fn get_post(
|
||||
&self,
|
||||
name: &str,
|
||||
name: Arc<str>,
|
||||
_query: &IndexMap<String, Value>,
|
||||
) -> Result<ReturnedPost, PostError> {
|
||||
let post = if self.config.markdown_access && name.ends_with(".md") {
|
||||
let path = self.config.dirs.posts.join(name);
|
||||
let post = if self.config.markdown_access && self.is_raw(&name) {
|
||||
let path = self.config.dirs.posts.join(&*name);
|
||||
|
||||
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 {
|
||||
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)),
|
||||
},
|
||||
};
|
||||
|
||||
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 {
|
||||
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 {
|
||||
Ok(value) => value,
|
||||
Err(err) => match err.kind() {
|
||||
io::ErrorKind::NotFound => {
|
||||
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)),
|
||||
},
|
||||
|
@ -261,30 +270,30 @@ impl PostManager for MarkdownPosts {
|
|||
let mtime = as_secs(&stat.modified()?);
|
||||
|
||||
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(
|
||||
hit.metadata,
|
||||
hit.rendered,
|
||||
RenderStats::Cached(start.elapsed()),
|
||||
)
|
||||
ReturnedPost::Rendered {
|
||||
meta,
|
||||
body,
|
||||
perf: RenderStats::Cached(start.elapsed()),
|
||||
}
|
||||
} else {
|
||||
let (metadata, rendered, stats) =
|
||||
self.parse_and_render(name.to_string(), path).await?;
|
||||
ReturnedPost::Rendered(
|
||||
metadata,
|
||||
rendered,
|
||||
RenderStats::Rendered {
|
||||
let (meta, body, stats) = self.parse_and_render(name, path).await?;
|
||||
ReturnedPost::Rendered {
|
||||
meta,
|
||||
body,
|
||||
perf: RenderStats::Rendered {
|
||||
total: start.elapsed(),
|
||||
parsed: stats.0,
|
||||
rendered: stats.1,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if let ReturnedPost::Rendered(.., stats) = &post {
|
||||
info!("rendered post in {:?}", stats);
|
||||
if let ReturnedPost::Rendered { perf, .. } = &post {
|
||||
info!("rendered post in {:?}", perf);
|
||||
}
|
||||
|
||||
Ok(post)
|
||||
|
@ -294,20 +303,29 @@ impl PostManager for MarkdownPosts {
|
|||
if let Some(cache) = &self.cache {
|
||||
cache
|
||||
.cleanup(|name| {
|
||||
std::fs::metadata(self.config.dirs.posts.join(name.to_owned() + ".md"))
|
||||
.ok()
|
||||
.and_then(|metadata| metadata.modified().ok())
|
||||
.map(|mtime| as_secs(&mtime))
|
||||
std::fs::metadata(
|
||||
self.config
|
||||
.dirs
|
||||
.posts
|
||||
.join(self.as_raw(name).unwrap_or_else(|| unreachable!())),
|
||||
)
|
||||
.ok()
|
||||
.and_then(|metadata| metadata.modified().ok())
|
||||
.map(|mtime| as_secs(&mtime))
|
||||
})
|
||||
.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);
|
||||
buf += name;
|
||||
buf += ".md";
|
||||
|
||||
Ok(Some(buf))
|
||||
Some(buf)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,9 +2,11 @@ pub mod blag;
|
|||
pub mod cache;
|
||||
pub mod markdown_posts;
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use axum::{async_trait, http::HeaderValue};
|
||||
use axum::async_trait;
|
||||
use axum::http::HeaderValue;
|
||||
use chrono::{DateTime, Utc};
|
||||
use indexmap::IndexMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
@ -17,19 +19,19 @@ pub use markdown_posts::MarkdownPosts;
|
|||
// TODO: replace String with Arc<str>
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct PostMetadata {
|
||||
pub name: String,
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
pub author: String,
|
||||
pub icon: Option<String>,
|
||||
pub icon_alt: Option<String>,
|
||||
pub color: Option<String>,
|
||||
pub name: Arc<str>,
|
||||
pub title: Arc<str>,
|
||||
pub description: Arc<str>,
|
||||
pub author: Arc<str>,
|
||||
pub icon: Option<Arc<str>>,
|
||||
pub icon_alt: Option<Arc<str>>,
|
||||
pub color: Option<Arc<str>>,
|
||||
pub created_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)]
|
||||
pub enum RenderStats {
|
||||
Cached(Duration),
|
||||
|
@ -39,13 +41,25 @@ pub enum RenderStats {
|
|||
rendered: Duration,
|
||||
},
|
||||
Fetched(Duration),
|
||||
Other {
|
||||
verb: Arc<str>,
|
||||
time: Duration,
|
||||
},
|
||||
Unknown,
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)] // Raw will be returned very rarely
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ReturnedPost {
|
||||
Rendered(PostMetadata, String, RenderStats),
|
||||
Raw(Vec<u8>, HeaderValue),
|
||||
Rendered {
|
||||
meta: PostMetadata,
|
||||
body: Arc<str>,
|
||||
perf: RenderStats,
|
||||
},
|
||||
Raw {
|
||||
buffer: Vec<u8>,
|
||||
content_type: HeaderValue,
|
||||
},
|
||||
}
|
||||
|
||||
pub enum Filter<'a> {
|
||||
|
@ -57,7 +71,7 @@ impl Filter<'_> {
|
|||
match self {
|
||||
Filter::Tags(tags) => tags
|
||||
.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,
|
||||
filters: &[Filter<'_>],
|
||||
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(
|
||||
&self,
|
||||
|
@ -119,25 +133,30 @@ pub trait PostManager {
|
|||
#[allow(unused)]
|
||||
async fn get_post_metadata(
|
||||
&self,
|
||||
name: &str,
|
||||
name: Arc<str>,
|
||||
query: &IndexMap<String, Value>,
|
||||
) -> Result<PostMetadata, PostError> {
|
||||
match self.get_post(name, query).await? {
|
||||
ReturnedPost::Rendered(metadata, ..) => Ok(metadata),
|
||||
ReturnedPost::Raw(..) => Err(PostError::NotFound(name.to_string())),
|
||||
match self.get_post(name.clone(), query).await? {
|
||||
ReturnedPost::Rendered { meta, .. } => Ok(meta),
|
||||
ReturnedPost::Raw { .. } => Err(PostError::NotFound(name)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_post(
|
||||
&self,
|
||||
name: &str,
|
||||
name: Arc<str>,
|
||||
query: &IndexMap<String, Value>,
|
||||
) -> Result<ReturnedPost, PostError>;
|
||||
|
||||
async fn cleanup(&self) {}
|
||||
|
||||
#[allow(unused)]
|
||||
async fn as_raw(&self, name: &str) -> Result<Option<String>, PostError> {
|
||||
Ok(None)
|
||||
fn is_raw(&self, name: &str) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
fn as_raw(&self, name: &str) -> Option<String> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue