move the rest of markdown-related stuff into it's own file
This commit is contained in:
parent
897e1cbf88
commit
cf102126b3
5 changed files with 128 additions and 92 deletions
|
@ -38,7 +38,8 @@ the default configuration with comments looks like this
|
|||
```toml
|
||||
title = "bingus-blog" # title of the website
|
||||
description = "blazingly fast markdown blog software written in rust memory safe" # description of the website
|
||||
raw_access = true # allow users to see the raw markdown of a post
|
||||
markdown_access = true # allow users to see the raw markdown of a post
|
||||
# endpoint: /posts/<name>.md
|
||||
|
||||
[rss]
|
||||
enable = false # serve an rss field under /feed.xml
|
||||
|
|
36
src/app.rs
36
src/app.rs
|
@ -3,13 +3,13 @@ use std::time::Duration;
|
|||
|
||||
use askama_axum::Template;
|
||||
use axum::extract::{Path, Query, State};
|
||||
use axum::http::header::CONTENT_TYPE;
|
||||
use axum::http::{header, Request};
|
||||
use axum::response::{IntoResponse, Redirect, Response};
|
||||
use axum::routing::get;
|
||||
use axum::{Json, Router};
|
||||
use rss::{Category, ChannelBuilder, ItemBuilder};
|
||||
use serde::Deserialize;
|
||||
use tokio::io::AsyncReadExt;
|
||||
use tower_http::services::ServeDir;
|
||||
use tower_http::trace::TraceLayer;
|
||||
use tracing::{info, info_span, Span};
|
||||
|
@ -17,7 +17,7 @@ use tracing::{info, info_span, Span};
|
|||
use crate::config::Config;
|
||||
use crate::error::{AppError, AppResult};
|
||||
use crate::filters;
|
||||
use crate::post::{MarkdownPosts, PostManager, PostMetadata, RenderStats};
|
||||
use crate::post::{MarkdownPosts, PostManager, PostMetadata, RenderStats, ReturnedPost};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
|
@ -138,26 +138,20 @@ async fn post(
|
|||
State(AppState { config, posts }): State<AppState>,
|
||||
Path(name): Path<String>,
|
||||
) -> AppResult<Response> {
|
||||
if name.ends_with(".md") && config.raw_access {
|
||||
let mut file = tokio::fs::OpenOptions::new()
|
||||
.read(true)
|
||||
.open(config.dirs.posts.join(&name))
|
||||
.await?;
|
||||
match posts.get_post(&name).await? {
|
||||
ReturnedPost::Rendered(meta, rendered, rendered_in) => {
|
||||
let page = PostTemplate {
|
||||
meta,
|
||||
rendered,
|
||||
rendered_in,
|
||||
markdown_access: config.markdown_access,
|
||||
};
|
||||
|
||||
let mut buf = Vec::new();
|
||||
file.read_to_end(&mut buf).await?;
|
||||
|
||||
Ok(([("content-type", "text/plain")], buf).into_response())
|
||||
} else {
|
||||
let post = posts.get_post(&name).await?;
|
||||
let page = PostTemplate {
|
||||
meta: post.0,
|
||||
rendered: post.1,
|
||||
rendered_in: post.2,
|
||||
markdown_access: config.raw_access,
|
||||
};
|
||||
|
||||
Ok(page.into_response())
|
||||
Ok(page.into_response())
|
||||
}
|
||||
ReturnedPost::Raw(body, content_type) => {
|
||||
Ok(([(CONTENT_TYPE, content_type)], body).into_response())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -64,7 +64,7 @@ pub struct RssConfig {
|
|||
pub struct Config {
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
pub raw_access: bool,
|
||||
pub markdown_access: bool,
|
||||
pub num_posts: usize,
|
||||
pub rss: RssConfig,
|
||||
pub dirs: DirsConfig,
|
||||
|
@ -78,7 +78,7 @@ impl Default for Config {
|
|||
Self {
|
||||
title: "bingus-blog".into(),
|
||||
description: "blazingly fast markdown blog software written in rust memory safe".into(),
|
||||
raw_access: true,
|
||||
markdown_access: true,
|
||||
num_posts: 5,
|
||||
// i have a love-hate relationship with serde
|
||||
// it was engimatic at first, but then i started actually using it
|
||||
|
|
|
@ -1,11 +1,16 @@
|
|||
use std::collections::BTreeSet;
|
||||
use std::io::{self, Write};
|
||||
use std::ops::Deref;
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
use std::time::SystemTime;
|
||||
|
||||
use axum::http::HeaderValue;
|
||||
use chrono::{DateTime, Utc};
|
||||
use color_eyre::eyre::{self, Context};
|
||||
use fronma::parser::{parse, ParsedData};
|
||||
use serde::Deserialize;
|
||||
use tokio::fs;
|
||||
use tokio::io::AsyncReadExt;
|
||||
use tracing::{error, info, warn};
|
||||
|
@ -13,9 +18,40 @@ use tracing::{error, info, warn};
|
|||
use crate::config::Config;
|
||||
use crate::markdown_render::render;
|
||||
use crate::post::cache::{load_cache, Cache, CACHE_VERSION};
|
||||
use crate::post::{FrontMatter, PostError, PostManager, PostMetadata, RenderStats};
|
||||
use crate::post::{PostError, PostManager, PostMetadata, RenderStats, ReturnedPost};
|
||||
use crate::systemtime_as_secs::as_secs;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct FrontMatter {
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
pub author: String,
|
||||
pub icon: Option<String>,
|
||||
pub created_at: Option<DateTime<Utc>>,
|
||||
pub modified_at: Option<DateTime<Utc>>,
|
||||
#[serde(default)]
|
||||
pub tags: BTreeSet<String>,
|
||||
}
|
||||
|
||||
impl FrontMatter {
|
||||
pub fn into_full(
|
||||
self,
|
||||
name: String,
|
||||
created: Option<SystemTime>,
|
||||
modified: Option<SystemTime>,
|
||||
) -> PostMetadata {
|
||||
PostMetadata {
|
||||
name,
|
||||
title: self.title,
|
||||
description: self.description,
|
||||
author: self.author,
|
||||
icon: self.icon,
|
||||
created_at: self.created_at.or_else(|| created.map(|t| t.into())),
|
||||
modified_at: self.modified_at.or_else(|| modified.map(|t| t.into())),
|
||||
tags: self.tags.into_iter().collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
pub struct MarkdownPosts<C>
|
||||
where
|
||||
C: Deref<Target = Config>,
|
||||
|
@ -219,8 +255,10 @@ where
|
|||
.to_string();
|
||||
|
||||
let post = self.get_post(&name).await?;
|
||||
if filter(&post.0, &post.1) {
|
||||
posts.push(post);
|
||||
if let ReturnedPost::Rendered(meta, content, stats) = post
|
||||
&& filter(&meta, &content)
|
||||
{
|
||||
posts.push((meta, content, stats));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -228,39 +266,66 @@ where
|
|||
Ok(posts)
|
||||
}
|
||||
|
||||
async fn get_post(&self, name: &str) -> Result<(PostMetadata, String, RenderStats), PostError> {
|
||||
let start = Instant::now();
|
||||
let path = self.config.dirs.posts.join(name.to_owned() + ".md");
|
||||
async fn get_post(&self, name: &str) -> Result<ReturnedPost, PostError> {
|
||||
if self.config.markdown_access && name.ends_with(".md") {
|
||||
let path = self.config.dirs.posts.join(name);
|
||||
|
||||
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.as_ref() {
|
||||
cache.remove(name).await;
|
||||
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.as_ref() {
|
||||
cache.remove(name).await;
|
||||
}
|
||||
return Err(PostError::NotFound(name.to_string()));
|
||||
}
|
||||
return Err(PostError::NotFound(name.to_string()));
|
||||
}
|
||||
_ => return Err(PostError::IoError(err)),
|
||||
},
|
||||
};
|
||||
let mtime = as_secs(&stat.modified()?);
|
||||
_ => return Err(PostError::IoError(err)),
|
||||
},
|
||||
};
|
||||
|
||||
if let Some(cache) = self.cache.as_ref()
|
||||
&& let Some(hit) = cache.lookup(name, mtime, &self.config.render).await
|
||||
{
|
||||
Ok((
|
||||
hit.metadata,
|
||||
hit.rendered,
|
||||
RenderStats::Cached(start.elapsed()),
|
||||
let mut buf = Vec::with_capacity(4096);
|
||||
|
||||
file.read_to_end(&mut buf).await?;
|
||||
|
||||
Ok(ReturnedPost::Raw(
|
||||
buf,
|
||||
HeaderValue::from_static("text/plain"),
|
||||
))
|
||||
} else {
|
||||
let (metadata, rendered, stats) = self.parse_and_render(name.to_string(), path).await?;
|
||||
Ok((
|
||||
metadata,
|
||||
rendered,
|
||||
RenderStats::ParsedAndRendered(start.elapsed(), stats.0, stats.1),
|
||||
))
|
||||
let start = Instant::now();
|
||||
let path = self.config.dirs.posts.join(name.to_owned() + ".md");
|
||||
|
||||
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.as_ref() {
|
||||
cache.remove(name).await;
|
||||
}
|
||||
return Err(PostError::NotFound(name.to_string()));
|
||||
}
|
||||
_ => return Err(PostError::IoError(err)),
|
||||
},
|
||||
};
|
||||
let mtime = as_secs(&stat.modified()?);
|
||||
|
||||
if let Some(cache) = self.cache.as_ref()
|
||||
&& let Some(hit) = cache.lookup(name, mtime, &self.config.render).await
|
||||
{
|
||||
Ok(ReturnedPost::Rendered(
|
||||
hit.metadata,
|
||||
hit.rendered,
|
||||
RenderStats::Cached(start.elapsed()),
|
||||
))
|
||||
} else {
|
||||
let (metadata, rendered, stats) =
|
||||
self.parse_and_render(name.to_string(), path).await?;
|
||||
Ok(ReturnedPost::Rendered(
|
||||
metadata,
|
||||
rendered,
|
||||
RenderStats::ParsedAndRendered(start.elapsed(), stats.0, stats.1),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,47 +1,15 @@
|
|||
pub mod cache;
|
||||
pub mod markdown_posts;
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
use std::time::{Duration, SystemTime};
|
||||
use std::time::Duration;
|
||||
|
||||
use axum::http::HeaderValue;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::error::PostError;
|
||||
pub use crate::post::markdown_posts::MarkdownPosts;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct FrontMatter {
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
pub author: String,
|
||||
pub icon: Option<String>,
|
||||
pub created_at: Option<DateTime<Utc>>,
|
||||
pub modified_at: Option<DateTime<Utc>>,
|
||||
#[serde(default)]
|
||||
pub tags: BTreeSet<String>,
|
||||
}
|
||||
|
||||
impl FrontMatter {
|
||||
pub fn into_full(
|
||||
self,
|
||||
name: String,
|
||||
created: Option<SystemTime>,
|
||||
modified: Option<SystemTime>,
|
||||
) -> PostMetadata {
|
||||
PostMetadata {
|
||||
name,
|
||||
title: self.title,
|
||||
description: self.description,
|
||||
author: self.author,
|
||||
icon: self.icon,
|
||||
created_at: self.created_at.or_else(|| created.map(|t| t.into())),
|
||||
modified_at: self.modified_at.or_else(|| modified.map(|t| t.into())),
|
||||
tags: self.tags.into_iter().collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct PostMetadata {
|
||||
pub name: String,
|
||||
|
@ -54,13 +22,18 @@ pub struct PostMetadata {
|
|||
pub tags: Vec<String>,
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub enum RenderStats {
|
||||
Cached(Duration),
|
||||
// format: Total, Parsed in, Rendered in
|
||||
ParsedAndRendered(Duration, Duration, Duration),
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)] // Raw will be returned very rarely
|
||||
pub enum ReturnedPost {
|
||||
Rendered(PostMetadata, String, RenderStats),
|
||||
Raw(Vec<u8>, HeaderValue),
|
||||
}
|
||||
|
||||
pub trait PostManager {
|
||||
async fn get_all_post_metadata(
|
||||
&self,
|
||||
|
@ -97,10 +70,13 @@ pub trait PostManager {
|
|||
|
||||
#[allow(unused)]
|
||||
async fn get_post_metadata(&self, name: &str) -> Result<PostMetadata, PostError> {
|
||||
self.get_post(name).await.map(|(meta, ..)| meta)
|
||||
match self.get_post(name).await? {
|
||||
ReturnedPost::Rendered(metadata, ..) => Ok(metadata),
|
||||
ReturnedPost::Raw(..) => Err(PostError::NotFound(name.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_post(&self, name: &str) -> Result<(PostMetadata, String, RenderStats), PostError>;
|
||||
async fn get_post(&self, name: &str) -> Result<ReturnedPost, PostError>;
|
||||
|
||||
async fn cleanup(&self);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue