add tags and cache versioning

This commit is contained in:
slonkazoid 2024-05-01 23:12:52 +03:00
parent 37c344b53c
commit d466f531eb
Signed by: slonk
SSH key fingerprint: SHA256:tbZfJX4IOvZ0LGWOWu5Ijo8jfMPi78TU7x1VoEeCIjM
8 changed files with 91 additions and 25 deletions

View file

@ -1,7 +1,9 @@
use std::time::Duration; use std::{collections::HashMap, time::Duration};
use chrono::{DateTime, TimeZone}; use chrono::{DateTime, TimeZone};
use crate::post::PostMetadata;
pub fn date<T: TimeZone>(date: &DateTime<T>) -> Result<String, askama::Error> { pub fn date<T: TimeZone>(date: &DateTime<T>) -> Result<String, askama::Error> {
Ok(date.to_rfc3339_opts(chrono::SecondsFormat::Secs, true)) Ok(date.to_rfc3339_opts(chrono::SecondsFormat::Secs, true))
} }
@ -9,3 +11,24 @@ pub fn date<T: TimeZone>(date: &DateTime<T>) -> Result<String, askama::Error> {
pub fn duration(duration: &&Duration) -> Result<String, askama::Error> { pub fn duration(duration: &&Duration) -> Result<String, askama::Error> {
Ok(format!("{:?}", duration)) Ok(format!("{:?}", duration))
} }
pub fn collect_tags(posts: &Vec<PostMetadata>) -> Result<Vec<(String, u64)>, askama::Error> {
let mut tags = HashMap::new();
for post in posts {
for tag in &post.tags {
if let Some((existing_tag, count)) = tags.remove_entry(tag) {
tags.insert(existing_tag, count + 1);
} else {
tags.insert(tag.clone(), 1);
}
}
}
let mut tags: Vec<(String, u64)> = tags.into_iter().collect();
tags.sort_unstable_by_key(|(v, _)| v.clone());
tags.sort_by_key(|(_, v)| -(*v as i64));
Ok(tags)
}

View file

@ -37,6 +37,7 @@ use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilte
use crate::config::Config; use crate::config::Config;
use crate::error::{AppResult, PostError}; use crate::error::{AppResult, PostError};
use crate::post::cache::{Cache, CACHE_VERSION};
use crate::post::{PostManager, PostMetadata, RenderStats}; use crate::post::{PostManager, PostMetadata, RenderStats};
type ArcState = Arc<AppState>; type ArcState = Arc<AppState>;
@ -179,8 +180,13 @@ async fn main() -> eyre::Result<()> {
.context("failed to read cache file")?; .context("failed to read cache file")?;
buf buf
}; };
let cache = let mut cache: Cache =
bitcode::deserialize(serialized.as_slice()).context("failed to parse cache")?; bitcode::deserialize(serialized.as_slice()).context("failed to parse cache")?;
if cache.version() < CACHE_VERSION {
warn!("cache version changed, clearing cache");
cache = Cache::default();
};
Ok::<PostManager, color_eyre::Report>(PostManager::new_with_cache( Ok::<PostManager, color_eyre::Report>(PostManager::new_with_cache(
config.dirs.posts.clone(), config.dirs.posts.clone(),
config.render.clone(), config.render.clone(),

View file

@ -7,6 +7,9 @@ use tracing::{debug, instrument};
use crate::config::RenderConfig; use crate::config::RenderConfig;
use crate::post::PostMetadata; use crate::post::PostMetadata;
/// do not persist cache if this version number changed
pub const CACHE_VERSION: u16 = 1;
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone)]
pub struct CacheValue { pub struct CacheValue {
pub metadata: PostMetadata, pub metadata: PostMetadata,
@ -15,8 +18,14 @@ pub struct CacheValue {
config_hash: u64, config_hash: u64,
} }
#[derive(Serialize, Deserialize, Default, Clone)] #[derive(Serialize, Deserialize, Clone)]
pub struct Cache(HashMap<String, CacheValue>); pub struct Cache(HashMap<String, CacheValue>, u16);
impl Default for Cache {
fn default() -> Self {
Self(Default::default(), CACHE_VERSION)
}
}
impl Cache { impl Cache {
pub async fn lookup( pub async fn lookup(
@ -117,4 +126,9 @@ impl Cache {
let new_size = self.0.len(); let new_size = self.0.len();
debug!("removed {i} entries ({old_size} -> {new_size} entries)"); debug!("removed {i} entries ({old_size} -> {new_size} entries)");
} }
#[inline(always)]
pub fn version(&self) -> u16 {
self.1
}
} }

View file

@ -1,5 +1,6 @@
mod cache; pub mod cache;
use std::collections::BTreeSet;
use std::io; use std::io;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::time::{Duration, Instant, SystemTime}; use std::time::{Duration, Instant, SystemTime};
@ -27,7 +28,7 @@ struct FrontMatter {
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: Vec<String>, pub tags: BTreeSet<String>,
} }
impl FrontMatter { impl FrontMatter {
@ -45,7 +46,7 @@ impl FrontMatter {
icon: self.icon, icon: self.icon,
created_at: self.created_at.or_else(|| created.map(|t| t.into())), created_at: self.created_at.or_else(|| created.map(|t| t.into())),
modified_at: self.modified_at.or_else(|| modified.map(|t| t.into())), modified_at: self.modified_at.or_else(|| modified.map(|t| t.into())),
tags: self.tags, tags: self.tags.into_iter().collect(),
} }
} }
} }

View file

@ -30,8 +30,7 @@
:root, :root,
code { code {
/* please have one at least one good monospace font */ /* please have one at least one good monospace font */
font-family: "Hack", "Hack Nerd Font", "JetBrains Mono", font-family: "Hack", "Hack Nerd Font", "JetBrains Mono", "JetBrainsMono Nerd Font", "Ubuntu Mono", monospace, sans-serif;
"JetBrainsMono Nerd Font", "Ubuntu Mono", monospace, sans-serif;
} }
:root { :root {
@ -81,6 +80,10 @@ footer {
opacity: 0.65; opacity: 0.65;
} }
div.post {
margin-bottom: 1em;
}
/* BEGIN cool effect everyone liked */ /* BEGIN cool effect everyone liked */
body { body {

View file

@ -1,3 +1,4 @@
{%- import "macros.askama" as macros -%}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
@ -16,21 +17,24 @@
<h2>posts</h2> <h2>posts</h2>
<!-- prettier-ignore --> <!-- prettier-ignore -->
<div> <div>
{% for post in posts %} {% for post in posts %}
<p> <div class="post">
<a href="/posts/{{ post.name }}"><b>{{ post.title }}</b></a> <a href="/posts/{{ post.name }}"><b>{{ post.title }}</b></a>
<span class="post-author">- by {{ post.author }}</span> <span class="post-author">- by {{ post.author }}</span>
<br /> <br />
{{ post.description }}<br /> {{ post.description }}<br />
{% match post.created_at %} {% when Some(created_at) %} {% call macros::table(post) %}
written:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {{ created_at|date }}<br /> </div>
{% when None %} {% endmatch %} {% endfor %}
{% match post.modified_at %} {% when Some(modified_at) %}
last modified: {{ modified_at|date }}
{% when None %} {% endmatch %}
</p>
{% endfor %}
</div> </div>
{% let tags = posts|collect_tags %}<!-- prettier-br -->
{% if !tags.is_empty() %}
<h2>tags</h2>
{% endif %}<!-- prettier-br -->
{% for tag in tags %}
<a href="/?tag={{ tag.0 }}" title="view all posts with this tag">{{ tag.0 }}</a>
<span class="post-author">- {{ tag.1 }} post{% if tag.1 != 1 %}s{%endif %}</span><br />
{% endfor %}
</main> </main>
</body> </body>
</html> </html>

19
templates/macros.askama Normal file
View file

@ -0,0 +1,19 @@
{% macro table(post) %}
{% match post.created_at %}
{% when Some(created_at) %}
written:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {{ created_at|date }}<br />
{% when None %}
{% endmatch %}
{% match post.modified_at %}
{% when Some(modified_at) %}
last modified: {{ modified_at|date }}<br />
{% when None %}
{% endmatch %}
{% if !post.tags.is_empty() %}
tags:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
{% for tag in post.tags %}
<a href="/?tag={{ tag }}" title="view all posts with this tag">{{ tag }}</a>
{% endfor %}<br />
{% endif %}
{% endmacro %}

View file

@ -1,3 +1,4 @@
{%- import "macros.askama" as macros -%}
<h1 class="post-title"> <h1 class="post-title">
{{ meta.title }} {{ meta.title }}
<span class="post-author">- by {{ meta.author }}</span> <span class="post-author">- by {{ meta.author }}</span>
@ -6,12 +7,7 @@
<p> <p>
<!-- prettier-ignore --> <!-- prettier-ignore -->
<div> <div>
{% match meta.created_at %} {% when Some(created_at) %} {% call macros::table(meta) %}
written:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {{ created_at|date }}<br />
{% when None %} {% endmatch %}
{% match meta.modified_at %} {% when Some(modified_at) %}
last modified: {{ modified_at|date }}
{% when None %} {% endmatch %}
</div> </div>
<a href="/posts/{{ meta.name }}">link</a><br /> <a href="/posts/{{ meta.name }}">link</a><br />
<a href="/">back to home</a> <a href="/">back to home</a>