decrease performance
This commit is contained in:
parent
c44b1a082e
commit
6b5c0beeaa
6 changed files with 220 additions and 172 deletions
6
Cargo.lock
generated
6
Cargo.lock
generated
|
@ -1,6 +1,6 @@
|
||||||
# This file is automatically @generated by Cargo.
|
# This file is automatically @generated by Cargo.
|
||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 3
|
version = 4
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "addr2line"
|
name = "addr2line"
|
||||||
|
@ -119,9 +119,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-trait"
|
name = "async-trait"
|
||||||
version = "0.1.81"
|
version = "0.1.83"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107"
|
checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|
30
src/app.rs
30
src/app.rs
|
@ -21,7 +21,7 @@ use tracing::{info, info_span, Span};
|
||||||
|
|
||||||
use crate::config::{Config, StyleConfig};
|
use crate::config::{Config, StyleConfig};
|
||||||
use crate::error::{AppError, AppResult};
|
use crate::error::{AppError, AppResult};
|
||||||
use crate::post::{MarkdownPosts, PostManager, PostMetadata, RenderStats, ReturnedPost};
|
use crate::post::{Filter, PostManager, PostMetadata, RenderStats, ReturnedPost};
|
||||||
use crate::serve_dir_included::handle;
|
use crate::serve_dir_included::handle;
|
||||||
|
|
||||||
const STATIC: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/static");
|
const STATIC: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/static");
|
||||||
|
@ -43,8 +43,8 @@ const BINGUS_INFO: BingusInfo = BingusInfo {
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
pub config: Arc<Config>,
|
pub config: Arc<Config>,
|
||||||
pub posts: Arc<MarkdownPosts<Arc<Config>>>,
|
pub posts: Arc<dyn PostManager + Send + Sync>,
|
||||||
pub reg: Arc<RwLock<Handlebars<'static>>>,
|
pub templates: Arc<RwLock<Handlebars<'static>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
|
@ -120,14 +120,17 @@ fn join_tags_for_meta(tags: &Map<String, serde_json::Value>, delim: &str) -> Str
|
||||||
s
|
s
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn index<'a>(
|
async fn index(
|
||||||
State(AppState {
|
State(AppState {
|
||||||
config, posts, reg, ..
|
config,
|
||||||
|
posts,
|
||||||
|
templates: reg,
|
||||||
|
..
|
||||||
}): State<AppState>,
|
}): State<AppState>,
|
||||||
Query(query): Query<QueryParams>,
|
Query(query): Query<QueryParams>,
|
||||||
) -> AppResult<impl IntoResponse> {
|
) -> AppResult<impl IntoResponse> {
|
||||||
let posts = posts
|
let posts = posts
|
||||||
.get_max_n_post_metadata_with_optional_tag_sorted(query.num_posts, query.tag.as_ref())
|
.get_max_n_post_metadata_with_optional_tag_sorted(query.num_posts, query.tag.as_deref())
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let tags = collect_tags(&posts);
|
let tags = collect_tags(&posts);
|
||||||
|
@ -157,7 +160,7 @@ async fn all_posts(
|
||||||
Query(query): Query<QueryParams>,
|
Query(query): Query<QueryParams>,
|
||||||
) -> AppResult<Json<Vec<PostMetadata>>> {
|
) -> AppResult<Json<Vec<PostMetadata>>> {
|
||||||
let posts = posts
|
let posts = posts
|
||||||
.get_max_n_post_metadata_with_optional_tag_sorted(query.num_posts, query.tag.as_ref())
|
.get_max_n_post_metadata_with_optional_tag_sorted(query.num_posts, query.tag.as_deref())
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(Json(posts))
|
Ok(Json(posts))
|
||||||
|
@ -172,11 +175,13 @@ async fn rss(
|
||||||
}
|
}
|
||||||
|
|
||||||
let posts = posts
|
let posts = posts
|
||||||
.get_all_posts(|metadata, _| {
|
.get_all_posts(
|
||||||
query
|
query
|
||||||
.tag
|
.tag
|
||||||
.as_ref().is_none_or(|tag| metadata.tags.contains(tag))
|
.as_ref()
|
||||||
})
|
.and(Some(Filter::Tags(query.tag.as_deref().as_slice())))
|
||||||
|
.as_slice(),
|
||||||
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let mut channel = ChannelBuilder::default();
|
let mut channel = ChannelBuilder::default();
|
||||||
|
@ -223,7 +228,10 @@ async fn rss(
|
||||||
|
|
||||||
async fn post(
|
async fn post(
|
||||||
State(AppState {
|
State(AppState {
|
||||||
config, posts, reg, ..
|
config,
|
||||||
|
posts,
|
||||||
|
templates: reg,
|
||||||
|
..
|
||||||
}): State<AppState>,
|
}): State<AppState>,
|
||||||
Path(name): Path<String>,
|
Path(name): Path<String>,
|
||||||
) -> AppResult<impl IntoResponse> {
|
) -> AppResult<impl IntoResponse> {
|
||||||
|
|
31
src/main.rs
31
src/main.rs
|
@ -31,6 +31,7 @@ use tracing_subscriber::layer::SubscriberExt;
|
||||||
use tracing_subscriber::{util::SubscriberInitExt, EnvFilter};
|
use tracing_subscriber::{util::SubscriberInitExt, EnvFilter};
|
||||||
|
|
||||||
use crate::app::AppState;
|
use crate::app::AppState;
|
||||||
|
use crate::post::cache::{load_cache, CacheGuard, CACHE_VERSION};
|
||||||
use crate::post::{MarkdownPosts, PostManager};
|
use crate::post::{MarkdownPosts, PostManager};
|
||||||
use crate::templates::new_registry;
|
use crate::templates::new_registry;
|
||||||
use crate::templates::watcher::watch_templates;
|
use crate::templates::watcher::watch_templates;
|
||||||
|
@ -87,7 +88,31 @@ async fn main() -> eyre::Result<()> {
|
||||||
.instrument(info_span!("custom_template_watcher")),
|
.instrument(info_span!("custom_template_watcher")),
|
||||||
);
|
);
|
||||||
|
|
||||||
let posts = Arc::new(MarkdownPosts::new(Arc::clone(&config)).await?);
|
let cache = if config.cache.enable {
|
||||||
|
if config.cache.persistence && tokio::fs::try_exists(&config.cache.file).await? {
|
||||||
|
info!("loading cache from file");
|
||||||
|
let mut cache = load_cache(&config.cache).await.unwrap_or_else(|err| {
|
||||||
|
error!("failed to load cache: {}", err);
|
||||||
|
info!("using empty cache");
|
||||||
|
Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
if cache.version() < CACHE_VERSION {
|
||||||
|
warn!("cache version changed, clearing cache");
|
||||||
|
cache = Default::default();
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(cache)
|
||||||
|
} else {
|
||||||
|
Some(Default::default())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
.map(|cache| CacheGuard::new(cache, config.cache.clone()))
|
||||||
|
.map(Arc::new);
|
||||||
|
|
||||||
|
let posts = Arc::new(MarkdownPosts::new(Arc::clone(&config), cache.clone()).await?);
|
||||||
|
|
||||||
if config.cache.enable && config.cache.cleanup {
|
if config.cache.enable && config.cache.cleanup {
|
||||||
if let Some(millis) = config.cache.cleanup_interval {
|
if let Some(millis) = config.cache.cleanup_interval {
|
||||||
|
@ -112,8 +137,8 @@ async fn main() -> eyre::Result<()> {
|
||||||
|
|
||||||
let state = AppState {
|
let state = AppState {
|
||||||
config: Arc::clone(&config),
|
config: Arc::clone(&config),
|
||||||
posts: Arc::clone(&posts),
|
posts: posts as Arc<dyn PostManager + Send + Sync>,
|
||||||
reg: Arc::clone(®),
|
templates: Arc::clone(®),
|
||||||
};
|
};
|
||||||
let app = app::new(&config).with_state(state.clone());
|
let app = app::new(&config).with_state(state.clone());
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
use std::hash::{DefaultHasher, Hash, Hasher};
|
use std::io::{Read, Write};
|
||||||
use std::io::Read;
|
use std::ops::Deref;
|
||||||
|
|
||||||
use crate::config::{Config, RenderConfig};
|
use crate::config::CacheConfig;
|
||||||
use crate::post::PostMetadata;
|
use crate::post::PostMetadata;
|
||||||
use color_eyre::eyre::{self, Context};
|
use color_eyre::eyre::{self, Context};
|
||||||
use scc::HashMap;
|
use scc::HashMap;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tokio::io::AsyncReadExt;
|
use tokio::io::AsyncReadExt;
|
||||||
use tracing::{debug, 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 = 2;
|
||||||
|
@ -17,34 +17,24 @@ pub struct CacheValue {
|
||||||
pub metadata: PostMetadata,
|
pub metadata: PostMetadata,
|
||||||
pub rendered: String,
|
pub rendered: String,
|
||||||
pub mtime: u64,
|
pub mtime: u64,
|
||||||
config_hash: u64,
|
extra: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
pub struct Cache(HashMap<String, CacheValue>, u16);
|
pub struct FileCache(HashMap<String, CacheValue>, u16);
|
||||||
|
|
||||||
impl Default for Cache {
|
impl Default for FileCache {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self(Default::default(), CACHE_VERSION)
|
Self(Default::default(), CACHE_VERSION)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Cache {
|
impl FileCache {
|
||||||
pub async fn lookup(
|
pub async fn lookup(&self, name: &str, mtime: u64, extra: u64) -> Option<CacheValue> {
|
||||||
&self,
|
|
||||||
name: &str,
|
|
||||||
mtime: u64,
|
|
||||||
config: &RenderConfig,
|
|
||||||
) -> Option<CacheValue> {
|
|
||||||
match self.0.get_async(name).await {
|
match self.0.get_async(name).await {
|
||||||
Some(entry) => {
|
Some(entry) => {
|
||||||
let cached = entry.get();
|
let cached = entry.get();
|
||||||
if mtime <= cached.mtime && {
|
if extra == cached.extra && mtime <= cached.mtime {
|
||||||
let mut hasher = DefaultHasher::new();
|
|
||||||
config.hash(&mut hasher);
|
|
||||||
hasher.finish()
|
|
||||||
} == cached.config_hash
|
|
||||||
{
|
|
||||||
Some(cached.clone())
|
Some(cached.clone())
|
||||||
} else {
|
} else {
|
||||||
let _ = entry.remove();
|
let _ = entry.remove();
|
||||||
|
@ -76,17 +66,13 @@ impl Cache {
|
||||||
metadata: PostMetadata,
|
metadata: PostMetadata,
|
||||||
mtime: u64,
|
mtime: u64,
|
||||||
rendered: String,
|
rendered: String,
|
||||||
config: &RenderConfig,
|
extra: u64,
|
||||||
) -> Result<(), (String, (PostMetadata, String))> {
|
) -> Result<(), (String, (PostMetadata, String))> {
|
||||||
let mut hasher = DefaultHasher::new();
|
|
||||||
config.hash(&mut hasher);
|
|
||||||
let hash = hasher.finish();
|
|
||||||
|
|
||||||
let value = CacheValue {
|
let value = CacheValue {
|
||||||
metadata,
|
metadata,
|
||||||
rendered,
|
rendered,
|
||||||
mtime,
|
mtime,
|
||||||
config_hash: hash,
|
extra,
|
||||||
};
|
};
|
||||||
|
|
||||||
if self
|
if self
|
||||||
|
@ -136,12 +122,67 @@ impl Cache {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn load_cache(config: &Config) -> Result<Cache, eyre::Report> {
|
pub struct CacheGuard {
|
||||||
let path = &config.cache.file;
|
inner: FileCache,
|
||||||
|
config: CacheConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CacheGuard {
|
||||||
|
pub fn new(cache: FileCache, config: CacheConfig) -> Self {
|
||||||
|
Self {
|
||||||
|
inner: cache,
|
||||||
|
config,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_drop(&mut self) -> Result<(), eyre::Report> {
|
||||||
|
// write cache to file
|
||||||
|
let path = &self.config.file;
|
||||||
|
let serialized = bitcode::serialize(&self.inner).context("failed to serialize cache")?;
|
||||||
|
let mut cache_file = std::fs::File::create(path)
|
||||||
|
.with_context(|| format!("failed to open cache at {}", path.display()))?;
|
||||||
|
let compression_level = self.config.compression_level;
|
||||||
|
if self.config.compress {
|
||||||
|
std::io::Write::write_all(
|
||||||
|
&mut zstd::stream::write::Encoder::new(cache_file, compression_level)?
|
||||||
|
.auto_finish(),
|
||||||
|
&serialized,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
cache_file.write_all(&serialized)
|
||||||
|
}
|
||||||
|
.context("failed to write cache to file")?;
|
||||||
|
info!("wrote cache to {}", path.display());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for CacheGuard {
|
||||||
|
type Target = FileCache;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.inner
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<FileCache> for CacheGuard {
|
||||||
|
fn as_ref(&self) -> &FileCache {
|
||||||
|
&self.inner
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for CacheGuard {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.try_drop().expect("cache to save successfully")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn load_cache(config: &CacheConfig) -> Result<FileCache, eyre::Report> {
|
||||||
|
let path = &config.file;
|
||||||
let mut cache_file = tokio::fs::File::open(&path)
|
let mut cache_file = tokio::fs::File::open(&path)
|
||||||
.await
|
.await
|
||||||
.context("failed to open cache file")?;
|
.context("failed to open cache file")?;
|
||||||
let serialized = if config.cache.compress {
|
let serialized = if config.compress {
|
||||||
let cache_file = cache_file.into_std().await;
|
let cache_file = cache_file.into_std().await;
|
||||||
tokio::task::spawn_blocking(move || {
|
tokio::task::spawn_blocking(move || {
|
||||||
let mut buf = Vec::with_capacity(4096);
|
let mut buf = Vec::with_capacity(4096);
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
use std::collections::BTreeSet;
|
use std::collections::BTreeSet;
|
||||||
use std::io::{self, Write};
|
use std::hash::{DefaultHasher, Hash, Hasher};
|
||||||
use std::ops::Deref;
|
use std::io;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
use std::time::SystemTime;
|
use std::time::SystemTime;
|
||||||
|
|
||||||
|
use axum::async_trait;
|
||||||
use axum::http::HeaderValue;
|
use axum::http::HeaderValue;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use color_eyre::eyre::{self, Context};
|
use color_eyre::eyre::{self, Context};
|
||||||
|
@ -14,14 +16,17 @@ use fronma::parser::{parse, ParsedData};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
use tokio::io::AsyncReadExt;
|
use tokio::io::AsyncReadExt;
|
||||||
use tracing::{error, info, warn};
|
use tracing::warn;
|
||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::markdown_render::{build_syntect, render};
|
use crate::markdown_render::{build_syntect, render};
|
||||||
use crate::post::cache::{load_cache, Cache, CACHE_VERSION};
|
|
||||||
use crate::post::{PostError, PostManager, PostMetadata, RenderStats, ReturnedPost};
|
|
||||||
use crate::systemtime_as_secs::as_secs;
|
use crate::systemtime_as_secs::as_secs;
|
||||||
|
|
||||||
|
use super::cache::CacheGuard;
|
||||||
|
use super::{
|
||||||
|
ApplyFilters, Filter, PostError, PostManager, PostMetadata, RenderStats, ReturnedPost,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct FrontMatter {
|
struct FrontMatter {
|
||||||
pub title: String,
|
pub title: String,
|
||||||
|
@ -57,48 +62,30 @@ impl FrontMatter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub struct MarkdownPosts<C>
|
|
||||||
where
|
pub struct MarkdownPosts {
|
||||||
C: Deref<Target = Config>,
|
cache: Option<Arc<CacheGuard>>,
|
||||||
{
|
config: Arc<Config>,
|
||||||
cache: Option<Cache>,
|
render_hash: u64,
|
||||||
config: C,
|
|
||||||
syntect: SyntectAdapter,
|
syntect: SyntectAdapter,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<C> MarkdownPosts<C>
|
impl MarkdownPosts {
|
||||||
where
|
pub async fn new(
|
||||||
C: Deref<Target = Config>,
|
config: Arc<Config>,
|
||||||
{
|
cache: Option<Arc<CacheGuard>>,
|
||||||
pub async fn new(config: C) -> eyre::Result<MarkdownPosts<C>> {
|
) -> eyre::Result<MarkdownPosts> {
|
||||||
let syntect =
|
let syntect =
|
||||||
build_syntect(&config.render).context("failed to create syntax highlighting engine")?;
|
build_syntect(&config.render).context("failed to create syntax highlighting engine")?;
|
||||||
|
|
||||||
let cache = if config.cache.enable {
|
let mut hasher = DefaultHasher::new();
|
||||||
if config.cache.persistence && tokio::fs::try_exists(&config.cache.file).await? {
|
config.render.hash(&mut hasher);
|
||||||
info!("loading cache from file");
|
let render_hash = hasher.finish();
|
||||||
let mut cache = load_cache(&config).await.unwrap_or_else(|err| {
|
|
||||||
error!("failed to load cache: {}", err);
|
|
||||||
info!("using empty cache");
|
|
||||||
Default::default()
|
|
||||||
});
|
|
||||||
|
|
||||||
if cache.version() < CACHE_VERSION {
|
|
||||||
warn!("cache version changed, clearing cache");
|
|
||||||
cache = Default::default();
|
|
||||||
};
|
|
||||||
|
|
||||||
Some(cache)
|
|
||||||
} else {
|
|
||||||
Some(Default::default())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
cache,
|
cache,
|
||||||
config,
|
config,
|
||||||
|
render_hash,
|
||||||
syntect,
|
syntect,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -131,14 +118,14 @@ where
|
||||||
let post = render(body, Some(&self.syntect));
|
let post = render(body, Some(&self.syntect));
|
||||||
let rendering = before_render.elapsed();
|
let rendering = before_render.elapsed();
|
||||||
|
|
||||||
if let Some(cache) = self.cache.as_ref() {
|
if let Some(cache) = &self.cache {
|
||||||
cache
|
cache
|
||||||
.insert(
|
.insert(
|
||||||
name.to_string(),
|
name.to_string(),
|
||||||
metadata.clone(),
|
metadata.clone(),
|
||||||
as_secs(&modified),
|
as_secs(&modified),
|
||||||
post.clone(),
|
post.clone(),
|
||||||
&self.config.render,
|
self.render_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))
|
||||||
|
@ -146,55 +133,44 @@ where
|
||||||
|
|
||||||
Ok((metadata, post, (parsing, rendering)))
|
Ok((metadata, post, (parsing, rendering)))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn cache(&self) -> Option<&Cache> {
|
#[async_trait]
|
||||||
self.cache.as_ref()
|
impl PostManager for MarkdownPosts {
|
||||||
}
|
async fn get_all_posts(
|
||||||
|
&self,
|
||||||
|
filters: &[Filter<'_>],
|
||||||
|
) -> Result<Vec<(PostMetadata, String, RenderStats)>, PostError> {
|
||||||
|
let mut posts = Vec::new();
|
||||||
|
|
||||||
fn try_drop(&mut self) -> Result<(), eyre::Report> {
|
let mut read_dir = fs::read_dir(&self.config.dirs.posts).await?;
|
||||||
// write cache to file
|
while let Some(entry) = read_dir.next_entry().await? {
|
||||||
let config = &self.config.cache;
|
let path = entry.path();
|
||||||
if config.enable
|
let stat = fs::metadata(&path).await?;
|
||||||
&& config.persistence
|
|
||||||
&& let Some(cache) = self.cache()
|
if stat.is_file() && path.extension().is_some_and(|ext| ext == "md") {
|
||||||
{
|
let name = path
|
||||||
let path = &config.file;
|
.clone()
|
||||||
let serialized = bitcode::serialize(cache).context("failed to serialize cache")?;
|
.file_stem()
|
||||||
let mut cache_file = std::fs::File::create(path)
|
.unwrap()
|
||||||
.with_context(|| format!("failed to open cache at {}", path.display()))?;
|
.to_string_lossy()
|
||||||
let compression_level = config.compression_level;
|
.to_string();
|
||||||
if config.compress {
|
|
||||||
std::io::Write::write_all(
|
let post = self.get_post(&name).await?;
|
||||||
&mut zstd::stream::write::Encoder::new(cache_file, compression_level)?
|
if let ReturnedPost::Rendered(meta, content, stats) = post
|
||||||
.auto_finish(),
|
&& meta.apply_filters(filters)
|
||||||
&serialized,
|
{
|
||||||
)
|
posts.push((meta, content, stats));
|
||||||
} else {
|
}
|
||||||
cache_file.write_all(&serialized)
|
|
||||||
}
|
}
|
||||||
.context("failed to write cache to file")?;
|
|
||||||
info!("wrote cache to {}", path.display());
|
|
||||||
}
|
}
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<C> Drop for MarkdownPosts<C>
|
Ok(posts)
|
||||||
where
|
|
||||||
C: Deref<Target = Config>,
|
|
||||||
{
|
|
||||||
fn drop(&mut self) {
|
|
||||||
self.try_drop().unwrap()
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl<C> PostManager for MarkdownPosts<C>
|
|
||||||
where
|
|
||||||
C: Deref<Target = Config>,
|
|
||||||
{
|
|
||||||
async fn get_all_post_metadata(
|
async fn get_all_post_metadata(
|
||||||
&self,
|
&self,
|
||||||
filter: impl Fn(&PostMetadata) -> bool,
|
filters: &[Filter<'_>],
|
||||||
) -> Result<Vec<PostMetadata>, PostError> {
|
) -> Result<Vec<PostMetadata>, PostError> {
|
||||||
let mut posts = Vec::new();
|
let mut posts = Vec::new();
|
||||||
|
|
||||||
|
@ -207,15 +183,15 @@ where
|
||||||
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 = String::from(path.file_stem().unwrap().to_string_lossy());
|
||||||
|
|
||||||
if let Some(cache) = self.cache.as_ref()
|
if let Some(cache) = &self.cache
|
||||||
&& let Some(hit) = cache.lookup_metadata(&name, mtime).await
|
&& let Some(hit) = cache.lookup_metadata(&name, mtime).await
|
||||||
&& filter(&hit)
|
&& hit.apply_filters(filters)
|
||||||
{
|
{
|
||||||
posts.push(hit);
|
posts.push(hit);
|
||||||
} else {
|
} else {
|
||||||
match self.parse_and_render(name, path).await {
|
match self.parse_and_render(name, path).await {
|
||||||
Ok((metadata, ..)) => {
|
Ok((metadata, ..)) => {
|
||||||
if filter(&metadata) {
|
if metadata.apply_filters(filters) {
|
||||||
posts.push(metadata);
|
posts.push(metadata);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -235,37 +211,6 @@ where
|
||||||
Ok(posts)
|
Ok(posts)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_all_posts(
|
|
||||||
&self,
|
|
||||||
filter: impl Fn(&PostMetadata, &str) -> bool,
|
|
||||||
) -> Result<Vec<(PostMetadata, String, RenderStats)>, PostError> {
|
|
||||||
let mut posts = Vec::new();
|
|
||||||
|
|
||||||
let mut read_dir = fs::read_dir(&self.config.dirs.posts).await?;
|
|
||||||
while let Some(entry) = read_dir.next_entry().await? {
|
|
||||||
let path = entry.path();
|
|
||||||
let stat = fs::metadata(&path).await?;
|
|
||||||
|
|
||||||
if stat.is_file() && path.extension().is_some_and(|ext| ext == "md") {
|
|
||||||
let name = path
|
|
||||||
.clone()
|
|
||||||
.file_stem()
|
|
||||||
.unwrap()
|
|
||||||
.to_string_lossy()
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
let post = self.get_post(&name).await?;
|
|
||||||
if let ReturnedPost::Rendered(meta, content, stats) = post
|
|
||||||
&& filter(&meta, &content)
|
|
||||||
{
|
|
||||||
posts.push((meta, content, stats));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(posts)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_post(&self, name: &str) -> Result<ReturnedPost, PostError> {
|
async fn get_post(&self, name: &str) -> Result<ReturnedPost, PostError> {
|
||||||
if self.config.markdown_access && name.ends_with(".md") {
|
if self.config.markdown_access && name.ends_with(".md") {
|
||||||
let path = self.config.dirs.posts.join(name);
|
let path = self.config.dirs.posts.join(name);
|
||||||
|
@ -274,7 +219,7 @@ where
|
||||||
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.as_ref() {
|
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.to_string()));
|
||||||
|
@ -299,7 +244,7 @@ where
|
||||||
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.as_ref() {
|
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.to_string()));
|
||||||
|
@ -309,8 +254,8 @@ where
|
||||||
};
|
};
|
||||||
let mtime = as_secs(&stat.modified()?);
|
let mtime = as_secs(&stat.modified()?);
|
||||||
|
|
||||||
if let Some(cache) = self.cache.as_ref()
|
if let Some(cache) = &self.cache
|
||||||
&& let Some(hit) = cache.lookup(name, mtime, &self.config.render).await
|
&& let Some(hit) = cache.lookup(name, mtime, self.render_hash).await
|
||||||
{
|
{
|
||||||
Ok(ReturnedPost::Rendered(
|
Ok(ReturnedPost::Rendered(
|
||||||
hit.metadata,
|
hit.metadata,
|
||||||
|
@ -330,7 +275,7 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn cleanup(&self) {
|
async fn cleanup(&self) {
|
||||||
if let Some(cache) = self.cache.as_ref() {
|
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(self.config.dirs.posts.join(name.to_owned() + ".md"))
|
||||||
|
|
|
@ -3,7 +3,7 @@ pub mod markdown_posts;
|
||||||
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use axum::http::HeaderValue;
|
use axum::{async_trait, http::HeaderValue};
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
@ -37,29 +37,58 @@ pub enum ReturnedPost {
|
||||||
Raw(Vec<u8>, HeaderValue),
|
Raw(Vec<u8>, HeaderValue),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub enum Filter<'a> {
|
||||||
|
Tags(&'a [&'a str]),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Filter<'a> {
|
||||||
|
pub fn apply(&self, meta: &PostMetadata) -> bool {
|
||||||
|
match self {
|
||||||
|
Filter::Tags(tags) => tags
|
||||||
|
.iter()
|
||||||
|
.any(|tag| meta.tags.iter().any(|meta_tag| meta_tag == tag)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait ApplyFilters {
|
||||||
|
fn apply_filters(&self, filters: &[Filter<'_>]) -> bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ApplyFilters for PostMetadata {
|
||||||
|
fn apply_filters(&self, filters: &[Filter<'_>]) -> bool {
|
||||||
|
for filter in filters {
|
||||||
|
if !filter.apply(self) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
pub trait PostManager {
|
pub trait PostManager {
|
||||||
async fn get_all_post_metadata(
|
async fn get_all_post_metadata(
|
||||||
&self,
|
&self,
|
||||||
filter: impl Fn(&PostMetadata) -> bool,
|
filters: &[Filter<'_>],
|
||||||
) -> Result<Vec<PostMetadata>, PostError> {
|
) -> Result<Vec<PostMetadata>, PostError> {
|
||||||
self.get_all_posts(|m, _| filter(m))
|
self.get_all_posts(filters)
|
||||||
.await
|
.await
|
||||||
.map(|vec| vec.into_iter().map(|(meta, ..)| meta).collect())
|
.map(|vec| vec.into_iter().map(|(meta, ..)| meta).collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_all_posts(
|
async fn get_all_posts(
|
||||||
&self,
|
&self,
|
||||||
filter: impl Fn(&PostMetadata, &str) -> bool,
|
filters: &[Filter<'_>],
|
||||||
) -> Result<Vec<(PostMetadata, String, RenderStats)>, PostError>;
|
) -> Result<Vec<(PostMetadata, String, 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,
|
||||||
n: Option<usize>,
|
n: Option<usize>,
|
||||||
tag: Option<&String>,
|
tag: Option<&str>,
|
||||||
) -> Result<Vec<PostMetadata>, PostError> {
|
) -> Result<Vec<PostMetadata>, PostError> {
|
||||||
let mut posts = self
|
let filters = tag.and(Some(Filter::Tags(tag.as_slice())));
|
||||||
.get_all_post_metadata(|metadata| tag.is_none_or(|tag| metadata.tags.contains(tag)))
|
let mut posts = self.get_all_post_metadata(filters.as_slice()).await?;
|
||||||
.await?;
|
|
||||||
// we still want some semblance of order if created_at is None so sort by mtime as well
|
// we still want some semblance of order if created_at is None so sort by mtime as well
|
||||||
posts.sort_unstable_by_key(|metadata| metadata.modified_at.unwrap_or_default());
|
posts.sort_unstable_by_key(|metadata| metadata.modified_at.unwrap_or_default());
|
||||||
posts.sort_by_key(|metadata| metadata.created_at.unwrap_or_default());
|
posts.sort_by_key(|metadata| metadata.created_at.unwrap_or_default());
|
||||||
|
|
Loading…
Reference in a new issue