split into posts
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
//! The in-memory posts cache: rebuilt from disk at startup and after every
|
||||
//! mutation, plus prev/next neighbour lookup over the visible set.
|
||||
|
||||
use tokio::fs;
|
||||
use tracing::warn;
|
||||
|
||||
use crate::models::{PostInfo, PostMeta, PostNeighbor};
|
||||
use crate::post::images::{cover_from, dim_for_url, extract_images};
|
||||
use crate::post::parse::{excerpt_from, parse_post, reading_time};
|
||||
use crate::{AppState, CachedPost};
|
||||
|
||||
fn build_post_info(slug: &str, meta: &PostMeta, body: &str) -> PostInfo {
|
||||
let images = extract_images(body);
|
||||
PostInfo {
|
||||
slug: slug.to_string(),
|
||||
date: meta.date,
|
||||
title: meta.title.clone(),
|
||||
summary: meta.summary.clone(),
|
||||
tags: meta.tags.clone(),
|
||||
draft: meta.draft,
|
||||
reading_time: reading_time(body),
|
||||
excerpt: excerpt_from(meta, body),
|
||||
cover_image: cover_from(&images),
|
||||
image_count: images.len() as u32,
|
||||
}
|
||||
}
|
||||
|
||||
/// Scans the posts directory and replaces the in-memory cache.
|
||||
/// Called at startup and after any mutation (create/rename/delete).
|
||||
pub(crate) async fn rebuild_posts_cache(state: &AppState) {
|
||||
let posts_dir = state.data_dir.join("posts");
|
||||
let mut posts: Vec<CachedPost> = Vec::new();
|
||||
|
||||
let mut rd = match fs::read_dir(&posts_dir).await {
|
||||
Ok(rd) => rd,
|
||||
Err(_) => {
|
||||
*state.posts_cache.write().await = posts;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
loop {
|
||||
match rd.next_entry().await {
|
||||
Ok(Some(entry)) => {
|
||||
let path = entry.path();
|
||||
if path.extension().and_then(|e| e.to_str()) != Some("md") {
|
||||
continue;
|
||||
}
|
||||
let Some(slug) = path.file_stem().and_then(|s| s.to_str()) else {
|
||||
continue;
|
||||
};
|
||||
if slug.starts_with('.') {
|
||||
continue;
|
||||
}
|
||||
let Ok(raw) = fs::read_to_string(&path).await else {
|
||||
continue;
|
||||
};
|
||||
let Ok((meta, body)) = parse_post(&raw) else {
|
||||
warn!("Skipping post with bad frontmatter: {}", slug);
|
||||
continue;
|
||||
};
|
||||
let mut info = build_post_info(slug, &meta, &body);
|
||||
if let Some(cover) = info.cover_image.as_mut() {
|
||||
if let Some(d) = dim_for_url(state, &cover.url).await {
|
||||
cover.w = Some(d.w);
|
||||
cover.h = Some(d.h);
|
||||
}
|
||||
}
|
||||
posts.push(CachedPost { info, body });
|
||||
}
|
||||
Ok(None) => break,
|
||||
Err(e) => {
|
||||
warn!("Error iterating posts dir: {}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
posts.sort_by(|a, b| {
|
||||
b.info
|
||||
.date
|
||||
.cmp(&a.info.date)
|
||||
.then_with(|| a.info.slug.cmp(&b.info.slug))
|
||||
});
|
||||
*state.posts_cache.write().await = posts;
|
||||
}
|
||||
|
||||
pub(crate) async fn neighbors_from_cache(
|
||||
state: &AppState,
|
||||
slug: &str,
|
||||
admin: bool,
|
||||
) -> (Option<PostNeighbor>, Option<PostNeighbor>) {
|
||||
let cache = state.posts_cache.read().await;
|
||||
let visible: Vec<&PostInfo> = cache
|
||||
.iter()
|
||||
.filter(|p| admin || !p.info.draft)
|
||||
.map(|p| &p.info)
|
||||
.collect();
|
||||
let Some(i) = visible.iter().position(|p| p.slug == slug) else {
|
||||
return (None, None);
|
||||
};
|
||||
let to_neighbor = |p: &PostInfo| PostNeighbor {
|
||||
slug: p.slug.clone(),
|
||||
title: p.title.clone(),
|
||||
};
|
||||
let prev = if i > 0 {
|
||||
Some(to_neighbor(visible[i - 1]))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let next = visible.get(i + 1).map(|p| to_neighbor(p));
|
||||
(prev, next)
|
||||
}
|
||||
Reference in New Issue
Block a user