114 lines
3.6 KiB
Rust
114 lines
3.6 KiB
Rust
//! 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)
|
|
}
|