Atom feed, GUID for RSS feed
This commit is contained in:
1 parent
aa06d2c5ef
commit
1b4be0862c
13 files changed
+264
-48
No files matched your search
Generated
+9
-9
@@ -485,9 +485,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http-body"
|
name = "http-body"
|
||||||
version = "0.4.4"
|
version = "0.4.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1ff4f84919677303da5f147645dbea6b1881f368d03ac84e1dc09031ebd7b2c6"
|
checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"http",
|
"http",
|
||||||
@@ -650,7 +650,7 @@ checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "libshire"
|
name = "libshire"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/pantonshire/libshire#62dae931409c14531cf338d73269e415c638dede"
|
source = "git+https://github.com/pantonshire/libshire#bd2b3a8a29b34ffeecd658b74ef81a1431fc91f0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "line-wrap"
|
name = "line-wrap"
|
||||||
@@ -920,9 +920,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.10.0"
|
version = "1.12.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9"
|
checksum = "7709cef83f0c1f58f666e746a08b21e0085f7440fa6a29cc194d68aac97a4225"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "onig"
|
name = "onig"
|
||||||
@@ -1108,9 +1108,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.5.5"
|
version = "1.5.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1a11647b6b25ff05a515cb92c365cec08801e83423a235b51e231e1808747286"
|
checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
@@ -1119,9 +1119,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex-syntax"
|
name = "regex-syntax"
|
||||||
version = "0.6.25"
|
version = "0.6.26"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
|
checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rss"
|
name = "rss"
|
||||||
|
|||||||
+27
-6
@@ -5,8 +5,10 @@ mod posts_store;
|
|||||||
mod render;
|
mod render;
|
||||||
mod service;
|
mod service;
|
||||||
mod template;
|
mod template;
|
||||||
|
mod time;
|
||||||
|
mod uuid;
|
||||||
|
|
||||||
use std::{env, fs, path::PathBuf, thread};
|
use std::{env, fs, path::PathBuf, sync::Arc, thread};
|
||||||
|
|
||||||
use axum::Server;
|
use axum::Server;
|
||||||
use miette::{IntoDiagnostic, Context};
|
use miette::{IntoDiagnostic, Context};
|
||||||
@@ -26,8 +28,22 @@ pub struct Config {
|
|||||||
posts_dir: PathBuf,
|
posts_dir: PathBuf,
|
||||||
#[knuffel(child, unwrap(argument))]
|
#[knuffel(child, unwrap(argument))]
|
||||||
static_dir: PathBuf,
|
static_dir: PathBuf,
|
||||||
|
#[knuffel(child, unwrap(argument))]
|
||||||
|
namespace_uuid: uuid::Uuid,
|
||||||
|
#[knuffel(child)]
|
||||||
|
self_ref: SelfRefConfig,
|
||||||
#[knuffel(child)]
|
#[knuffel(child)]
|
||||||
rss: RssConfig,
|
rss: RssConfig,
|
||||||
|
#[knuffel(child)]
|
||||||
|
atom: AtomConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(knuffel::Decode, Clone, Debug)]
|
||||||
|
pub struct SelfRefConfig {
|
||||||
|
#[knuffel(child, unwrap(argument))]
|
||||||
|
protocol: String,
|
||||||
|
#[knuffel(child, unwrap(argument))]
|
||||||
|
domain: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(knuffel::Decode, Clone, Debug)]
|
#[derive(knuffel::Decode, Clone, Debug)]
|
||||||
@@ -38,10 +54,14 @@ pub struct RssConfig {
|
|||||||
title: String,
|
title: String,
|
||||||
#[knuffel(child, unwrap(argument))]
|
#[knuffel(child, unwrap(argument))]
|
||||||
ttl: u32,
|
ttl: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(knuffel::Decode, Clone, Debug)]
|
||||||
|
pub struct AtomConfig {
|
||||||
#[knuffel(child, unwrap(argument))]
|
#[knuffel(child, unwrap(argument))]
|
||||||
protocol: String,
|
num_posts: usize,
|
||||||
#[knuffel(child, unwrap(argument))]
|
#[knuffel(child, unwrap(argument))]
|
||||||
domain: String,
|
title: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> miette::Result<()> {
|
fn main() -> miette::Result<()> {
|
||||||
@@ -49,7 +69,7 @@ fn main() -> miette::Result<()> {
|
|||||||
|
|
||||||
// Load the configuration from the KDL config file specified by the first command-line
|
// Load the configuration from the KDL config file specified by the first command-line
|
||||||
// argument.
|
// argument.
|
||||||
let config = {
|
let config = Arc::new({
|
||||||
let config_path = env::args().nth(1)
|
let config_path = env::args().nth(1)
|
||||||
.ok_or_else(|| miette::Error::msg("No config file specified"))?;
|
.ok_or_else(|| miette::Error::msg("No config file specified"))?;
|
||||||
|
|
||||||
@@ -61,7 +81,7 @@ fn main() -> miette::Result<()> {
|
|||||||
|
|
||||||
knuffel::parse::<Config>(&config_path, &contents)
|
knuffel::parse::<Config>(&config_path, &contents)
|
||||||
.wrap_err_with(|| format!("Failed to parse config file {}", config_path))?
|
.wrap_err_with(|| format!("Failed to parse config file {}", config_path))?
|
||||||
};
|
});
|
||||||
|
|
||||||
// Create the data structure used to store the rendered posts. This uses an `Arc` internally,
|
// Create the data structure used to store the rendered posts. This uses an `Arc` internally,
|
||||||
// so clones will point to the same underlying data.
|
// so clones will point to the same underlying data.
|
||||||
@@ -71,6 +91,7 @@ fn main() -> miette::Result<()> {
|
|||||||
|
|
||||||
// Create the post renderer and the mpsc channel that will be used to communicate with it.
|
// Create the post renderer and the mpsc channel that will be used to communicate with it.
|
||||||
let (renderer, tx) = Renderer::new(
|
let (renderer, tx) = Renderer::new(
|
||||||
|
config.clone(),
|
||||||
posts_store.clone(),
|
posts_store.clone(),
|
||||||
code_renderer,
|
code_renderer,
|
||||||
config.posts_dir.clone()
|
config.posts_dir.clone()
|
||||||
@@ -96,7 +117,7 @@ fn main() -> miette::Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn run(
|
async fn run(
|
||||||
config: Config,
|
config: Arc<Config>,
|
||||||
posts_store: ConcurrentPostsStore,
|
posts_store: ConcurrentPostsStore,
|
||||||
) -> miette::Result<()>
|
) -> miette::Result<()>
|
||||||
{
|
{
|
||||||
|
|||||||
+28
-10
@@ -1,7 +1,7 @@
|
|||||||
use std::{borrow, error, fmt, ops};
|
use std::{borrow, error, fmt, ops};
|
||||||
|
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use libshire::strings::ShString22;
|
use libshire::{strings::ShString22, uuid::{Uuid, UuidV5Error}};
|
||||||
use maud::{Markup, PreEscaped};
|
use maud::{Markup, PreEscaped};
|
||||||
|
|
||||||
use crate::codeblock::CodeBlockRenderer;
|
use crate::codeblock::CodeBlockRenderer;
|
||||||
@@ -73,6 +73,7 @@ impl fmt::Display for PostId {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct Post {
|
pub struct Post {
|
||||||
|
uuid: Uuid,
|
||||||
id: PostId,
|
id: PostId,
|
||||||
title: String,
|
title: String,
|
||||||
author: String,
|
author: String,
|
||||||
@@ -83,8 +84,8 @@ pub struct Post {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Post {
|
impl Post {
|
||||||
pub fn id_str(&self) -> &str {
|
pub fn uuid(&self) -> Uuid {
|
||||||
&self.id
|
self.uuid
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn id(&self) -> &PostId {
|
pub fn id(&self) -> &PostId {
|
||||||
@@ -117,6 +118,7 @@ impl Post {
|
|||||||
|
|
||||||
pub fn parse(
|
pub fn parse(
|
||||||
code_renderer: &CodeBlockRenderer,
|
code_renderer: &CodeBlockRenderer,
|
||||||
|
namespace: Uuid,
|
||||||
post_id: PostId,
|
post_id: PostId,
|
||||||
file_name: &str,
|
file_name: &str,
|
||||||
created: DateTime<Utc>,
|
created: DateTime<Utc>,
|
||||||
@@ -124,17 +126,25 @@ impl Post {
|
|||||||
source: &str,
|
source: &str,
|
||||||
) -> Result<Self, ParseError>
|
) -> Result<Self, ParseError>
|
||||||
{
|
{
|
||||||
let mdpost = MdPost::parse(file_name, source)?;
|
MdPost::parse(file_name, source)
|
||||||
Ok(Self::from_mdpost(code_renderer, post_id, created, updated, mdpost))
|
.and_then(|post| Self::from_mdpost(
|
||||||
|
code_renderer,
|
||||||
|
namespace,
|
||||||
|
post_id,
|
||||||
|
created,
|
||||||
|
updated,
|
||||||
|
post
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn from_mdpost(
|
fn from_mdpost(
|
||||||
code_renderer: &CodeBlockRenderer,
|
code_renderer: &CodeBlockRenderer,
|
||||||
|
namespace: Uuid,
|
||||||
id: PostId,
|
id: PostId,
|
||||||
created: DateTime<Utc>,
|
created: DateTime<Utc>,
|
||||||
updated: DateTime<Utc>,
|
updated: DateTime<Utc>,
|
||||||
mdpost: MdPost,
|
mdpost: MdPost,
|
||||||
) -> Self
|
) -> Result<Self, ParseError>
|
||||||
{
|
{
|
||||||
use pulldown_cmark::{Options, Parser, html::push_html};
|
use pulldown_cmark::{Options, Parser, html::push_html};
|
||||||
|
|
||||||
@@ -142,6 +152,11 @@ impl Post {
|
|||||||
.union(Options::ENABLE_FOOTNOTES)
|
.union(Options::ENABLE_FOOTNOTES)
|
||||||
.union(Options::ENABLE_STRIKETHROUGH);
|
.union(Options::ENABLE_STRIKETHROUGH);
|
||||||
|
|
||||||
|
let uuid = Uuid::new_v5(namespace, &*id)
|
||||||
|
.map_err(|err| match err {
|
||||||
|
UuidV5Error::NameTooLong(len) => ParseError::IdTooLong(len),
|
||||||
|
})?;
|
||||||
|
|
||||||
let mut parser = PostMdParser::new(
|
let mut parser = PostMdParser::new(
|
||||||
Parser::new_ext(&mdpost.markdown, PARSER_OPTIONS),
|
Parser::new_ext(&mdpost.markdown, PARSER_OPTIONS),
|
||||||
code_renderer
|
code_renderer
|
||||||
@@ -150,7 +165,8 @@ impl Post {
|
|||||||
let mut html_buf = String::new();
|
let mut html_buf = String::new();
|
||||||
push_html(&mut html_buf, parser.by_ref());
|
push_html(&mut html_buf, parser.by_ref());
|
||||||
|
|
||||||
Self {
|
Ok(Self {
|
||||||
|
uuid,
|
||||||
id,
|
id,
|
||||||
title: mdpost.title,
|
title: mdpost.title,
|
||||||
author: mdpost.author,
|
author: mdpost.author,
|
||||||
@@ -158,7 +174,7 @@ impl Post {
|
|||||||
tags: mdpost.tags,
|
tags: mdpost.tags,
|
||||||
created,
|
created,
|
||||||
updated,
|
updated,
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,13 +297,15 @@ impl MdPost {
|
|||||||
pub enum ParseError {
|
pub enum ParseError {
|
||||||
MissingHeader,
|
MissingHeader,
|
||||||
InvalidHeader(Box<knuffel::Error>),
|
InvalidHeader(Box<knuffel::Error>),
|
||||||
|
IdTooLong(usize),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for ParseError {
|
impl fmt::Display for ParseError {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
ParseError::MissingHeader => write!(f, "Post file has no header"),
|
Self::MissingHeader => write!(f, "post file has no header"),
|
||||||
ParseError::InvalidHeader(err) => fmt::Display::fmt(err, f),
|
Self::InvalidHeader(err) => fmt::Display::fmt(err, f),
|
||||||
|
Self::IdTooLong(len) => write!(f, "post id too long ({} bytes)", len),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ impl ConcurrentPostsStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get(&self, id: &str) -> Option<Arc<Post>> {
|
pub async fn get(&self, id: &str) -> Option<Arc<Post>> {
|
||||||
self.read().await.get(id)
|
self.read().await.get(id).cloned()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,12 +56,12 @@ impl PostsStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get(&self, id: &str) -> Option<Arc<Post>> {
|
pub fn get(&self, id: &str) -> Option<&Arc<Post>> {
|
||||||
self.posts.get(id).cloned()
|
self.posts.get(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert(&mut self, post: Post) -> Option<Arc<Post>> {
|
pub fn insert(&mut self, post: Post) -> Option<Arc<Post>> {
|
||||||
let old_post = self.remove(post.id_str());
|
let old_post = self.remove(post.id());
|
||||||
|
|
||||||
// Insert the post into each of the tag indexes.
|
// Insert the post into each of the tag indexes.
|
||||||
for tag in post.tags() {
|
for tag in post.tags() {
|
||||||
@@ -109,19 +109,23 @@ impl PostsStore {
|
|||||||
self.posts.clear();
|
self.posts.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn last_updated(&self) -> Option<DateTime<Utc>> {
|
||||||
|
self.iter().map(|post| post.updated()).max()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn iter(&self)
|
pub fn iter(&self)
|
||||||
-> impl '_
|
-> impl '_
|
||||||
+ Iterator<Item = Arc<Post>>
|
+ Iterator<Item = &Arc<Post>>
|
||||||
+ ExactSizeIterator
|
+ ExactSizeIterator
|
||||||
+ FusedIterator
|
+ FusedIterator
|
||||||
+ Clone
|
+ Clone
|
||||||
{
|
{
|
||||||
self.posts.values().cloned()
|
self.posts.values()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn iter_by_created(&self)
|
pub fn iter_by_created(&self)
|
||||||
-> impl '_
|
-> impl '_
|
||||||
+ Iterator<Item = Arc<Post>>
|
+ Iterator<Item = &Arc<Post>>
|
||||||
+ DoubleEndedIterator
|
+ DoubleEndedIterator
|
||||||
+ ExactSizeIterator
|
+ ExactSizeIterator
|
||||||
+ FusedIterator
|
+ FusedIterator
|
||||||
|
|||||||
@@ -3,20 +3,23 @@ use std::{
|
|||||||
fs,
|
fs,
|
||||||
io::{self, Read},
|
io::{self, Read},
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
sync::mpsc,
|
sync::{Arc, mpsc},
|
||||||
};
|
};
|
||||||
|
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::DateTime;
|
||||||
use notify::DebouncedEvent;
|
use notify::DebouncedEvent;
|
||||||
use tracing::{info, warn, error};
|
use tracing::{info, warn, error};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
codeblock::CodeBlockRenderer,
|
codeblock::CodeBlockRenderer,
|
||||||
|
Config,
|
||||||
post::{ParseError, Post, PostId},
|
post::{ParseError, Post, PostId},
|
||||||
posts_store::ConcurrentPostsStore,
|
posts_store::ConcurrentPostsStore,
|
||||||
|
time::unix_epoch,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct Renderer {
|
pub struct Renderer {
|
||||||
|
config: Arc<Config>,
|
||||||
posts: ConcurrentPostsStore,
|
posts: ConcurrentPostsStore,
|
||||||
code_renderer: CodeBlockRenderer,
|
code_renderer: CodeBlockRenderer,
|
||||||
posts_dir_path: PathBuf,
|
posts_dir_path: PathBuf,
|
||||||
@@ -25,6 +28,7 @@ pub struct Renderer {
|
|||||||
|
|
||||||
impl Renderer {
|
impl Renderer {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
|
config: Arc<Config>,
|
||||||
posts: ConcurrentPostsStore,
|
posts: ConcurrentPostsStore,
|
||||||
code_renderer: CodeBlockRenderer,
|
code_renderer: CodeBlockRenderer,
|
||||||
posts_dir_path: PathBuf,
|
posts_dir_path: PathBuf,
|
||||||
@@ -38,6 +42,7 @@ impl Renderer {
|
|||||||
tx.send(DebouncedEvent::Rescan).unwrap();
|
tx.send(DebouncedEvent::Rescan).unwrap();
|
||||||
|
|
||||||
(Self {
|
(Self {
|
||||||
|
config,
|
||||||
posts,
|
posts,
|
||||||
code_renderer,
|
code_renderer,
|
||||||
posts_dir_path,
|
posts_dir_path,
|
||||||
@@ -185,10 +190,11 @@ impl Renderer {
|
|||||||
|
|
||||||
let (created, updated) = metadata.created()
|
let (created, updated) = metadata.created()
|
||||||
.and_then(|created| metadata.modified()
|
.and_then(|created| metadata.modified()
|
||||||
.map(|modified| (DateTime::<Utc>::from(created), DateTime::<Utc>::from(modified))))
|
.map(|modified| (DateTime::from(created), DateTime::from(modified))))
|
||||||
|
// If created / modified metadata is not available, default to the UNIX epoch.
|
||||||
.unwrap_or_else(|_| {
|
.unwrap_or_else(|_| {
|
||||||
let now = Utc::now();
|
let epoch = unix_epoch();
|
||||||
(now, now)
|
(epoch, epoch)
|
||||||
});
|
});
|
||||||
|
|
||||||
let contents = {
|
let contents = {
|
||||||
@@ -202,6 +208,7 @@ impl Renderer {
|
|||||||
|
|
||||||
Post::parse(
|
Post::parse(
|
||||||
&self.code_renderer,
|
&self.code_renderer,
|
||||||
|
*self.config.namespace_uuid,
|
||||||
target.id.clone(),
|
target.id.clone(),
|
||||||
&target.path.to_string_lossy(),
|
&target.path.to_string_lossy(),
|
||||||
created,
|
created,
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use atom_syndication as atom;
|
||||||
|
use axum::{
|
||||||
|
body::Bytes,
|
||||||
|
extract::Extension,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::response::Atom;
|
||||||
|
use crate::{
|
||||||
|
Config,
|
||||||
|
posts_store::ConcurrentPostsStore,
|
||||||
|
time::unix_epoch,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub async fn handle(
|
||||||
|
Extension(config): Extension<Arc<Config>>,
|
||||||
|
Extension(posts): Extension<ConcurrentPostsStore>,
|
||||||
|
) -> Atom<Bytes> {
|
||||||
|
let (atom_entries, updated) = {
|
||||||
|
let guard = posts.read().await;
|
||||||
|
|
||||||
|
let atom_entries = guard.iter_by_created()
|
||||||
|
.take(config.atom.num_posts)
|
||||||
|
.map(|post| {
|
||||||
|
atom::EntryBuilder::default()
|
||||||
|
.id(format!("urn:uuid:{}", post.uuid()))
|
||||||
|
.title(post.title().to_owned())
|
||||||
|
.updated(post.updated())
|
||||||
|
.links(vec![
|
||||||
|
atom::LinkBuilder::default()
|
||||||
|
.href(format!(
|
||||||
|
"{}://{}/articles/{}",
|
||||||
|
config.self_ref.protocol,
|
||||||
|
config.self_ref.domain,
|
||||||
|
post.id()
|
||||||
|
))
|
||||||
|
.rel("alternate".to_owned())
|
||||||
|
.mime_type(Some("text/html".to_owned()))
|
||||||
|
.build()
|
||||||
|
])
|
||||||
|
.author(atom::PersonBuilder::default()
|
||||||
|
.name(post.author().to_owned())
|
||||||
|
.build())
|
||||||
|
.build()
|
||||||
|
})
|
||||||
|
.collect::<Vec<atom::Entry>>();
|
||||||
|
|
||||||
|
let updated = guard.last_updated()
|
||||||
|
.unwrap_or_else(unix_epoch);
|
||||||
|
|
||||||
|
(atom_entries, updated)
|
||||||
|
};
|
||||||
|
|
||||||
|
Atom(atom::FeedBuilder::default()
|
||||||
|
.id(format!("urn:uuid:{}", *config.namespace_uuid))
|
||||||
|
.title(config.atom.title.clone())
|
||||||
|
.updated(updated)
|
||||||
|
.links(vec![
|
||||||
|
atom::LinkBuilder::default()
|
||||||
|
.href(format!(
|
||||||
|
"{}://{}/atom.xml",
|
||||||
|
config.self_ref.protocol,
|
||||||
|
config.self_ref.domain
|
||||||
|
))
|
||||||
|
.rel("self".to_owned())
|
||||||
|
.build(),
|
||||||
|
atom::LinkBuilder::default()
|
||||||
|
.href(format!(
|
||||||
|
"{}://{}/articles/",
|
||||||
|
config.self_ref.protocol,
|
||||||
|
config.self_ref.domain
|
||||||
|
))
|
||||||
|
.rel("alternate".to_owned())
|
||||||
|
.mime_type(Some("text/html".to_owned()))
|
||||||
|
.build()
|
||||||
|
])
|
||||||
|
.entries(atom_entries)
|
||||||
|
.build()
|
||||||
|
.to_string()
|
||||||
|
.into())
|
||||||
|
}
|
||||||
@@ -43,7 +43,7 @@ pub async fn handle(Extension(posts): Extension<ConcurrentPostsStore>) -> Html {
|
|||||||
ul {
|
ul {
|
||||||
@for post in posts.read().await.iter_by_created().rev().take(5) {
|
@for post in posts.read().await.iter_by_created().rev().take(5) {
|
||||||
li {
|
li {
|
||||||
a href={"/articles/" (post.id_str())} { (post.title()) }
|
a href={"/articles/" (post.id())} { (post.title()) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
mod atom;
|
||||||
mod contact;
|
mod contact;
|
||||||
mod index;
|
mod index;
|
||||||
mod post;
|
mod post;
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ pub async fn handle(Extension(posts): Extension<ConcurrentPostsStore>) -> Html {
|
|||||||
ul {
|
ul {
|
||||||
@for post in posts.read().await.iter_by_created().rev() {
|
@for post in posts.read().await.iter_by_created().rev() {
|
||||||
li {
|
li {
|
||||||
a href={"/articles/" (post.id_str())} { (post.title()) }
|
a href={"/articles/" (post.id())} { (post.title()) }
|
||||||
span class="quiet" {
|
span class="quiet" {
|
||||||
" — " (post.created().format("%Y/%m/%d"))
|
" — " (post.created().format("%Y/%m/%d"))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,23 +6,32 @@ use axum::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use super::response::Rss;
|
use super::response::Rss;
|
||||||
use crate::{posts_store::ConcurrentPostsStore, Config};
|
use crate::{
|
||||||
|
Config,
|
||||||
|
posts_store::ConcurrentPostsStore,
|
||||||
|
time::unix_epoch,
|
||||||
|
};
|
||||||
|
|
||||||
pub async fn handle(
|
pub async fn handle(
|
||||||
Extension(config): Extension<Arc<Config>>,
|
Extension(config): Extension<Arc<Config>>,
|
||||||
Extension(posts): Extension<ConcurrentPostsStore>,
|
Extension(posts): Extension<ConcurrentPostsStore>,
|
||||||
) -> Rss<Bytes> {
|
) -> Rss<Bytes> {
|
||||||
let rss_items = posts.read()
|
let (rss_items, updated) = {
|
||||||
.await
|
let guard = posts.read().await;
|
||||||
.iter_by_created()
|
|
||||||
|
let rss_items = guard.iter_by_created()
|
||||||
.take(config.rss.num_posts)
|
.take(config.rss.num_posts)
|
||||||
.map(|post| {
|
.map(|post| {
|
||||||
rss::ItemBuilder::default()
|
rss::ItemBuilder::default()
|
||||||
.title(Some(post.title().to_owned()))
|
.title(Some(post.title().to_owned()))
|
||||||
|
.guid(Some(rss::GuidBuilder::default()
|
||||||
|
.value(post.uuid().to_string())
|
||||||
|
.permalink(false)
|
||||||
|
.build()))
|
||||||
.link(Some(format!(
|
.link(Some(format!(
|
||||||
"{}://{}/articles/{}",
|
"{}://{}/articles/{}",
|
||||||
config.rss.protocol,
|
config.self_ref.protocol,
|
||||||
config.rss.domain,
|
config.self_ref.domain,
|
||||||
post.id()
|
post.id()
|
||||||
)))
|
)))
|
||||||
.pub_date(Some(post.created().to_rfc2822()))
|
.pub_date(Some(post.created().to_rfc2822()))
|
||||||
@@ -30,13 +39,20 @@ pub async fn handle(
|
|||||||
})
|
})
|
||||||
.collect::<Vec<rss::Item>>();
|
.collect::<Vec<rss::Item>>();
|
||||||
|
|
||||||
|
let updated = guard.last_updated()
|
||||||
|
.unwrap_or_else(unix_epoch);
|
||||||
|
|
||||||
|
(rss_items, updated)
|
||||||
|
};
|
||||||
|
|
||||||
Rss(rss::ChannelBuilder::default()
|
Rss(rss::ChannelBuilder::default()
|
||||||
.title(config.rss.title.clone())
|
.title(config.rss.title.clone())
|
||||||
.link(format!(
|
.link(format!(
|
||||||
"{}://{}",
|
"{}://{}",
|
||||||
config.rss.protocol, config.rss.domain
|
config.self_ref.protocol, config.self_ref.domain
|
||||||
))
|
))
|
||||||
.ttl(Some(config.rss.ttl.to_string()))
|
.ttl(Some(config.rss.ttl.to_string()))
|
||||||
|
.last_build_date(Some(updated.to_rfc2822()))
|
||||||
.items(rss_items)
|
.items(rss_items)
|
||||||
.build()
|
.build()
|
||||||
.to_string()
|
.to_string()
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ use crate::{
|
|||||||
posts_store::ConcurrentPostsStore
|
posts_store::ConcurrentPostsStore
|
||||||
};
|
};
|
||||||
use super::{
|
use super::{
|
||||||
|
atom,
|
||||||
contact,
|
contact,
|
||||||
index,
|
index,
|
||||||
post,
|
post,
|
||||||
@@ -26,13 +27,14 @@ use super::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub fn service(
|
pub fn service(
|
||||||
config: Config,
|
config: Arc<Config>,
|
||||||
posts_store: ConcurrentPostsStore,
|
posts_store: ConcurrentPostsStore,
|
||||||
) -> Router
|
) -> Router
|
||||||
{
|
{
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/", get(index::handle))
|
.route("/", get(index::handle))
|
||||||
.route("/rss.xml", get(rss::handle))
|
.route("/rss.xml", get(rss::handle))
|
||||||
|
.route("/atom.xml", get(atom::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))
|
||||||
@@ -40,7 +42,7 @@ pub fn service(
|
|||||||
.fallback(handle_fallback.into_service())
|
.fallback(handle_fallback.into_service())
|
||||||
.layer(ConcurrencyLimitLayer::new(config.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(config))
|
||||||
.layer(Extension(posts_store))
|
.layer(Extension(posts_store))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
use chrono::{DateTime, NaiveDateTime, Utc};
|
||||||
|
|
||||||
|
pub fn unix_epoch() -> DateTime<Utc> {
|
||||||
|
DateTime::from_utc(NaiveDateTime::from_timestamp(0, 0), Utc)
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
use std::ops;
|
||||||
|
|
||||||
|
use knuffel::{
|
||||||
|
ast::{Literal, TypeName},
|
||||||
|
decode::{Context, Kind},
|
||||||
|
errors::{DecodeError, ExpectedType},
|
||||||
|
span::Spanned,
|
||||||
|
traits::ErrorSpan,
|
||||||
|
DecodeScalar,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Default, Debug)]
|
||||||
|
#[repr(transparent)]
|
||||||
|
pub struct Uuid(pub libshire::uuid::Uuid);
|
||||||
|
|
||||||
|
impl Uuid {
|
||||||
|
pub fn as_inner(&self) -> &libshire::uuid::Uuid {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ops::Deref for Uuid {
|
||||||
|
type Target = libshire::uuid::Uuid;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
self.as_inner()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S: ErrorSpan> DecodeScalar<S> for Uuid {
|
||||||
|
fn type_check(type_name: &Option<Spanned<TypeName, S>>, ctx: &mut Context<S>) {
|
||||||
|
if let Some(type_name) = type_name {
|
||||||
|
ctx.emit_error(DecodeError::TypeName {
|
||||||
|
span: type_name.span().clone(),
|
||||||
|
found: Some((&**type_name).clone()),
|
||||||
|
expected: ExpectedType::no_type(),
|
||||||
|
rust_type: "Uuid",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn raw_decode(
|
||||||
|
value: &Spanned<Literal, S>,
|
||||||
|
ctx: &mut Context<S>,
|
||||||
|
) -> Result<Self, DecodeError<S>> {
|
||||||
|
match &**value {
|
||||||
|
Literal::String(s) => match s.parse() {
|
||||||
|
Ok(uuid) => Ok(Self(uuid)),
|
||||||
|
Err(err) => {
|
||||||
|
ctx.emit_error(DecodeError::conversion(value, err));
|
||||||
|
Ok(Default::default())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
ctx.emit_error(DecodeError::scalar_kind(Kind::String, value));
|
||||||
|
Ok(Default::default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user