forked from slonk/bingus-blog
main #1
10 changed files with 319 additions and 100 deletions
115
Cargo.lock
generated
115
Cargo.lock
generated
|
@ -128,6 +128,19 @@ dependencies = [
|
||||||
"syn 2.0.60",
|
"syn 2.0.60",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "atom_syndication"
|
||||||
|
version = "0.12.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "571832dcff775e26562e8e6930cd483de5587301d40d3a3b85d532b6383e15a7"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"derive_builder",
|
||||||
|
"diligent-date-parser",
|
||||||
|
"never",
|
||||||
|
"quick-xml 0.30.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "autocfg"
|
name = "autocfg"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
|
@ -290,6 +303,7 @@ dependencies = [
|
||||||
"comrak",
|
"comrak",
|
||||||
"console-subscriber",
|
"console-subscriber",
|
||||||
"fronma",
|
"fronma",
|
||||||
|
"rss",
|
||||||
"scc",
|
"scc",
|
||||||
"serde",
|
"serde",
|
||||||
"syntect",
|
"syntect",
|
||||||
|
@ -300,6 +314,7 @@ dependencies = [
|
||||||
"tower-http",
|
"tower-http",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
|
"url",
|
||||||
"zstd",
|
"zstd",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -576,12 +591,30 @@ version = "1.4.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "322ef0094744e63628e6f0eb2295517f79276a5b342a4c2ff3042566ca181d4e"
|
checksum = "322ef0094744e63628e6f0eb2295517f79276a5b342a4c2ff3042566ca181d4e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "diligent-date-parser"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f6cf7fe294274a222363f84bcb63cdea762979a0443b4cf1f4f8fd17c86b1182"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "either"
|
name = "either"
|
||||||
version = "1.11.0"
|
version = "1.11.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2"
|
checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "encoding_rs"
|
||||||
|
version = "0.8.34"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "entities"
|
name = "entities"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
|
@ -918,6 +951,16 @@ version = "1.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "idna"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-bidi",
|
||||||
|
"unicode-normalization",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indenter"
|
name = "indenter"
|
||||||
version = "0.3.3"
|
version = "0.3.3"
|
||||||
|
@ -1061,6 +1104,12 @@ dependencies = [
|
||||||
"windows-sys 0.48.0",
|
"windows-sys 0.48.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "never"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c96aba5aa877601bb3f6dd6a63a969e1f82e60646e81e71b14496995e9853c91"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nom"
|
name = "nom"
|
||||||
version = "7.1.3"
|
version = "7.1.3"
|
||||||
|
@ -1208,7 +1257,7 @@ dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"indexmap 2.2.6",
|
"indexmap 2.2.6",
|
||||||
"line-wrap",
|
"line-wrap",
|
||||||
"quick-xml",
|
"quick-xml 0.31.0",
|
||||||
"serde",
|
"serde",
|
||||||
"time",
|
"time",
|
||||||
]
|
]
|
||||||
|
@ -1266,6 +1315,16 @@ dependencies = [
|
||||||
"prost",
|
"prost",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quick-xml"
|
||||||
|
version = "0.30.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956"
|
||||||
|
dependencies = [
|
||||||
|
"encoding_rs",
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quick-xml"
|
name = "quick-xml"
|
||||||
version = "0.31.0"
|
version = "0.31.0"
|
||||||
|
@ -1358,6 +1417,18 @@ version = "0.8.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56"
|
checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rss"
|
||||||
|
version = "2.0.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f7b2c77eb4450d7d5f98df52c381cd6c4e19b75dad9209a9530b85a44510219a"
|
||||||
|
dependencies = [
|
||||||
|
"atom_syndication",
|
||||||
|
"derive_builder",
|
||||||
|
"never",
|
||||||
|
"quick-xml 0.30.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc-demangle"
|
name = "rustc-demangle"
|
||||||
version = "0.1.23"
|
version = "0.1.23"
|
||||||
|
@ -1652,6 +1723,21 @@ dependencies = [
|
||||||
"time-core",
|
"time-core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tinyvec"
|
||||||
|
version = "1.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
|
||||||
|
dependencies = [
|
||||||
|
"tinyvec_macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tinyvec_macros"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.37.0"
|
version = "1.37.0"
|
||||||
|
@ -1929,18 +2015,45 @@ dependencies = [
|
||||||
"version_check",
|
"version_check",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-bidi"
|
||||||
|
version = "0.3.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.12"
|
version = "1.0.12"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
|
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-normalization"
|
||||||
|
version = "0.1.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5"
|
||||||
|
dependencies = [
|
||||||
|
"tinyvec",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode_categories"
|
name = "unicode_categories"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
|
checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "url"
|
||||||
|
version = "2.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633"
|
||||||
|
dependencies = [
|
||||||
|
"form_urlencoded",
|
||||||
|
"idna",
|
||||||
|
"percent-encoding",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "valuable"
|
name = "valuable"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
|
@ -37,6 +37,7 @@ comrak = { version = "0.22.0", features = [
|
||||||
], default-features = false }
|
], default-features = false }
|
||||||
console-subscriber = { version = "0.2.0", optional = true }
|
console-subscriber = { version = "0.2.0", optional = true }
|
||||||
fronma = "0.2.0"
|
fronma = "0.2.0"
|
||||||
|
rss = "2.0.7"
|
||||||
scc = { version = "2.1.0", features = ["serde"] }
|
scc = { version = "2.1.0", features = ["serde"] }
|
||||||
serde = { version = "1.0.197", features = ["derive"] }
|
serde = { version = "1.0.197", features = ["derive"] }
|
||||||
syntect = "5.2.0"
|
syntect = "5.2.0"
|
||||||
|
@ -56,4 +57,5 @@ tower-http = { version = "0.5.2", features = [
|
||||||
], default-features = false }
|
], default-features = false }
|
||||||
tracing = "0.1.40"
|
tracing = "0.1.40"
|
||||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||||
|
url = { version = "2.5.0", features = ["serde"] }
|
||||||
zstd = { version = "0.13.1", default-features = false }
|
zstd = { version = "0.13.1", default-features = false }
|
||||||
|
|
|
@ -11,7 +11,7 @@ blazingly fast markdown blog software written in rust memory safe
|
||||||
|
|
||||||
## TODO
|
## TODO
|
||||||
|
|
||||||
- [ ] RSS
|
- [x] RSS
|
||||||
- [x] finish writing this document
|
- [x] finish writing this document
|
||||||
- [x] document config
|
- [x] document config
|
||||||
- [ ] extend syntect options
|
- [ ] extend syntect options
|
||||||
|
@ -27,6 +27,7 @@ blazingly fast markdown blog software written in rust memory safe
|
||||||
- [x] clean up imports and require less features
|
- [x] clean up imports and require less features
|
||||||
- [ ] improve home page
|
- [ ] improve home page
|
||||||
- [x] tags
|
- [x] tags
|
||||||
|
- [ ] multi-language support
|
||||||
- [x] be blazingly fast
|
- [x] be blazingly fast
|
||||||
- [x] 100+ MiB binary size
|
- [x] 100+ MiB binary size
|
||||||
|
|
||||||
|
@ -39,6 +40,11 @@ title = "bingus-blog" # title of the website
|
||||||
description = "blazingly fast markdown blog software written in rust memory safe" # description 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
|
raw_access = true # allow users to see the raw markdown of a post
|
||||||
|
|
||||||
|
[rss]
|
||||||
|
enable = false # serve an rss field under /feed.xml
|
||||||
|
# this may be a bit resource intensive
|
||||||
|
link = "https://..." # public url of the blog, required if rss is enabled
|
||||||
|
|
||||||
[dirs]
|
[dirs]
|
||||||
posts = "posts" # where posts are stored
|
posts = "posts" # where posts are stored
|
||||||
media = "media" # directory served under /media/
|
media = "media" # directory served under /media/
|
||||||
|
|
|
@ -6,6 +6,7 @@ use color_eyre::eyre::{bail, Context, Result};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
use tracing::{error, info};
|
use tracing::{error, info};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
use crate::ranged_i128_visitor::RangedI128Visitor;
|
use crate::ranged_i128_visitor::RangedI128Visitor;
|
||||||
|
|
||||||
|
@ -50,6 +51,12 @@ pub struct DirsConfig {
|
||||||
pub media: PathBuf,
|
pub media: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub struct RssConfig {
|
||||||
|
pub enable: bool,
|
||||||
|
pub link: Url,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
|
@ -57,6 +64,7 @@ pub struct Config {
|
||||||
pub description: String,
|
pub description: String,
|
||||||
pub raw_access: bool,
|
pub raw_access: bool,
|
||||||
pub num_posts: usize,
|
pub num_posts: usize,
|
||||||
|
pub rss: RssConfig,
|
||||||
pub dirs: DirsConfig,
|
pub dirs: DirsConfig,
|
||||||
pub http: HttpConfig,
|
pub http: HttpConfig,
|
||||||
pub render: RenderConfig,
|
pub render: RenderConfig,
|
||||||
|
@ -70,6 +78,16 @@ impl Default for Config {
|
||||||
description: "blazingly fast markdown blog software written in rust memory safe".into(),
|
description: "blazingly fast markdown blog software written in rust memory safe".into(),
|
||||||
raw_access: true,
|
raw_access: true,
|
||||||
num_posts: 5,
|
num_posts: 5,
|
||||||
|
// i have a love-hate relationship with serde
|
||||||
|
// it was engimatic at first, but then i started actually using it
|
||||||
|
// writing my own serialize and deserialize implementations.. spending
|
||||||
|
// a lot of time in the docs trying to understand each and every option..
|
||||||
|
// now with this knowledge i can do stuff like this! (see rss field)
|
||||||
|
// and i'm proud to say that it still makes 0 sense.
|
||||||
|
rss: RssConfig {
|
||||||
|
enable: false,
|
||||||
|
link: Url::parse("http://example.com").unwrap(),
|
||||||
|
},
|
||||||
dirs: Default::default(),
|
dirs: Default::default(),
|
||||||
http: Default::default(),
|
http: Default::default(),
|
||||||
render: Default::default(),
|
render: Default::default(),
|
||||||
|
|
|
@ -53,6 +53,10 @@ pub type AppResult<T> = Result<T, AppError>;
|
||||||
pub enum AppError {
|
pub enum AppError {
|
||||||
#[error("failed to fetch post: {0}")]
|
#[error("failed to fetch post: {0}")]
|
||||||
PostError(#[from] PostError),
|
PostError(#[from] PostError),
|
||||||
|
#[error("rss is disabled")]
|
||||||
|
RssDisabled,
|
||||||
|
#[error(transparent)]
|
||||||
|
UrlError(#[from] url::ParseError),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<std::io::Error> for AppError {
|
impl From<std::io::Error> for AppError {
|
||||||
|
@ -75,7 +79,8 @@ impl IntoResponse for AppError {
|
||||||
PostError::NotFound(_) => StatusCode::NOT_FOUND,
|
PostError::NotFound(_) => StatusCode::NOT_FOUND,
|
||||||
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
},
|
},
|
||||||
//_ => StatusCode::INTERNAL_SERVER_ERROR,
|
AppError::RssDisabled => StatusCode::FORBIDDEN,
|
||||||
|
AppError::UrlError(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
};
|
};
|
||||||
(
|
(
|
||||||
status_code,
|
status_code,
|
||||||
|
|
96
src/main.rs
96
src/main.rs
|
@ -18,11 +18,13 @@ use std::time::Duration;
|
||||||
|
|
||||||
use askama_axum::Template;
|
use askama_axum::Template;
|
||||||
use axum::extract::{Path, Query, State};
|
use axum::extract::{Path, Query, State};
|
||||||
use axum::http::Request;
|
use axum::http::{header, Request};
|
||||||
use axum::response::{IntoResponse, Redirect, Response};
|
use axum::response::{IntoResponse, Redirect, Response};
|
||||||
use axum::routing::{get, Router};
|
use axum::routing::{get, Router};
|
||||||
use axum::Json;
|
use axum::Json;
|
||||||
use color_eyre::eyre::{self, Context};
|
use color_eyre::eyre::{self, Context};
|
||||||
|
use error::AppError;
|
||||||
|
use rss::{Category, ChannelBuilder, ItemBuilder};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
|
@ -57,8 +59,8 @@ struct IndexTemplate {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "view_post.html")]
|
#[template(path = "post.html")]
|
||||||
struct ViewPostTemplate {
|
struct PostTemplate {
|
||||||
meta: PostMetadata,
|
meta: PostMetadata,
|
||||||
rendered: String,
|
rendered: String,
|
||||||
rendered_in: RenderStats,
|
rendered_in: RenderStats,
|
||||||
|
@ -78,7 +80,7 @@ async fn index(
|
||||||
) -> AppResult<IndexTemplate> {
|
) -> AppResult<IndexTemplate> {
|
||||||
let posts = state
|
let posts = state
|
||||||
.posts
|
.posts
|
||||||
.get_max_n_posts_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_ref())
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(IndexTemplate {
|
Ok(IndexTemplate {
|
||||||
|
@ -94,12 +96,73 @@ async fn all_posts(
|
||||||
) -> AppResult<Json<Vec<PostMetadata>>> {
|
) -> AppResult<Json<Vec<PostMetadata>>> {
|
||||||
let posts = state
|
let posts = state
|
||||||
.posts
|
.posts
|
||||||
.get_max_n_posts_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_ref())
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(Json(posts))
|
Ok(Json(posts))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn rss(
|
||||||
|
State(state): State<ArcState>,
|
||||||
|
Query(query): Query<QueryParams>,
|
||||||
|
) -> AppResult<Response> {
|
||||||
|
if !state.config.rss.enable {
|
||||||
|
return Err(AppError::RssDisabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
let posts = state
|
||||||
|
.posts
|
||||||
|
.get_all_posts_filtered(|metadata, _| {
|
||||||
|
!query
|
||||||
|
.tag
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|tag| !metadata.tags.contains(tag))
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let mut channel = ChannelBuilder::default();
|
||||||
|
channel
|
||||||
|
.title(&state.config.title)
|
||||||
|
.link(state.config.rss.link.to_string())
|
||||||
|
.description(&state.config.description);
|
||||||
|
//TODO: .language()
|
||||||
|
|
||||||
|
for (metadata, content, _) in posts {
|
||||||
|
channel.item(
|
||||||
|
ItemBuilder::default()
|
||||||
|
.title(metadata.title)
|
||||||
|
.description(metadata.description)
|
||||||
|
.author(metadata.author)
|
||||||
|
.categories(
|
||||||
|
metadata
|
||||||
|
.tags
|
||||||
|
.into_iter()
|
||||||
|
.map(|tag| Category {
|
||||||
|
name: tag,
|
||||||
|
domain: None,
|
||||||
|
})
|
||||||
|
.collect::<Vec<Category>>(),
|
||||||
|
)
|
||||||
|
.pub_date(metadata.created_at.map(|date| date.to_rfc2822()))
|
||||||
|
.content(content)
|
||||||
|
.link(
|
||||||
|
state
|
||||||
|
.config
|
||||||
|
.rss
|
||||||
|
.link
|
||||||
|
.join(&format!("/posts/{}", metadata.name))?
|
||||||
|
.to_string(),
|
||||||
|
)
|
||||||
|
.build(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let body = channel.build().to_string();
|
||||||
|
drop(channel);
|
||||||
|
|
||||||
|
Ok(([(header::CONTENT_TYPE, "text/xml")], body).into_response())
|
||||||
|
}
|
||||||
|
|
||||||
async fn post(State(state): State<ArcState>, Path(name): Path<String>) -> AppResult<Response> {
|
async fn post(State(state): State<ArcState>, Path(name): Path<String>) -> AppResult<Response> {
|
||||||
if name.ends_with(".md") && state.config.raw_access {
|
if name.ends_with(".md") && state.config.raw_access {
|
||||||
let mut file = tokio::fs::OpenOptions::new()
|
let mut file = tokio::fs::OpenOptions::new()
|
||||||
|
@ -113,7 +176,7 @@ async fn post(State(state): State<ArcState>, Path(name): Path<String>) -> AppRes
|
||||||
Ok(([("content-type", "text/plain")], buf).into_response())
|
Ok(([("content-type", "text/plain")], buf).into_response())
|
||||||
} else {
|
} else {
|
||||||
let post = state.posts.get_post(&name).await?;
|
let post = state.posts.get_post(&name).await?;
|
||||||
let page = ViewPostTemplate {
|
let page = PostTemplate {
|
||||||
meta: post.0,
|
meta: post.0,
|
||||||
rendered: post.1,
|
rendered: post.1,
|
||||||
rendered_in: post.2,
|
rendered_in: post.2,
|
||||||
|
@ -250,6 +313,7 @@ async fn main() -> eyre::Result<()> {
|
||||||
)
|
)
|
||||||
.route("/posts/:name", get(post))
|
.route("/posts/:name", get(post))
|
||||||
.route("/posts", get(all_posts))
|
.route("/posts", get(all_posts))
|
||||||
|
.route("/feed.xml", get(rss))
|
||||||
.nest_service("/static", ServeDir::new("static").precompressed_gzip())
|
.nest_service("/static", ServeDir::new("static").precompressed_gzip())
|
||||||
.nest_service("/media", ServeDir::new("media"))
|
.nest_service("/media", ServeDir::new("media"))
|
||||||
.layer(
|
.layer(
|
||||||
|
@ -316,30 +380,24 @@ async fn main() -> eyre::Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// write cache to file
|
// write cache to file
|
||||||
let AppState { config, posts } = Arc::<AppState>::try_unwrap(state).unwrap_or_else(|state| {
|
let config = &state.config;
|
||||||
warn!("couldn't unwrap Arc over AppState, more than one strong reference exists for Arc. cloning instead");
|
let posts = &state.posts;
|
||||||
// TODO: only do this when persistence is enabled
|
|
||||||
// first check config from inside the arc, then try unwrap
|
|
||||||
AppState::clone(state.as_ref())
|
|
||||||
});
|
|
||||||
if config.cache.enable
|
if config.cache.enable
|
||||||
&& config.cache.persistence
|
&& config.cache.persistence
|
||||||
&& let Some(cache) = posts.into_cache()
|
&& let Some(cache) = posts.cache()
|
||||||
{
|
{
|
||||||
let path = &config.cache.file;
|
let path = &config.cache.file;
|
||||||
let serialized = bitcode::serialize(&cache).context("failed to serialize cache")?;
|
let serialized = bitcode::serialize(cache).context("failed to serialize cache")?;
|
||||||
let mut cache_file = tokio::fs::File::create(path)
|
let mut cache_file = tokio::fs::File::create(path)
|
||||||
.await
|
.await
|
||||||
.with_context(|| format!("failed to open cache at {}", path.display()))?;
|
.with_context(|| format!("failed to open cache at {}", path.display()))?;
|
||||||
|
let compression_level = config.cache.compression_level;
|
||||||
if config.cache.compress {
|
if config.cache.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 || {
|
||||||
std::io::Write::write_all(
|
std::io::Write::write_all(
|
||||||
&mut zstd::stream::write::Encoder::new(
|
&mut zstd::stream::write::Encoder::new(cache_file, compression_level)?
|
||||||
cache_file,
|
.auto_finish(),
|
||||||
config.cache.compression_level,
|
|
||||||
)?
|
|
||||||
.auto_finish(),
|
|
||||||
&serialized,
|
&serialized,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
@ -8,7 +8,7 @@ use crate::config::RenderConfig;
|
||||||
use crate::post::PostMetadata;
|
use crate::post::PostMetadata;
|
||||||
|
|
||||||
/// do not persist cache if this version number changed
|
/// do not persist cache if this version number changed
|
||||||
pub const CACHE_VERSION: u16 = 1;
|
pub const CACHE_VERSION: u16 = 2;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
pub struct CacheValue {
|
pub struct CacheValue {
|
||||||
|
@ -111,6 +111,7 @@ impl Cache {
|
||||||
let old_size = self.0.len();
|
let old_size = self.0.len();
|
||||||
let mut i = 0;
|
let mut i = 0;
|
||||||
|
|
||||||
|
// TODO: multithread
|
||||||
self.0
|
self.0
|
||||||
.retain_async(|k, v| {
|
.retain_async(|k, v| {
|
||||||
if get_mtime(k).is_some_and(|mtime| mtime == v.mtime) {
|
if get_mtime(k).is_some_and(|mtime| mtime == v.mtime) {
|
||||||
|
|
|
@ -5,7 +5,6 @@ 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};
|
||||||
|
|
||||||
use askama::Template;
|
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use fronma::parser::{parse, ParsedData};
|
use fronma::parser::{parse, ParsedData};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
@ -63,14 +62,6 @@ pub struct PostMetadata {
|
||||||
pub tags: Vec<String>,
|
pub tags: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
use crate::filters;
|
|
||||||
#[derive(Template)]
|
|
||||||
#[template(path = "post.html")]
|
|
||||||
struct Post<'a> {
|
|
||||||
pub meta: &'a PostMetadata,
|
|
||||||
pub rendered_markdown: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
pub enum RenderStats {
|
pub enum RenderStats {
|
||||||
Cached(Duration),
|
Cached(Duration),
|
||||||
|
@ -127,12 +118,7 @@ impl PostManager {
|
||||||
let parsing = parsing_start.elapsed();
|
let parsing = parsing_start.elapsed();
|
||||||
|
|
||||||
let before_render = Instant::now();
|
let before_render = Instant::now();
|
||||||
let rendered_markdown = render(body, &self.config);
|
let post = render(body, &self.config);
|
||||||
let post = Post {
|
|
||||||
meta: &metadata,
|
|
||||||
rendered_markdown,
|
|
||||||
}
|
|
||||||
.render()?;
|
|
||||||
let rendering = before_render.elapsed();
|
let rendering = before_render.elapsed();
|
||||||
|
|
||||||
if let Some(cache) = self.cache.as_ref() {
|
if let Some(cache) = self.cache.as_ref() {
|
||||||
|
@ -151,7 +137,7 @@ impl PostManager {
|
||||||
Ok((metadata, post, (parsing, rendering)))
|
Ok((metadata, post, (parsing, rendering)))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn list_posts(
|
pub async fn get_all_post_metadata_filtered(
|
||||||
&self,
|
&self,
|
||||||
filter: impl Fn(&PostMetadata) -> bool,
|
filter: impl Fn(&PostMetadata) -> bool,
|
||||||
) -> Result<Vec<PostMetadata>, PostError> {
|
) -> Result<Vec<PostMetadata>, PostError> {
|
||||||
|
@ -200,21 +186,53 @@ impl PostManager {
|
||||||
Ok(posts)
|
Ok(posts)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_max_n_posts_with_optional_tag_sorted(
|
pub async fn get_all_posts_filtered(
|
||||||
|
&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.dir).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 filter(&post.0, &post.1) {
|
||||||
|
posts.push(post);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(posts)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_max_n_post_metadata_with_optional_tag_sorted(
|
||||||
&self,
|
&self,
|
||||||
n: Option<usize>,
|
n: Option<usize>,
|
||||||
tag: Option<&String>,
|
tag: Option<&String>,
|
||||||
) -> Result<Vec<PostMetadata>, PostError> {
|
) -> Result<Vec<PostMetadata>, PostError> {
|
||||||
let mut posts = self
|
let mut posts = self
|
||||||
.list_posts(|metadata| !tag.is_some_and(|tag| !metadata.tags.contains(tag)))
|
.get_all_post_metadata_filtered(|metadata| {
|
||||||
|
!tag.is_some_and(|tag| !metadata.tags.contains(tag))
|
||||||
|
})
|
||||||
.await?;
|
.await?;
|
||||||
posts.sort_unstable_by_key(|metadata| metadata.created_at.unwrap_or_default());
|
// 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_by_key(|metadata| metadata.created_at.unwrap_or_default());
|
||||||
|
posts.reverse();
|
||||||
if let Some(n) = n {
|
if let Some(n) = n {
|
||||||
posts = Vec::from(&posts[posts.len().saturating_sub(n)..]);
|
posts.truncate(n);
|
||||||
}
|
}
|
||||||
|
|
||||||
posts.reverse();
|
|
||||||
Ok(posts)
|
Ok(posts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -257,8 +275,8 @@ impl PostManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn into_cache(self) -> Option<Cache> {
|
pub fn cache(&self) -> Option<&Cache> {
|
||||||
self.cache
|
self.cache.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn cleanup(&self) {
|
pub async fn cleanup(&self) {
|
||||||
|
|
|
@ -1,16 +1,52 @@
|
||||||
{%- import "macros.askama" as macros -%}
|
{%- import "macros.askama" as macros -%}
|
||||||
<h1 class="post-title">
|
<!DOCTYPE html>
|
||||||
{{ meta.title }}
|
<html lang="en">
|
||||||
<span class="post-author">- by {{ meta.author }}</span>
|
<head>
|
||||||
</h1>
|
<head>
|
||||||
<p class="post-desc">{{ meta.description }}</p>
|
<meta charset="UTF-8" />
|
||||||
<p>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<!-- prettier-ignore -->
|
<meta name="description" content="{{ meta.title }}" />
|
||||||
<div>
|
<meta property="og:title" content="{{ meta.title }}" />
|
||||||
{% call macros::table(meta) %}
|
<meta property="og:description" content="{{ meta.description }}" />
|
||||||
</div>
|
{% match meta.icon %} {% when Some with (url) %}
|
||||||
<a href="/posts/{{ meta.name }}">link</a><br />
|
<meta property="og:image" content="{{ url }}" />
|
||||||
<a href="/">back to home</a>
|
<link rel="shortcut icon" href="{{ url }}" />
|
||||||
</p>
|
{% when None %} {% endmatch %}
|
||||||
<hr />
|
<title>{{ meta.title }}</title>
|
||||||
{{ rendered_markdown|escape("none") }}
|
<link rel="stylesheet" href="/static/style.css" />
|
||||||
|
<link rel="stylesheet" href="/static/post.css" />
|
||||||
|
</head>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<h1 class="post-title">
|
||||||
|
{{ meta.title }}
|
||||||
|
<span class="post-author">- by {{ meta.author }}</span>
|
||||||
|
</h1>
|
||||||
|
<p class="post-desc">{{ meta.description }}</p>
|
||||||
|
<div class="" post>
|
||||||
|
<!-- prettier-ignore -->
|
||||||
|
<div>
|
||||||
|
{% call macros::table(meta) %}
|
||||||
|
</div>
|
||||||
|
<a href="/posts/{{ meta.name }}">link</a><br />
|
||||||
|
<a href="/">back to home</a>
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
|
{{ rendered|escape("none") }}
|
||||||
|
</main>
|
||||||
|
<!-- prettier-ignore -->
|
||||||
|
<footer>
|
||||||
|
{% match rendered_in %}
|
||||||
|
{% when RenderStats::ParsedAndRendered(total, parsing, rendering) %}
|
||||||
|
<span class="tooltipped" title="parsing took {{ parsing|duration }}">parsed</span> and
|
||||||
|
<span class="tooltipped" title="rendering took {{ rendering|duration }}">rendered</span> in {{ total|duration }}
|
||||||
|
{% when RenderStats::Cached(total) %}
|
||||||
|
retrieved from cache in {{ total|duration }}
|
||||||
|
{% endmatch %}
|
||||||
|
{% if markdown_access %}
|
||||||
|
- <a href="/posts/{{ meta.name }}.md">view raw</a>
|
||||||
|
{% endif %}
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
|
@ -1,38 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta
|
|
||||||
name="viewport"
|
|
||||||
content="width=device-width, initial-scale=1.0"
|
|
||||||
/>
|
|
||||||
<meta name="description" content="{{ meta.title }}" />
|
|
||||||
<meta property="og:title" content="{{ meta.title }}" />
|
|
||||||
<meta property="og:description" content="{{ meta.description }}" />
|
|
||||||
{% match meta.icon %} {% when Some with (url) %}
|
|
||||||
<meta property="og:image" content="{{ url }}" />
|
|
||||||
<link rel="shortcut icon" href="{{ url }}" />
|
|
||||||
{% when None %} {% endmatch %}
|
|
||||||
<title>{{ meta.title }}</title>
|
|
||||||
<link rel="stylesheet" href="/static/style.css" />
|
|
||||||
<link rel="stylesheet" href="/static/post.css" />
|
|
||||||
</head>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<main>{{ rendered|escape("none") }}</main>
|
|
||||||
<!-- prettier-ignore -->
|
|
||||||
<footer>
|
|
||||||
{% match rendered_in %}
|
|
||||||
{% when RenderStats::ParsedAndRendered(total, parsing, rendering) %}
|
|
||||||
<span class="tooltipped" title="parsing took {{ parsing|duration }}">parsed</span> and
|
|
||||||
<span class="tooltipped" title="rendering took {{ rendering|duration }}">rendered</span> in {{ total|duration }}
|
|
||||||
{% when RenderStats::Cached(total) %}
|
|
||||||
retrieved from cache in {{ total|duration }}
|
|
||||||
{% endmatch %}
|
|
||||||
{% if markdown_access %}
|
|
||||||
- <a href="/posts/{{ meta.name }}.md">view raw</a>
|
|
||||||
{% endif %}
|
|
||||||
</footer>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
Loading…
Reference in a new issue