forked from slonk/bingus-blog
Compare commits
No commits in common. "514d4e033d8cf8783bb50cc93fcae48537da6fb8" and "cf85c60c1cdd1d5b80cdb8775273fcbc95a03a91" have entirely different histories.
514d4e033d
...
cf85c60c1c
10 changed files with 100 additions and 319 deletions
115
Cargo.lock
generated
115
Cargo.lock
generated
|
@ -128,19 +128,6 @@ 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"
|
||||||
|
@ -303,7 +290,6 @@ dependencies = [
|
||||||
"comrak",
|
"comrak",
|
||||||
"console-subscriber",
|
"console-subscriber",
|
||||||
"fronma",
|
"fronma",
|
||||||
"rss",
|
|
||||||
"scc",
|
"scc",
|
||||||
"serde",
|
"serde",
|
||||||
"syntect",
|
"syntect",
|
||||||
|
@ -314,7 +300,6 @@ dependencies = [
|
||||||
"tower-http",
|
"tower-http",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"url",
|
|
||||||
"zstd",
|
"zstd",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -591,30 +576,12 @@ 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"
|
||||||
|
@ -951,16 +918,6 @@ 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"
|
||||||
|
@ -1104,12 +1061,6 @@ 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"
|
||||||
|
@ -1257,7 +1208,7 @@ dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"indexmap 2.2.6",
|
"indexmap 2.2.6",
|
||||||
"line-wrap",
|
"line-wrap",
|
||||||
"quick-xml 0.31.0",
|
"quick-xml",
|
||||||
"serde",
|
"serde",
|
||||||
"time",
|
"time",
|
||||||
]
|
]
|
||||||
|
@ -1315,16 +1266,6 @@ 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"
|
||||||
|
@ -1417,18 +1358,6 @@ 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"
|
||||||
|
@ -1723,21 +1652,6 @@ 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"
|
||||||
|
@ -2015,45 +1929,18 @@ 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,7 +37,6 @@ 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"
|
||||||
|
@ -57,5 +56,4 @@ 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
|
||||||
|
|
||||||
- [x] RSS
|
- [ ] RSS
|
||||||
- [x] finish writing this document
|
- [x] finish writing this document
|
||||||
- [x] document config
|
- [x] document config
|
||||||
- [ ] extend syntect options
|
- [ ] extend syntect options
|
||||||
|
@ -27,7 +27,6 @@ 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
|
||||||
|
|
||||||
|
@ -40,11 +39,6 @@ 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,7 +6,6 @@ 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;
|
||||||
|
|
||||||
|
@ -51,12 +50,6 @@ 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 {
|
||||||
|
@ -64,7 +57,6 @@ 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,
|
||||||
|
@ -78,16 +70,6 @@ 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,10 +53,6 @@ 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 {
|
||||||
|
@ -79,8 +75,7 @@ impl IntoResponse for AppError {
|
||||||
PostError::NotFound(_) => StatusCode::NOT_FOUND,
|
PostError::NotFound(_) => StatusCode::NOT_FOUND,
|
||||||
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
},
|
},
|
||||||
AppError::RssDisabled => StatusCode::FORBIDDEN,
|
//_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
AppError::UrlError(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
};
|
};
|
||||||
(
|
(
|
||||||
status_code,
|
status_code,
|
||||||
|
|
94
src/main.rs
94
src/main.rs
|
@ -18,13 +18,11 @@ 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::{header, Request};
|
use axum::http::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;
|
||||||
|
@ -59,8 +57,8 @@ struct IndexTemplate {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "post.html")]
|
#[template(path = "view_post.html")]
|
||||||
struct PostTemplate {
|
struct ViewPostTemplate {
|
||||||
meta: PostMetadata,
|
meta: PostMetadata,
|
||||||
rendered: String,
|
rendered: String,
|
||||||
rendered_in: RenderStats,
|
rendered_in: RenderStats,
|
||||||
|
@ -80,7 +78,7 @@ async fn index(
|
||||||
) -> AppResult<IndexTemplate> {
|
) -> AppResult<IndexTemplate> {
|
||||||
let posts = state
|
let posts = state
|
||||||
.posts
|
.posts
|
||||||
.get_max_n_post_metadata_with_optional_tag_sorted(query.num_posts, query.tag.as_ref())
|
.get_max_n_posts_with_optional_tag_sorted(query.num_posts, query.tag.as_ref())
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(IndexTemplate {
|
Ok(IndexTemplate {
|
||||||
|
@ -96,73 +94,12 @@ async fn all_posts(
|
||||||
) -> AppResult<Json<Vec<PostMetadata>>> {
|
) -> AppResult<Json<Vec<PostMetadata>>> {
|
||||||
let posts = state
|
let posts = state
|
||||||
.posts
|
.posts
|
||||||
.get_max_n_post_metadata_with_optional_tag_sorted(query.num_posts, query.tag.as_ref())
|
.get_max_n_posts_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()
|
||||||
|
@ -176,7 +113,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 = PostTemplate {
|
let page = ViewPostTemplate {
|
||||||
meta: post.0,
|
meta: post.0,
|
||||||
rendered: post.1,
|
rendered: post.1,
|
||||||
rendered_in: post.2,
|
rendered_in: post.2,
|
||||||
|
@ -313,7 +250,6 @@ 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(
|
||||||
|
@ -380,23 +316,29 @@ async fn main() -> eyre::Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// write cache to file
|
// write cache to file
|
||||||
let config = &state.config;
|
let AppState { config, posts } = Arc::<AppState>::try_unwrap(state).unwrap_or_else(|state| {
|
||||||
let posts = &state.posts;
|
warn!("couldn't unwrap Arc over AppState, more than one strong reference exists for Arc. cloning instead");
|
||||||
|
// 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.cache()
|
&& let Some(cache) = posts.into_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(cache_file, compression_level)?
|
&mut zstd::stream::write::Encoder::new(
|
||||||
|
cache_file,
|
||||||
|
config.cache.compression_level,
|
||||||
|
)?
|
||||||
.auto_finish(),
|
.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 = 2;
|
pub const CACHE_VERSION: u16 = 1;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
pub struct CacheValue {
|
pub struct CacheValue {
|
||||||
|
@ -111,7 +111,6 @@ 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,6 +5,7 @@ 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};
|
||||||
|
@ -62,6 +63,14 @@ 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),
|
||||||
|
@ -118,7 +127,12 @@ impl PostManager {
|
||||||
let parsing = parsing_start.elapsed();
|
let parsing = parsing_start.elapsed();
|
||||||
|
|
||||||
let before_render = Instant::now();
|
let before_render = Instant::now();
|
||||||
let post = render(body, &self.config);
|
let rendered_markdown = 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() {
|
||||||
|
@ -137,7 +151,7 @@ impl PostManager {
|
||||||
Ok((metadata, post, (parsing, rendering)))
|
Ok((metadata, post, (parsing, rendering)))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_all_post_metadata_filtered(
|
pub async fn list_posts(
|
||||||
&self,
|
&self,
|
||||||
filter: impl Fn(&PostMetadata) -> bool,
|
filter: impl Fn(&PostMetadata) -> bool,
|
||||||
) -> Result<Vec<PostMetadata>, PostError> {
|
) -> Result<Vec<PostMetadata>, PostError> {
|
||||||
|
@ -186,53 +200,21 @@ impl PostManager {
|
||||||
Ok(posts)
|
Ok(posts)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_all_posts_filtered(
|
pub async fn get_max_n_posts_with_optional_tag_sorted(
|
||||||
&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
|
||||||
.get_all_post_metadata_filtered(|metadata| {
|
.list_posts(|metadata| !tag.is_some_and(|tag| !metadata.tags.contains(tag)))
|
||||||
!tag.is_some_and(|tag| !metadata.tags.contains(tag))
|
|
||||||
})
|
|
||||||
.await?;
|
.await?;
|
||||||
// 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.created_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.reverse();
|
|
||||||
if let Some(n) = n {
|
if let Some(n) = n {
|
||||||
posts.truncate(n);
|
posts = Vec::from(&posts[posts.len().saturating_sub(n)..]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
posts.reverse();
|
||||||
Ok(posts)
|
Ok(posts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -275,8 +257,8 @@ impl PostManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cache(&self) -> Option<&Cache> {
|
pub fn into_cache(self) -> Option<Cache> {
|
||||||
self.cache.as_ref()
|
self.cache
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn cleanup(&self) {
|
pub async fn cleanup(&self) {
|
||||||
|
|
|
@ -1,52 +1,16 @@
|
||||||
{%- import "macros.askama" as macros -%}
|
{%- import "macros.askama" as macros -%}
|
||||||
<!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>
|
|
||||||
<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>
|
||||||
</h1>
|
</h1>
|
||||||
<p class="post-desc">{{ meta.description }}</p>
|
<p class="post-desc">{{ meta.description }}</p>
|
||||||
<div class="" post>
|
<p>
|
||||||
<!-- prettier-ignore -->
|
<!-- prettier-ignore -->
|
||||||
<div>
|
<div>
|
||||||
{% call macros::table(meta) %}
|
{% call macros::table(meta) %}
|
||||||
</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>
|
||||||
</div>
|
</p>
|
||||||
<hr />
|
<hr />
|
||||||
{{ rendered|escape("none") }}
|
{{ rendered_markdown|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>
|
|
||||||
|
|
38
templates/view_post.html
Normal file
38
templates/view_post.html
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
<!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