RSS support

main
Pantonshire 4 years ago
parent c79d5a479a
commit aa06d2c5ef

139
Cargo.lock generated

@ -46,6 +46,19 @@ dependencies = [
"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]]
name = "atty"
version = "0.2.14"
@ -148,6 +161,7 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
name = "blog_server"
version = "0.1.0"
dependencies = [
"atom_syndication",
"axum",
"chrono",
"knuffel",
@ -156,6 +170,7 @@ dependencies = [
"miette",
"notify",
"pulldown-cmark",
"rss",
"syntect",
"tokio",
"tower",
@ -223,6 +238,90 @@ dependencies = [
"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]]
name = "filetime"
version = "0.2.16"
@ -436,6 +535,12 @@ dependencies = [
"want",
]
[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "indexmap"
version = "1.8.1"
@ -742,6 +847,12 @@ dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "never"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c96aba5aa877601bb3f6dd6a63a969e1f82e60646e81e71b14496995e9853c91"
[[package]]
name = "notify"
version = "4.0.17"
@ -967,6 +1078,16 @@ dependencies = [
"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]]
name = "quote"
version = "1.0.18"
@ -1002,6 +1123,18 @@ version = "0.6.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "rustc-demangle"
version = "0.1.21"
@ -1121,6 +1254,12 @@ dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "strsim"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "supports-color"
version = "1.3.0"

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

@ -16,16 +16,32 @@ use codeblock::CodeBlockRenderer;
use posts_store::ConcurrentPostsStore;
use render::Renderer;
#[derive(knuffel::Decode)]
struct Config {
#[derive(knuffel::Decode, Clone, Debug)]
pub struct Config {
#[knuffel(child, unwrap(argument))]
bind: String,
#[knuffel(child, unwrap(argument))]
concurrency_limit: usize,
#[knuffel(child, unwrap(argument))]
posts_dir: PathBuf,
#[knuffel(child, unwrap(argument))]
static_dir: PathBuf,
#[knuffel(child)]
rss: RssConfig,
}
#[derive(knuffel::Decode, Clone, Debug)]
pub struct RssConfig {
#[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<()> {
@ -79,18 +95,18 @@ fn main() -> miette::Result<()> {
.block_on(run(config, posts_store))
}
async fn run(config: Config, posts_store: ConcurrentPostsStore) -> miette::Result<()> {
let service = service::site_service(
posts_store,
&config.static_dir,
config.concurrency_limit
);
async fn run(
config: Config,
posts_store: ConcurrentPostsStore,
) -> miette::Result<()>
{
let bind_address = &config.bind
.parse()
.into_diagnostic()
.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");
Server::try_bind(bind_address)

@ -114,7 +114,7 @@ impl Post {
pub fn updated(&self) -> DateTime<Utc> {
self.updated
}
pub fn parse(
code_renderer: &CodeBlockRenderer,
post_id: PostId,
@ -125,12 +125,12 @@ impl Post {
) -> Result<Self, ParseError>
{
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(
id: PostId,
code_renderer: &CodeBlockRenderer,
id: PostId,
created: DateTime<Utc>,
updated: DateTime<Utc>,
mdpost: MdPost,

@ -10,9 +10,11 @@ use chrono::{DateTime, Utc};
use notify::DebouncedEvent;
use tracing::{info, warn, error};
use crate::codeblock::CodeBlockRenderer;
use crate::post::{ParseError, Post, PostId};
use crate::posts_store::ConcurrentPostsStore;
use crate::{
codeblock::CodeBlockRenderer,
post::{ParseError, Post, PostId},
posts_store::ConcurrentPostsStore,
};
pub struct Renderer {
posts: ConcurrentPostsStore,
@ -125,7 +127,8 @@ impl Renderer {
#[tracing::instrument(skip(self))]
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))]

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

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

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

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

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

@ -4,31 +4,32 @@ use std::{
};
use axum::{
http::StatusCode,
response::{IntoResponse, Html, Response},
body::{Bytes, Full},
http::{header::{self, HeaderValue}, StatusCode},
response::{IntoResponse, Response},
};
use maud::{html, Markup, Render, Escaper, DOCTYPE};
#[derive(Debug)]
pub enum ErrorResponse {
pub enum Error {
Internal,
PostNotFound,
StaticResourceNotFound,
RouteNotFound,
}
impl ErrorResponse {
impl Error {
fn status_code(&self) -> StatusCode {
match self {
ErrorResponse::Internal => StatusCode::INTERNAL_SERVER_ERROR,
ErrorResponse::PostNotFound => StatusCode::NOT_FOUND,
ErrorResponse::StaticResourceNotFound => StatusCode::NOT_FOUND,
ErrorResponse::RouteNotFound => StatusCode::NOT_FOUND,
Error::Internal => StatusCode::INTERNAL_SERVER_ERROR,
Error::PostNotFound => StatusCode::NOT_FOUND,
Error::StaticResourceNotFound => StatusCode::NOT_FOUND,
Error::RouteNotFound => StatusCode::NOT_FOUND,
}
}
}
impl IntoResponse for ErrorResponse {
impl IntoResponse for Error {
fn into_response(self) -> Response {
let status_code = self.status_code();
@ -51,7 +52,7 @@ impl IntoResponse for ErrorResponse {
buf
};
HtmlResponse::new()
Html::new()
.with_status(status_code)
.with_body(html! {
p { (status_text) }
@ -61,7 +62,7 @@ impl IntoResponse for ErrorResponse {
}
}
pub struct HtmlResponse {
pub struct Html {
status: StatusCode,
title: Cow<'static, str>,
head: Option<Markup>,
@ -69,7 +70,7 @@ pub struct HtmlResponse {
crawler_hints: CrawlerHints,
}
impl HtmlResponse {
impl Html {
pub fn new() -> Self {
Self {
status: StatusCode::OK,
@ -117,13 +118,13 @@ impl HtmlResponse {
}
}
impl Default for HtmlResponse {
impl Default for Html {
fn default() -> Self {
Self::new()
}
}
impl IntoResponse for HtmlResponse {
impl IntoResponse for Html {
fn into_response(self) -> Response {
let html_doc = html! {
(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()
}
}
@ -261,3 +262,29 @@ impl Render for CrawlerHints {
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::{
handler::Handler,
@ -11,35 +11,40 @@ use tower::limit::ConcurrencyLimitLayer;
use tower_http::trace::TraceLayer;
use tracing::info;
use crate::posts_store::ConcurrentPostsStore;
use crate::{
Config,
posts_store::ConcurrentPostsStore
};
use super::{
contact,
index,
post,
posts_list,
response::ErrorResponse,
response::Error,
rss,
static_content,
};
pub fn service(
config: Config,
posts_store: ConcurrentPostsStore,
static_dir: &Path,
concurrency_limit: usize
) -> Router
{
Router::new()
.route("/", get(index::handle))
.route("/rss.xml", get(rss::handle))
.route("/contact", get(contact::handle))
.route("/articles", get(posts_list::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())
.layer(ConcurrencyLimitLayer::new(concurrency_limit))
.layer(ConcurrencyLimitLayer::new(config.concurrency_limit))
.layer(TraceLayer::new_for_http())
.layer(Extension(Arc::new(config)))
.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");
ErrorResponse::RouteNotFound
Error::RouteNotFound
}

@ -15,7 +15,7 @@ use tower::ServiceExt;
use tower_http::services::ServeDir;
use tracing::{info, error};
use super::response::ErrorResponse;
use super::response::Error;
pub fn service(static_dir: &Path) -> MethodRouter<Body, Infallible> {
let fallback_service = handle_fallback
@ -29,12 +29,12 @@ pub fn service(static_dir: &Path) -> MethodRouter<Body, Infallible> {
.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");
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");
ErrorResponse::Internal
Error::Internal
}

Loading…
Cancel
Save