RSS support

This commit is contained in:
Pantonshire committed 2022-05-20 20:13:03 +01:00
1 parent c79d5a479a
commit aa06d2c5ef
14 files changed
+297 -59

No files matched your search

+26 -10
View File
@@ -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)
+3 -3
View File
@@ -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,
+7 -4
View File
@@ -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))]
+3 -3
View File
@@ -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! {
+3 -3
View File
@@ -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! {
+1
View File
@@ -3,6 +3,7 @@ mod index;
mod post;
mod posts_list;
mod response;
mod rss;
mod site;
mod static_content;
+4 -4
View File
@@ -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! {
+3 -3
View File
@@ -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! {
+42 -15
View File
@@ -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()
}
}
+44
View File
@@ -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())
}
+14 -9
View File
@@ -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
}
+5 -5
View File
@@ -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
}