RSS support

main
Pantonshire 4 years ago
parent c79d5a479a
commit aa06d2c5ef

139
Cargo.lock generated

@ -46,6 +46,19 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "atom_syndication"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21fb6a0b39c6517edafe46f8137e53c51742425a4dae1c73ee12264a37ad7541"
dependencies = [
"chrono",
"derive_builder",
"diligent-date-parser",
"never",
"quick-xml",
]
[[package]] [[package]]
name = "atty" name = "atty"
version = "0.2.14" version = "0.2.14"
@ -148,6 +161,7 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
name = "blog_server" name = "blog_server"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"atom_syndication",
"axum", "axum",
"chrono", "chrono",
"knuffel", "knuffel",
@ -156,6 +170,7 @@ dependencies = [
"miette", "miette",
"notify", "notify",
"pulldown-cmark", "pulldown-cmark",
"rss",
"syntect", "syntect",
"tokio", "tokio",
"tower", "tower",
@ -223,6 +238,90 @@ dependencies = [
"syntect", "syntect",
] ]
[[package]]
name = "darling"
version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f2c43f534ea4b0b049015d00269734195e6d3f0f6635cb692251aca6f9f8b3c"
dependencies = [
"darling_core",
"darling_macro",
]
[[package]]
name = "darling_core"
version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e91455b86830a1c21799d94524df0845183fa55bafd9aa137b01c7d1065fa36"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn",
]
[[package]]
name = "darling_macro"
version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29b5acf0dea37a7f66f7b25d2c5e93fd46f8f6968b1a5d7a3e02e97768afc95a"
dependencies = [
"darling_core",
"quote",
"syn",
]
[[package]]
name = "derive_builder"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d13202debe11181040ae9063d739fa32cfcaaebe2275fe387703460ae2365b30"
dependencies = [
"derive_builder_macro",
]
[[package]]
name = "derive_builder_core"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66e616858f6187ed828df7c64a6d71720d83767a7f19740b2d1b6fe6327b36e5"
dependencies = [
"darling",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "derive_builder_macro"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58a94ace95092c5acb1e97a7e846b310cfbd499652f72297da7493f618a98d73"
dependencies = [
"derive_builder_core",
"syn",
]
[[package]]
name = "diligent-date-parser"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2d0fd95c7c02e2d6c588c6c5628466fff9bdde4b8c6196465e087b08e792720"
dependencies = [
"chrono",
]
[[package]]
name = "encoding_rs"
version = "0.8.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b"
dependencies = [
"cfg-if 1.0.0",
]
[[package]] [[package]]
name = "filetime" name = "filetime"
version = "0.2.16" version = "0.2.16"
@ -436,6 +535,12 @@ dependencies = [
"want", "want",
] ]
[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "1.8.1" version = "1.8.1"
@ -742,6 +847,12 @@ dependencies = [
"winapi 0.3.9", "winapi 0.3.9",
] ]
[[package]]
name = "never"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c96aba5aa877601bb3f6dd6a63a969e1f82e60646e81e71b14496995e9853c91"
[[package]] [[package]]
name = "notify" name = "notify"
version = "4.0.17" version = "4.0.17"
@ -967,6 +1078,16 @@ dependencies = [
"unicase", "unicase",
] ]
[[package]]
name = "quick-xml"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8533f14c8382aaad0d592c812ac3b826162128b65662331e1127b45c3d18536b"
dependencies = [
"encoding_rs",
"memchr",
]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.18" version = "1.0.18"
@ -1002,6 +1123,18 @@ version = "0.6.25"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
[[package]]
name = "rss"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acaf1331b7fc4edc3c2920819fee1766c27e8d40da593155832db3d6dea64e92"
dependencies = [
"atom_syndication",
"derive_builder",
"never",
"quick-xml",
]
[[package]] [[package]]
name = "rustc-demangle" name = "rustc-demangle"
version = "0.1.21" version = "0.1.21"
@ -1121,6 +1254,12 @@ dependencies = [
"winapi 0.3.9", "winapi 0.3.9",
] ]
[[package]]
name = "strsim"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]] [[package]]
name = "supports-color" name = "supports-color"
version = "1.3.0" version = "1.3.0"

@ -15,6 +15,9 @@ tower = { version = "0.4", features = ["limit"] }
tower-http = { version = "0.3", features = ["fs", "trace"] } tower-http = { version = "0.3", features = ["fs", "trace"] }
# Compile-time HTTP templating # Compile-time HTTP templating
maud = "0.23" maud = "0.23"
# Serialisation for RSS and Atom
atom_syndication = "0.11"
rss = "2"
# KDL parsing # KDL parsing
knuffel = "2" knuffel = "2"
# CommonMark parsing # CommonMark parsing

@ -16,16 +16,32 @@ use codeblock::CodeBlockRenderer;
use posts_store::ConcurrentPostsStore; use posts_store::ConcurrentPostsStore;
use render::Renderer; use render::Renderer;
#[derive(knuffel::Decode)] #[derive(knuffel::Decode, Clone, Debug)]
struct Config { pub struct Config {
#[knuffel(child, unwrap(argument))] #[knuffel(child, unwrap(argument))]
bind: String, bind: String,
#[knuffel(child, unwrap(argument))] #[knuffel(child, unwrap(argument))]
concurrency_limit: usize,
#[knuffel(child, unwrap(argument))]
posts_dir: PathBuf, posts_dir: PathBuf,
#[knuffel(child, unwrap(argument))] #[knuffel(child, unwrap(argument))]
static_dir: PathBuf, static_dir: PathBuf,
#[knuffel(child)]
rss: RssConfig,
}
#[derive(knuffel::Decode, Clone, Debug)]
pub struct RssConfig {
#[knuffel(child, unwrap(argument))] #[knuffel(child, unwrap(argument))]
concurrency_limit: usize, num_posts: usize,
#[knuffel(child, unwrap(argument))]
title: String,
#[knuffel(child, unwrap(argument))]
ttl: u32,
#[knuffel(child, unwrap(argument))]
protocol: String,
#[knuffel(child, unwrap(argument))]
domain: String,
} }
fn main() -> miette::Result<()> { fn main() -> miette::Result<()> {
@ -79,18 +95,18 @@ fn main() -> miette::Result<()> {
.block_on(run(config, posts_store)) .block_on(run(config, posts_store))
} }
async fn run(config: Config, posts_store: ConcurrentPostsStore) -> miette::Result<()> { async fn run(
let service = service::site_service( config: Config,
posts_store, posts_store: ConcurrentPostsStore,
&config.static_dir, ) -> miette::Result<()>
config.concurrency_limit {
);
let bind_address = &config.bind let bind_address = &config.bind
.parse() .parse()
.into_diagnostic() .into_diagnostic()
.wrap_err_with(|| format!("Failed to parse socket address \"{}\"", config.bind))?; .wrap_err_with(|| format!("Failed to parse socket address \"{}\"", config.bind))?;
let service = service::site_service(config, posts_store);
info!(address = %bind_address, "Starting server"); info!(address = %bind_address, "Starting server");
Server::try_bind(bind_address) Server::try_bind(bind_address)

@ -114,7 +114,7 @@ impl Post {
pub fn updated(&self) -> DateTime<Utc> { pub fn updated(&self) -> DateTime<Utc> {
self.updated self.updated
} }
pub fn parse( pub fn parse(
code_renderer: &CodeBlockRenderer, code_renderer: &CodeBlockRenderer,
post_id: PostId, post_id: PostId,
@ -125,12 +125,12 @@ impl Post {
) -> Result<Self, ParseError> ) -> Result<Self, ParseError>
{ {
let mdpost = MdPost::parse(file_name, source)?; let mdpost = MdPost::parse(file_name, source)?;
Ok(Self::from_mdpost(post_id, code_renderer, created, updated, mdpost)) Ok(Self::from_mdpost(code_renderer, post_id, created, updated, mdpost))
} }
fn from_mdpost( fn from_mdpost(
id: PostId,
code_renderer: &CodeBlockRenderer, code_renderer: &CodeBlockRenderer,
id: PostId,
created: DateTime<Utc>, created: DateTime<Utc>,
updated: DateTime<Utc>, updated: DateTime<Utc>,
mdpost: MdPost, mdpost: MdPost,

@ -10,9 +10,11 @@ use chrono::{DateTime, Utc};
use notify::DebouncedEvent; use notify::DebouncedEvent;
use tracing::{info, warn, error}; use tracing::{info, warn, error};
use crate::codeblock::CodeBlockRenderer; use crate::{
use crate::post::{ParseError, Post, PostId}; codeblock::CodeBlockRenderer,
use crate::posts_store::ConcurrentPostsStore; post::{ParseError, Post, PostId},
posts_store::ConcurrentPostsStore,
};
pub struct Renderer { pub struct Renderer {
posts: ConcurrentPostsStore, posts: ConcurrentPostsStore,
@ -125,7 +127,8 @@ impl Renderer {
#[tracing::instrument(skip(self))] #[tracing::instrument(skip(self))]
fn remove(&self, target: &EventTarget) { fn remove(&self, target: &EventTarget) {
self.posts.write_blocking().remove(&target.id); let mut guard = self.posts.write_blocking();
guard.remove(&target.id);
} }
#[tracing::instrument(skip(self))] #[tracing::instrument(skip(self))]

@ -1,10 +1,10 @@
use maud::html; use maud::html;
use crate::template; use crate::template;
use super::response::HtmlResponse; use super::response::Html;
pub async fn handle() -> HtmlResponse { pub async fn handle() -> Html {
HtmlResponse::new() Html::new()
.with_title_static("Contact") .with_title_static("Contact")
.with_crawler_permissive() .with_crawler_permissive()
.with_head(html! { .with_head(html! {

@ -5,10 +5,10 @@ use crate::{
posts_store::ConcurrentPostsStore, posts_store::ConcurrentPostsStore,
template, template,
}; };
use super::response::HtmlResponse; use super::response::Html;
pub async fn handle(Extension(posts): Extension<ConcurrentPostsStore>) -> HtmlResponse { pub async fn handle(Extension(posts): Extension<ConcurrentPostsStore>) -> Html {
HtmlResponse::new() Html::new()
.with_title_static("Pantonshire") .with_title_static("Pantonshire")
.with_crawler_permissive() .with_crawler_permissive()
.with_head(html! { .with_head(html! {

@ -3,6 +3,7 @@ mod index;
mod post; mod post;
mod posts_list; mod posts_list;
mod response; mod response;
mod rss;
mod site; mod site;
mod static_content; mod static_content;

@ -2,18 +2,18 @@ use axum::extract::{Extension, Path};
use maud::html; use maud::html;
use crate::posts_store::ConcurrentPostsStore; use crate::posts_store::ConcurrentPostsStore;
use super::response::{ErrorResponse, HtmlResponse}; use super::response::{Error, Html};
pub async fn handle( pub async fn handle(
Path(post_id): Path<String>, Path(post_id): Path<String>,
Extension(posts): Extension<ConcurrentPostsStore> Extension(posts): Extension<ConcurrentPostsStore>
) -> Result<HtmlResponse, ErrorResponse> ) -> Result<Html, Error>
{ {
let post = posts.get(&post_id) let post = posts.get(&post_id)
.await .await
.ok_or(ErrorResponse::PostNotFound)?; .ok_or(Error::PostNotFound)?;
Ok(HtmlResponse::new() Ok(Html::new()
.with_crawler_permissive() .with_crawler_permissive()
.with_title_owned(post.title().to_owned()) .with_title_owned(post.title().to_owned())
.with_head(html! { .with_head(html! {

@ -5,10 +5,10 @@ use crate::{
posts_store::ConcurrentPostsStore, posts_store::ConcurrentPostsStore,
template, template,
}; };
use super::response::HtmlResponse; use super::response::Html;
pub async fn handle(Extension(posts): Extension<ConcurrentPostsStore>) -> HtmlResponse { pub async fn handle(Extension(posts): Extension<ConcurrentPostsStore>) -> Html {
HtmlResponse::new() Html::new()
.with_title_static("Articles") .with_title_static("Articles")
.with_crawler_permissive() .with_crawler_permissive()
.with_head(html! { .with_head(html! {

@ -4,31 +4,32 @@ use std::{
}; };
use axum::{ use axum::{
http::StatusCode, body::{Bytes, Full},
response::{IntoResponse, Html, Response}, http::{header::{self, HeaderValue}, StatusCode},
response::{IntoResponse, Response},
}; };
use maud::{html, Markup, Render, Escaper, DOCTYPE}; use maud::{html, Markup, Render, Escaper, DOCTYPE};
#[derive(Debug)] #[derive(Debug)]
pub enum ErrorResponse { pub enum Error {
Internal, Internal,
PostNotFound, PostNotFound,
StaticResourceNotFound, StaticResourceNotFound,
RouteNotFound, RouteNotFound,
} }
impl ErrorResponse { impl Error {
fn status_code(&self) -> StatusCode { fn status_code(&self) -> StatusCode {
match self { match self {
ErrorResponse::Internal => StatusCode::INTERNAL_SERVER_ERROR, Error::Internal => StatusCode::INTERNAL_SERVER_ERROR,
ErrorResponse::PostNotFound => StatusCode::NOT_FOUND, Error::PostNotFound => StatusCode::NOT_FOUND,
ErrorResponse::StaticResourceNotFound => StatusCode::NOT_FOUND, Error::StaticResourceNotFound => StatusCode::NOT_FOUND,
ErrorResponse::RouteNotFound => StatusCode::NOT_FOUND, Error::RouteNotFound => StatusCode::NOT_FOUND,
} }
} }
} }
impl IntoResponse for ErrorResponse { impl IntoResponse for Error {
fn into_response(self) -> Response { fn into_response(self) -> Response {
let status_code = self.status_code(); let status_code = self.status_code();
@ -51,7 +52,7 @@ impl IntoResponse for ErrorResponse {
buf buf
}; };
HtmlResponse::new() Html::new()
.with_status(status_code) .with_status(status_code)
.with_body(html! { .with_body(html! {
p { (status_text) } p { (status_text) }
@ -61,7 +62,7 @@ impl IntoResponse for ErrorResponse {
} }
} }
pub struct HtmlResponse { pub struct Html {
status: StatusCode, status: StatusCode,
title: Cow<'static, str>, title: Cow<'static, str>,
head: Option<Markup>, head: Option<Markup>,
@ -69,7 +70,7 @@ pub struct HtmlResponse {
crawler_hints: CrawlerHints, crawler_hints: CrawlerHints,
} }
impl HtmlResponse { impl Html {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
status: StatusCode::OK, status: StatusCode::OK,
@ -117,13 +118,13 @@ impl HtmlResponse {
} }
} }
impl Default for HtmlResponse { impl Default for Html {
fn default() -> Self { fn default() -> Self {
Self::new() Self::new()
} }
} }
impl IntoResponse for HtmlResponse { impl IntoResponse for Html {
fn into_response(self) -> Response { fn into_response(self) -> Response {
let html_doc = html! { let html_doc = html! {
(DOCTYPE) (DOCTYPE)
@ -145,7 +146,7 @@ impl IntoResponse for HtmlResponse {
} }
}; };
(self.status, Html(html_doc.into_string())) (self.status, axum::response::Html(html_doc.into_string()))
.into_response() .into_response()
} }
} }
@ -261,3 +262,29 @@ impl Render for CrawlerHints {
let _result = self.write_meta_list_to(escaper); let _result = self.write_meta_list_to(escaper);
} }
} }
pub struct Rss<T>(pub T);
impl<T: Into<Full<Bytes>>> IntoResponse for Rss<T> {
fn into_response(self) -> Response {
let headers = [
(header::CONTENT_TYPE, HeaderValue::from_static("application/rss+xml")),
];
(headers, self.0.into())
.into_response()
}
}
pub struct Atom<T>(pub T);
impl<T: Into<Full<Bytes>>> IntoResponse for Atom<T> {
fn into_response(self) -> Response {
let headers = [
(header::CONTENT_TYPE, HeaderValue::from_static("application/atom+xml")),
];
(headers, self.0.into())
.into_response()
}
}

@ -0,0 +1,44 @@
use std::sync::Arc;
use axum::{
body::Bytes,
extract::Extension,
};
use super::response::Rss;
use crate::{posts_store::ConcurrentPostsStore, Config};
pub async fn handle(
Extension(config): Extension<Arc<Config>>,
Extension(posts): Extension<ConcurrentPostsStore>,
) -> Rss<Bytes> {
let rss_items = posts.read()
.await
.iter_by_created()
.take(config.rss.num_posts)
.map(|post| {
rss::ItemBuilder::default()
.title(Some(post.title().to_owned()))
.link(Some(format!(
"{}://{}/articles/{}",
config.rss.protocol,
config.rss.domain,
post.id()
)))
.pub_date(Some(post.created().to_rfc2822()))
.build()
})
.collect::<Vec<rss::Item>>();
Rss(rss::ChannelBuilder::default()
.title(config.rss.title.clone())
.link(format!(
"{}://{}",
config.rss.protocol, config.rss.domain
))
.ttl(Some(config.rss.ttl.to_string()))
.items(rss_items)
.build()
.to_string()
.into())
}

@ -1,4 +1,4 @@
use std::path::Path; use std::sync::Arc;
use axum::{ use axum::{
handler::Handler, handler::Handler,
@ -11,35 +11,40 @@ use tower::limit::ConcurrencyLimitLayer;
use tower_http::trace::TraceLayer; use tower_http::trace::TraceLayer;
use tracing::info; use tracing::info;
use crate::posts_store::ConcurrentPostsStore; use crate::{
Config,
posts_store::ConcurrentPostsStore
};
use super::{ use super::{
contact, contact,
index, index,
post, post,
posts_list, posts_list,
response::ErrorResponse, response::Error,
rss,
static_content, static_content,
}; };
pub fn service( pub fn service(
config: Config,
posts_store: ConcurrentPostsStore, posts_store: ConcurrentPostsStore,
static_dir: &Path,
concurrency_limit: usize
) -> Router ) -> Router
{ {
Router::new() Router::new()
.route("/", get(index::handle)) .route("/", get(index::handle))
.route("/rss.xml", get(rss::handle))
.route("/contact", get(contact::handle)) .route("/contact", get(contact::handle))
.route("/articles", get(posts_list::handle)) .route("/articles", get(posts_list::handle))
.route("/articles/:post_id", get(post::handle)) .route("/articles/:post_id", get(post::handle))
.nest("/static", static_content::service(static_dir)) .nest("/static", static_content::service(&config.static_dir))
.fallback(handle_fallback.into_service()) .fallback(handle_fallback.into_service())
.layer(ConcurrencyLimitLayer::new(concurrency_limit)) .layer(ConcurrencyLimitLayer::new(config.concurrency_limit))
.layer(TraceLayer::new_for_http()) .layer(TraceLayer::new_for_http())
.layer(Extension(Arc::new(config)))
.layer(Extension(posts_store)) .layer(Extension(posts_store))
} }
pub async fn handle_fallback(uri: Uri) -> ErrorResponse { pub async fn handle_fallback(uri: Uri) -> Error {
info!(path = %uri.path(), "Requested resource not found"); info!(path = %uri.path(), "Requested resource not found");
ErrorResponse::RouteNotFound Error::RouteNotFound
} }

@ -15,7 +15,7 @@ use tower::ServiceExt;
use tower_http::services::ServeDir; use tower_http::services::ServeDir;
use tracing::{info, error}; use tracing::{info, error};
use super::response::ErrorResponse; use super::response::Error;
pub fn service(static_dir: &Path) -> MethodRouter<Body, Infallible> { pub fn service(static_dir: &Path) -> MethodRouter<Body, Infallible> {
let fallback_service = handle_fallback let fallback_service = handle_fallback
@ -29,12 +29,12 @@ pub fn service(static_dir: &Path) -> MethodRouter<Body, Infallible> {
.handle_error(handle_error) .handle_error(handle_error)
} }
pub async fn handle_fallback(uri: Uri) -> ErrorResponse { pub async fn handle_fallback(uri: Uri) -> Error {
info!(path = %uri.path(), "Requested static file not found"); info!(path = %uri.path(), "Requested static file not found");
ErrorResponse::StaticResourceNotFound Error::StaticResourceNotFound
} }
pub async fn handle_error(uri: Uri, err: io::Error) -> ErrorResponse { pub async fn handle_error(uri: Uri, err: io::Error) -> Error {
error!(path = %uri.path(), err = %err, "IO error"); error!(path = %uri.path(), err = %err, "IO error");
ErrorResponse::Internal Error::Internal
} }

Loading…
Cancel
Save