alloc optimization and enum refactors
This commit is contained in:
parent
ed81dcd223
commit
bed8ae7849
7 changed files with 212 additions and 179 deletions
|
@ -47,7 +47,7 @@ mime_guess = "2.0.5"
|
||||||
notify-debouncer-full = { version = "0.3.1", default-features = false }
|
notify-debouncer-full = { version = "0.3.1", default-features = false }
|
||||||
rss = "2.0.7"
|
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", "rc"] }
|
||||||
serde-value = "0.7.0"
|
serde-value = "0.7.0"
|
||||||
serde_json = { version = "1.0.124", features = ["preserve_order"] }
|
serde_json = { version = "1.0.124", features = ["preserve_order"] }
|
||||||
syntect = "5.2.0"
|
syntect = "5.2.0"
|
||||||
|
|
68
src/app.rs
68
src/app.rs
|
@ -1,4 +1,3 @@
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
@ -13,7 +12,6 @@ use include_dir::{include_dir, Dir};
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
use rss::{Category, ChannelBuilder, ItemBuilder};
|
use rss::{Category, ChannelBuilder, ItemBuilder};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Map;
|
|
||||||
use serde_value::Value;
|
use serde_value::Value;
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
use tower::service_fn;
|
use tower::service_fn;
|
||||||
|
@ -57,7 +55,7 @@ struct IndexTemplate<'a> {
|
||||||
posts: Vec<PostMetadata>,
|
posts: Vec<PostMetadata>,
|
||||||
rss: bool,
|
rss: bool,
|
||||||
js: bool,
|
js: bool,
|
||||||
tags: Map<String, serde_json::Value>,
|
tags: IndexMap<Arc<str>, u64>,
|
||||||
joined_tags: String,
|
joined_tags: String,
|
||||||
style: &'a StyleConfig,
|
style: &'a StyleConfig,
|
||||||
}
|
}
|
||||||
|
@ -66,13 +64,13 @@ struct IndexTemplate<'a> {
|
||||||
struct PostTemplate<'a> {
|
struct PostTemplate<'a> {
|
||||||
bingus_info: &'a BingusInfo,
|
bingus_info: &'a BingusInfo,
|
||||||
meta: &'a PostMetadata,
|
meta: &'a PostMetadata,
|
||||||
rendered: String,
|
rendered: Arc<str>,
|
||||||
rendered_in: RenderStats,
|
rendered_in: RenderStats,
|
||||||
js: bool,
|
js: bool,
|
||||||
color: Option<&'a str>,
|
color: Option<&'a str>,
|
||||||
joined_tags: String,
|
joined_tags: String,
|
||||||
style: &'a StyleConfig,
|
style: &'a StyleConfig,
|
||||||
raw_name: Option<&'a str>,
|
raw_name: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
@ -84,12 +82,12 @@ struct QueryParams {
|
||||||
other: IndexMap<String, Value>,
|
other: IndexMap<String, Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn collect_tags(posts: &Vec<PostMetadata>) -> Map<String, serde_json::Value> {
|
fn collect_tags(posts: &Vec<PostMetadata>) -> IndexMap<Arc<str>, u64> {
|
||||||
let mut tags = HashMap::new();
|
let mut tags = IndexMap::new();
|
||||||
|
|
||||||
for post in posts {
|
for post in posts {
|
||||||
for tag in &post.tags {
|
for tag in &post.tags {
|
||||||
if let Some((existing_tag, count)) = tags.remove_entry(tag) {
|
if let Some((existing_tag, count)) = tags.swap_remove_entry(tag) {
|
||||||
tags.insert(existing_tag, count + 1);
|
tags.insert(existing_tag, count + 1);
|
||||||
} else {
|
} else {
|
||||||
tags.insert(tag.clone(), 1);
|
tags.insert(tag.clone(), 1);
|
||||||
|
@ -97,21 +95,13 @@ fn collect_tags(posts: &Vec<PostMetadata>) -> Map<String, serde_json::Value> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut tags: Vec<(String, u64)> = tags.into_iter().collect();
|
tags.sort_unstable_by(|k1, _v1, k2, _v2| k1.cmp(k2));
|
||||||
|
tags.sort_by(|_k1, v1, _k2, v2| v1.cmp(v2));
|
||||||
|
|
||||||
tags.sort_unstable_by_key(|(v, _)| v.clone());
|
tags
|
||||||
tags.sort_by_key(|(_, v)| -(*v as i64));
|
|
||||||
|
|
||||||
let mut map = Map::new();
|
|
||||||
|
|
||||||
for tag in tags.into_iter() {
|
|
||||||
map.insert(tag.0, tag.1.into());
|
|
||||||
}
|
|
||||||
|
|
||||||
map
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn join_tags_for_meta(tags: &Map<String, serde_json::Value>, delim: &str) -> String {
|
fn join_tags_for_meta(tags: &IndexMap<Arc<str>, u64>, delim: &str) -> String {
|
||||||
let mut s = String::new();
|
let mut s = String::new();
|
||||||
let tags = tags.keys().enumerate();
|
let tags = tags.keys().enumerate();
|
||||||
let len = tags.len();
|
let len = tags.len();
|
||||||
|
@ -207,21 +197,21 @@ async fn rss(
|
||||||
for (metadata, content, _) in posts {
|
for (metadata, content, _) in posts {
|
||||||
channel.item(
|
channel.item(
|
||||||
ItemBuilder::default()
|
ItemBuilder::default()
|
||||||
.title(metadata.title)
|
.title(metadata.title.to_string())
|
||||||
.description(metadata.description)
|
.description(metadata.description.to_string())
|
||||||
.author(metadata.author)
|
.author(metadata.author.to_string())
|
||||||
.categories(
|
.categories(
|
||||||
metadata
|
metadata
|
||||||
.tags
|
.tags
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|tag| Category {
|
.map(|tag| Category {
|
||||||
name: tag,
|
name: tag.to_string(),
|
||||||
domain: None,
|
domain: None,
|
||||||
})
|
})
|
||||||
.collect::<Vec<Category>>(),
|
.collect::<Vec<Category>>(),
|
||||||
)
|
)
|
||||||
.pub_date(metadata.created_at.map(|date| date.to_rfc2822()))
|
.pub_date(metadata.created_at.map(|date| date.to_rfc2822()))
|
||||||
.content(content)
|
.content(content.to_string())
|
||||||
.link(
|
.link(
|
||||||
config
|
config
|
||||||
.rss
|
.rss
|
||||||
|
@ -246,15 +236,18 @@ async fn post(
|
||||||
templates: reg,
|
templates: reg,
|
||||||
..
|
..
|
||||||
}): State<AppState>,
|
}): State<AppState>,
|
||||||
Path(name): Path<String>,
|
Path(name): Path<Arc<str>>,
|
||||||
Query(query): Query<QueryParams>,
|
Query(query): Query<QueryParams>,
|
||||||
) -> AppResult<impl IntoResponse> {
|
) -> AppResult<impl IntoResponse> {
|
||||||
match posts.get_post(&name, &query.other).await? {
|
match posts.get_post(name.clone(), &query.other).await? {
|
||||||
ReturnedPost::Rendered(ref meta, rendered, rendered_in) => {
|
ReturnedPost::Rendered {
|
||||||
|
ref meta,
|
||||||
|
body: rendered,
|
||||||
|
perf: rendered_in,
|
||||||
|
} => {
|
||||||
let joined_tags = meta.tags.join(", ");
|
let joined_tags = meta.tags.join(", ");
|
||||||
|
|
||||||
let reg = reg.read().await;
|
let reg = reg.read().await;
|
||||||
let raw_name;
|
|
||||||
let rendered = reg.render(
|
let rendered = reg.render(
|
||||||
"post",
|
"post",
|
||||||
&PostTemplate {
|
&PostTemplate {
|
||||||
|
@ -269,20 +262,19 @@ async fn post(
|
||||||
.or(config.style.default_color.as_deref()),
|
.or(config.style.default_color.as_deref()),
|
||||||
joined_tags,
|
joined_tags,
|
||||||
style: &config.style,
|
style: &config.style,
|
||||||
raw_name: if config.markdown_access {
|
raw_name: config
|
||||||
raw_name = posts.as_raw(&meta.name).await?;
|
.markdown_access
|
||||||
raw_name.as_deref()
|
.then(|| posts.as_raw(&meta.name))
|
||||||
} else {
|
.unwrap_or(None),
|
||||||
None
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
drop(reg);
|
drop(reg);
|
||||||
Ok(Html(rendered?).into_response())
|
Ok(Html(rendered?).into_response())
|
||||||
}
|
}
|
||||||
ReturnedPost::Raw(body, content_type) => {
|
ReturnedPost::Raw {
|
||||||
Ok(([(CONTENT_TYPE, content_type)], body).into_response())
|
buffer,
|
||||||
}
|
content_type,
|
||||||
|
} => Ok(([(CONTENT_TYPE, content_type)], buffer).into_response()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use askama_axum::Template;
|
use askama_axum::Template;
|
||||||
use axum::http::StatusCode;
|
use axum::http::StatusCode;
|
||||||
use axum::response::{IntoResponse, Response};
|
use axum::response::{IntoResponse, Response};
|
||||||
|
@ -14,7 +16,7 @@ pub enum PostError {
|
||||||
#[error("failed to render post: {0}")]
|
#[error("failed to render post: {0}")]
|
||||||
RenderError(String),
|
RenderError(String),
|
||||||
#[error("post {0:?} not found")]
|
#[error("post {0:?} not found")]
|
||||||
NotFound(String),
|
NotFound(Arc<str>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<fronma::error::Error> for PostError {
|
impl From<fronma::error::Error> for PostError {
|
||||||
|
|
109
src/post/blag.rs
109
src/post/blag.rs
|
@ -27,21 +27,21 @@ use super::{ApplyFilters, PostManager, PostMetadata, RenderStats, ReturnedPost};
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
struct BlagMetadata {
|
struct BlagMetadata {
|
||||||
pub title: String,
|
pub title: Arc<str>,
|
||||||
pub description: String,
|
pub description: Arc<str>,
|
||||||
pub author: String,
|
pub author: Arc<str>,
|
||||||
pub icon: Option<String>,
|
pub icon: Option<Arc<str>>,
|
||||||
pub icon_alt: Option<String>,
|
pub icon_alt: Option<Arc<str>>,
|
||||||
pub color: Option<String>,
|
pub color: Option<Arc<str>>,
|
||||||
pub created_at: Option<DateTime<Utc>>,
|
pub created_at: Option<DateTime<Utc>>,
|
||||||
pub modified_at: Option<DateTime<Utc>>,
|
pub modified_at: Option<DateTime<Utc>>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub tags: BTreeSet<String>,
|
pub tags: BTreeSet<Arc<str>>,
|
||||||
pub dont_cache: bool,
|
pub dont_cache: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BlagMetadata {
|
impl BlagMetadata {
|
||||||
pub fn into_full(self, name: String) -> (PostMetadata, bool) {
|
pub fn into_full(self, name: Arc<str>) -> (PostMetadata, bool) {
|
||||||
(
|
(
|
||||||
PostMetadata {
|
PostMetadata {
|
||||||
name,
|
name,
|
||||||
|
@ -79,7 +79,7 @@ impl Blag {
|
||||||
|
|
||||||
async fn render(
|
async fn render(
|
||||||
&self,
|
&self,
|
||||||
name: &str,
|
name: Arc<str>,
|
||||||
path: impl AsRef<Path>,
|
path: impl AsRef<Path>,
|
||||||
query_json: String,
|
query_json: String,
|
||||||
) -> Result<(PostMetadata, String, (Duration, Duration), bool), PostError> {
|
) -> Result<(PostMetadata, String, (Duration, Duration), bool), PostError> {
|
||||||
|
@ -105,7 +105,7 @@ impl Blag {
|
||||||
|
|
||||||
let blag_meta: BlagMetadata = serde_json::from_str(&buf)?;
|
let blag_meta: BlagMetadata = serde_json::from_str(&buf)?;
|
||||||
debug!("blag meta: {blag_meta:?}");
|
debug!("blag meta: {blag_meta:?}");
|
||||||
let (meta, dont_cache) = blag_meta.into_full(name.to_string());
|
let (meta, dont_cache) = blag_meta.into_full(name);
|
||||||
let parsed = start.elapsed();
|
let parsed = start.elapsed();
|
||||||
|
|
||||||
let rendering = Instant::now();
|
let rendering = Instant::now();
|
||||||
|
@ -132,7 +132,7 @@ impl PostManager for Blag {
|
||||||
&self,
|
&self,
|
||||||
filters: &[Filter<'_>],
|
filters: &[Filter<'_>],
|
||||||
query: &IndexMap<String, Value>,
|
query: &IndexMap<String, Value>,
|
||||||
) -> Result<Vec<(PostMetadata, String, RenderStats)>, PostError> {
|
) -> Result<Vec<(PostMetadata, Arc<str>, RenderStats)>, PostError> {
|
||||||
let mut set = FuturesUnordered::new();
|
let mut set = FuturesUnordered::new();
|
||||||
let mut posts = Vec::new();
|
let mut posts = Vec::new();
|
||||||
let mut files = tokio::fs::read_dir(&self.root).await?;
|
let mut files = tokio::fs::read_dir(&self.root).await?;
|
||||||
|
@ -149,17 +149,16 @@ impl PostManager for Blag {
|
||||||
|
|
||||||
let file_type = entry.file_type().await?;
|
let file_type = entry.file_type().await?;
|
||||||
if file_type.is_file() {
|
if file_type.is_file() {
|
||||||
let name = match entry.file_name().into_string() {
|
let mut name = match entry.file_name().into_string() {
|
||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if name.ends_with(".sh") {
|
if self.is_raw(&name) {
|
||||||
set.push(
|
name.truncate(3);
|
||||||
async move { self.get_post(name.trim_end_matches(".sh"), query).await },
|
set.push(self.get_post(name.into(), query));
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -167,8 +166,8 @@ impl PostManager for Blag {
|
||||||
while let Some(result) = set.next().await {
|
while let Some(result) = set.next().await {
|
||||||
let post = match result {
|
let post = match result {
|
||||||
Ok(v) => match v {
|
Ok(v) => match v {
|
||||||
ReturnedPost::Rendered(meta, content, stats) => (meta, content, stats),
|
ReturnedPost::Rendered { meta, body, perf } => (meta, body, perf),
|
||||||
ReturnedPost::Raw(..) => unreachable!(),
|
ReturnedPost::Raw { .. } => unreachable!(),
|
||||||
},
|
},
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("error while rendering blagpost: {err}");
|
error!("error while rendering blagpost: {err}");
|
||||||
|
@ -189,29 +188,29 @@ impl PostManager for Blag {
|
||||||
#[instrument(level = "info", skip(self))]
|
#[instrument(level = "info", skip(self))]
|
||||||
async fn get_post(
|
async fn get_post(
|
||||||
&self,
|
&self,
|
||||||
name: &str,
|
name: Arc<str>,
|
||||||
query: &IndexMap<String, Value>,
|
query: &IndexMap<String, Value>,
|
||||||
) -> Result<ReturnedPost, PostError> {
|
) -> Result<ReturnedPost, PostError> {
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
let mut path = self.root.join(name);
|
let mut path = self.root.join(&*name);
|
||||||
|
|
||||||
if name.ends_with(".sh") {
|
if self.is_raw(&name) {
|
||||||
let mut buf = Vec::new();
|
let mut buffer = Vec::new();
|
||||||
let mut file =
|
let mut file =
|
||||||
OpenOptions::new()
|
OpenOptions::new()
|
||||||
.read(true)
|
.read(true)
|
||||||
.open(&path)
|
.open(&path)
|
||||||
.await
|
.await
|
||||||
.map_err(|err| match err.kind() {
|
.map_err(|err| match err.kind() {
|
||||||
std::io::ErrorKind::NotFound => PostError::NotFound(name.to_string()),
|
std::io::ErrorKind::NotFound => PostError::NotFound(name),
|
||||||
_ => PostError::IoError(err),
|
_ => PostError::IoError(err),
|
||||||
})?;
|
})?;
|
||||||
file.read_to_end(&mut buf).await?;
|
file.read_to_end(&mut buffer).await?;
|
||||||
|
|
||||||
return Ok(ReturnedPost::Raw(
|
return Ok(ReturnedPost::Raw {
|
||||||
buf,
|
buffer,
|
||||||
HeaderValue::from_static("text/x-shellscript"),
|
content_type: HeaderValue::from_static("text/x-shellscript"),
|
||||||
));
|
});
|
||||||
} else {
|
} else {
|
||||||
path.add_extension("sh");
|
path.add_extension("sh");
|
||||||
}
|
}
|
||||||
|
@ -219,12 +218,12 @@ impl PostManager for Blag {
|
||||||
let stat = tokio::fs::metadata(&path)
|
let stat = tokio::fs::metadata(&path)
|
||||||
.await
|
.await
|
||||||
.map_err(|err| match err.kind() {
|
.map_err(|err| match err.kind() {
|
||||||
std::io::ErrorKind::NotFound => PostError::NotFound(name.to_string()),
|
std::io::ErrorKind::NotFound => PostError::NotFound(name.clone()),
|
||||||
_ => PostError::IoError(err),
|
_ => PostError::IoError(err),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
if !stat.is_file() {
|
if !stat.is_file() {
|
||||||
return Err(PostError::NotFound(name.to_string()));
|
return Err(PostError::NotFound(name));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mtime = as_secs(&stat.modified()?);
|
let mtime = as_secs(&stat.modified()?);
|
||||||
|
@ -235,67 +234,69 @@ impl PostManager for Blag {
|
||||||
let query_hash = hasher.finish();
|
let query_hash = hasher.finish();
|
||||||
|
|
||||||
let post = if let Some(cache) = &self.cache {
|
let post = if let Some(cache) = &self.cache {
|
||||||
if let Some(CacheValue {
|
if let Some(CacheValue { meta, body, .. }) =
|
||||||
metadata, rendered, ..
|
cache.lookup(&name, mtime, query_hash).await
|
||||||
}) = cache.lookup(name, mtime, query_hash).await
|
|
||||||
{
|
{
|
||||||
ReturnedPost::Rendered(metadata, rendered, RenderStats::Cached(start.elapsed()))
|
ReturnedPost::Rendered {
|
||||||
|
meta,
|
||||||
|
body,
|
||||||
|
perf: RenderStats::Cached(start.elapsed()),
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
let (meta, content, (parsed, rendered), dont_cache) =
|
let (meta, content, (parsed, rendered), dont_cache) =
|
||||||
self.render(name, path, query_json).await?;
|
self.render(name.clone(), path, query_json).await?;
|
||||||
|
let body = content.into();
|
||||||
|
|
||||||
if !dont_cache {
|
if !dont_cache {
|
||||||
cache
|
cache
|
||||||
.insert(
|
.insert(name, meta.clone(), mtime, Arc::clone(&body), query_hash)
|
||||||
name.to_string(),
|
|
||||||
meta.clone(),
|
|
||||||
mtime,
|
|
||||||
content.clone(),
|
|
||||||
query_hash,
|
|
||||||
)
|
|
||||||
.await
|
.await
|
||||||
.unwrap_or_else(|err| warn!("failed to insert {:?} into cache", err.0));
|
.unwrap_or_else(|err| warn!("failed to insert {:?} into cache", err.0));
|
||||||
}
|
}
|
||||||
|
|
||||||
let total = start.elapsed();
|
let total = start.elapsed();
|
||||||
ReturnedPost::Rendered(
|
ReturnedPost::Rendered {
|
||||||
meta,
|
meta,
|
||||||
content,
|
body,
|
||||||
RenderStats::Rendered {
|
perf: RenderStats::Rendered {
|
||||||
total,
|
total,
|
||||||
parsed,
|
parsed,
|
||||||
rendered,
|
rendered,
|
||||||
},
|
},
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let (meta, content, (parsed, rendered), ..) =
|
let (meta, content, (parsed, rendered), ..) =
|
||||||
self.render(name, path, query_json).await?;
|
self.render(name, path, query_json).await?;
|
||||||
|
|
||||||
let total = start.elapsed();
|
let total = start.elapsed();
|
||||||
ReturnedPost::Rendered(
|
ReturnedPost::Rendered {
|
||||||
meta,
|
meta,
|
||||||
content,
|
body: content.into(),
|
||||||
RenderStats::Rendered {
|
perf: RenderStats::Rendered {
|
||||||
total,
|
total,
|
||||||
parsed,
|
parsed,
|
||||||
rendered,
|
rendered,
|
||||||
},
|
},
|
||||||
)
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if let ReturnedPost::Rendered(.., stats) = &post {
|
if let ReturnedPost::Rendered { perf, .. } = &post {
|
||||||
info!("rendered blagpost in {:?}", stats);
|
info!("rendered blagpost in {:?}", perf);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(post)
|
Ok(post)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn as_raw(&self, name: &str) -> Result<Option<String>, PostError> {
|
fn is_raw(&self, name: &str) -> bool {
|
||||||
|
name.ends_with(".sh")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_raw(&self, name: &str) -> Option<String> {
|
||||||
let mut buf = String::with_capacity(name.len() + 3);
|
let mut buf = String::with_capacity(name.len() + 3);
|
||||||
buf += name;
|
buf += name;
|
||||||
buf += ".sh";
|
buf += ".sh";
|
||||||
|
|
||||||
Ok(Some(buf))
|
Some(buf)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use std::io::{Read, Write};
|
use std::io::{Read, Write};
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::config::CacheConfig;
|
use crate::config::CacheConfig;
|
||||||
use crate::post::PostMetadata;
|
use crate::post::PostMetadata;
|
||||||
|
@ -10,18 +11,18 @@ use tokio::io::AsyncReadExt;
|
||||||
use tracing::{debug, info, instrument};
|
use tracing::{debug, info, instrument};
|
||||||
|
|
||||||
/// 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 = 3;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
pub struct CacheValue {
|
pub struct CacheValue {
|
||||||
pub metadata: PostMetadata,
|
pub meta: PostMetadata,
|
||||||
pub rendered: String,
|
pub body: Arc<str>,
|
||||||
pub mtime: u64,
|
pub mtime: u64,
|
||||||
pub extra: u64,
|
pub extra: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
pub struct FileCache(HashMap<String, CacheValue>, u16);
|
pub struct FileCache(HashMap<Arc<str>, CacheValue>, u16);
|
||||||
|
|
||||||
impl Default for FileCache {
|
impl Default for FileCache {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
|
@ -50,7 +51,7 @@ impl FileCache {
|
||||||
Some(entry) => {
|
Some(entry) => {
|
||||||
let cached = entry.get();
|
let cached = entry.get();
|
||||||
if mtime <= cached.mtime {
|
if mtime <= cached.mtime {
|
||||||
Some(cached.metadata.clone())
|
Some(cached.meta.clone())
|
||||||
} else {
|
} else {
|
||||||
let _ = entry.remove();
|
let _ = entry.remove();
|
||||||
None
|
None
|
||||||
|
@ -62,15 +63,15 @@ impl FileCache {
|
||||||
|
|
||||||
pub async fn insert(
|
pub async fn insert(
|
||||||
&self,
|
&self,
|
||||||
name: String,
|
name: Arc<str>,
|
||||||
metadata: PostMetadata,
|
metadata: PostMetadata,
|
||||||
mtime: u64,
|
mtime: u64,
|
||||||
rendered: String,
|
rendered: Arc<str>,
|
||||||
extra: u64,
|
extra: u64,
|
||||||
) -> Result<(), (String, (PostMetadata, String))> {
|
) -> Result<(), (Arc<str>, (PostMetadata, Arc<str>))> {
|
||||||
let value = CacheValue {
|
let value = CacheValue {
|
||||||
metadata,
|
meta: metadata,
|
||||||
rendered,
|
body: rendered,
|
||||||
mtime,
|
mtime,
|
||||||
extra,
|
extra,
|
||||||
};
|
};
|
||||||
|
@ -84,13 +85,13 @@ impl FileCache {
|
||||||
self.0
|
self.0
|
||||||
.insert_async(name, value)
|
.insert_async(name, value)
|
||||||
.await
|
.await
|
||||||
.map_err(|x| (x.0, (x.1.metadata, x.1.rendered)))
|
.map_err(|x| (x.0, (x.1.meta, x.1.body)))
|
||||||
} else {
|
} else {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn remove(&self, name: &str) -> Option<(String, CacheValue)> {
|
pub async fn remove(&self, name: &str) -> Option<(Arc<str>, CacheValue)> {
|
||||||
self.0.remove_async(name).await
|
self.0.remove_async(name).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,29 +24,29 @@ use crate::config::Config;
|
||||||
use crate::markdown_render::{build_syntect, render};
|
use crate::markdown_render::{build_syntect, render};
|
||||||
use crate::systemtime_as_secs::as_secs;
|
use crate::systemtime_as_secs::as_secs;
|
||||||
|
|
||||||
use super::cache::CacheGuard;
|
use super::cache::{CacheGuard, CacheValue};
|
||||||
use super::{
|
use super::{
|
||||||
ApplyFilters, Filter, PostError, PostManager, PostMetadata, RenderStats, ReturnedPost,
|
ApplyFilters, Filter, PostError, PostManager, PostMetadata, RenderStats, ReturnedPost,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct FrontMatter {
|
struct FrontMatter {
|
||||||
pub title: String,
|
pub title: Arc<str>,
|
||||||
pub description: String,
|
pub description: Arc<str>,
|
||||||
pub author: String,
|
pub author: Arc<str>,
|
||||||
pub icon: Option<String>,
|
pub icon: Option<Arc<str>>,
|
||||||
pub icon_alt: Option<String>,
|
pub icon_alt: Option<Arc<str>>,
|
||||||
pub color: Option<String>,
|
pub color: Option<Arc<str>>,
|
||||||
pub created_at: Option<DateTime<Utc>>,
|
pub created_at: Option<DateTime<Utc>>,
|
||||||
pub modified_at: Option<DateTime<Utc>>,
|
pub modified_at: Option<DateTime<Utc>>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub tags: BTreeSet<String>,
|
pub tags: BTreeSet<Arc<str>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FrontMatter {
|
impl FrontMatter {
|
||||||
pub fn into_full(
|
pub fn into_full(
|
||||||
self,
|
self,
|
||||||
name: String,
|
name: Arc<str>,
|
||||||
created: Option<SystemTime>,
|
created: Option<SystemTime>,
|
||||||
modified: Option<SystemTime>,
|
modified: Option<SystemTime>,
|
||||||
) -> PostMetadata {
|
) -> PostMetadata {
|
||||||
|
@ -94,9 +94,9 @@ impl MarkdownPosts {
|
||||||
|
|
||||||
async fn parse_and_render(
|
async fn parse_and_render(
|
||||||
&self,
|
&self,
|
||||||
name: String,
|
name: Arc<str>,
|
||||||
path: impl AsRef<Path>,
|
path: impl AsRef<Path>,
|
||||||
) -> Result<(PostMetadata, String, (Duration, Duration)), PostError> {
|
) -> Result<(PostMetadata, Arc<str>, (Duration, Duration)), PostError> {
|
||||||
let parsing_start = Instant::now();
|
let parsing_start = Instant::now();
|
||||||
let mut file = match tokio::fs::OpenOptions::new().read(true).open(&path).await {
|
let mut file = match tokio::fs::OpenOptions::new().read(true).open(&path).await {
|
||||||
Ok(val) => val,
|
Ok(val) => val,
|
||||||
|
@ -117,16 +117,16 @@ impl MarkdownPosts {
|
||||||
let parsing = parsing_start.elapsed();
|
let parsing = parsing_start.elapsed();
|
||||||
|
|
||||||
let before_render = Instant::now();
|
let before_render = Instant::now();
|
||||||
let post = render(body, Some(&self.syntect));
|
let post = render(body, Some(&self.syntect)).into();
|
||||||
let rendering = before_render.elapsed();
|
let rendering = before_render.elapsed();
|
||||||
|
|
||||||
if let Some(cache) = &self.cache {
|
if let Some(cache) = &self.cache {
|
||||||
cache
|
cache
|
||||||
.insert(
|
.insert(
|
||||||
name.to_string(),
|
name.clone(),
|
||||||
metadata.clone(),
|
metadata.clone(),
|
||||||
as_secs(&modified),
|
as_secs(&modified),
|
||||||
post.clone(),
|
Arc::clone(&post),
|
||||||
self.render_hash,
|
self.render_hash,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
@ -143,7 +143,7 @@ impl PostManager for MarkdownPosts {
|
||||||
&self,
|
&self,
|
||||||
filters: &[Filter<'_>],
|
filters: &[Filter<'_>],
|
||||||
query: &IndexMap<String, Value>,
|
query: &IndexMap<String, Value>,
|
||||||
) -> Result<Vec<(PostMetadata, String, RenderStats)>, PostError> {
|
) -> Result<Vec<(PostMetadata, Arc<str>, RenderStats)>, PostError> {
|
||||||
let mut posts = Vec::new();
|
let mut posts = Vec::new();
|
||||||
|
|
||||||
let mut read_dir = fs::read_dir(&self.config.dirs.posts).await?;
|
let mut read_dir = fs::read_dir(&self.config.dirs.posts).await?;
|
||||||
|
@ -157,13 +157,14 @@ impl PostManager for MarkdownPosts {
|
||||||
.file_stem()
|
.file_stem()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.to_string_lossy()
|
.to_string_lossy()
|
||||||
.to_string();
|
.to_string()
|
||||||
|
.into();
|
||||||
|
|
||||||
let post = self.get_post(&name, query).await?;
|
let post = self.get_post(Arc::clone(&name), query).await?;
|
||||||
if let ReturnedPost::Rendered(meta, content, stats) = post
|
if let ReturnedPost::Rendered { meta, body, perf } = post
|
||||||
&& meta.apply_filters(filters)
|
&& meta.apply_filters(filters)
|
||||||
{
|
{
|
||||||
posts.push((meta, content, stats));
|
posts.push((meta, body, perf));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -185,7 +186,8 @@ impl PostManager for MarkdownPosts {
|
||||||
|
|
||||||
if stat.is_file() && path.extension().is_some_and(|ext| ext == "md") {
|
if stat.is_file() && path.extension().is_some_and(|ext| ext == "md") {
|
||||||
let mtime = as_secs(&stat.modified()?);
|
let mtime = as_secs(&stat.modified()?);
|
||||||
let name = String::from(path.file_stem().unwrap().to_string_lossy());
|
let name: Arc<str> =
|
||||||
|
String::from(path.file_stem().unwrap().to_string_lossy()).into();
|
||||||
|
|
||||||
if let Some(cache) = &self.cache
|
if let Some(cache) = &self.cache
|
||||||
&& let Some(hit) = cache.lookup_metadata(&name, mtime).await
|
&& let Some(hit) = cache.lookup_metadata(&name, mtime).await
|
||||||
|
@ -218,42 +220,49 @@ impl PostManager for MarkdownPosts {
|
||||||
#[instrument(level = "info", skip(self))]
|
#[instrument(level = "info", skip(self))]
|
||||||
async fn get_post(
|
async fn get_post(
|
||||||
&self,
|
&self,
|
||||||
name: &str,
|
name: Arc<str>,
|
||||||
_query: &IndexMap<String, Value>,
|
_query: &IndexMap<String, Value>,
|
||||||
) -> Result<ReturnedPost, PostError> {
|
) -> Result<ReturnedPost, PostError> {
|
||||||
let post = if self.config.markdown_access && name.ends_with(".md") {
|
let post = if self.config.markdown_access && self.is_raw(&name) {
|
||||||
let path = self.config.dirs.posts.join(name);
|
let path = self.config.dirs.posts.join(&*name);
|
||||||
|
|
||||||
let mut file = match tokio::fs::OpenOptions::new().read(true).open(&path).await {
|
let mut file = match tokio::fs::OpenOptions::new().read(true).open(&path).await {
|
||||||
Ok(value) => value,
|
Ok(value) => value,
|
||||||
Err(err) => match err.kind() {
|
Err(err) => match err.kind() {
|
||||||
io::ErrorKind::NotFound => {
|
io::ErrorKind::NotFound => {
|
||||||
if let Some(cache) = &self.cache {
|
if let Some(cache) = &self.cache {
|
||||||
cache.remove(name).await;
|
cache.remove(&name).await;
|
||||||
}
|
}
|
||||||
return Err(PostError::NotFound(name.to_string()));
|
return Err(PostError::NotFound(name));
|
||||||
}
|
}
|
||||||
_ => return Err(PostError::IoError(err)),
|
_ => return Err(PostError::IoError(err)),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut buf = Vec::with_capacity(4096);
|
let mut buffer = Vec::with_capacity(4096);
|
||||||
|
|
||||||
file.read_to_end(&mut buf).await?;
|
file.read_to_end(&mut buffer).await?;
|
||||||
|
|
||||||
ReturnedPost::Raw(buf, HeaderValue::from_static("text/plain"))
|
ReturnedPost::Raw {
|
||||||
|
buffer,
|
||||||
|
content_type: HeaderValue::from_static("text/plain"),
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
let path = self.config.dirs.posts.join(name.to_owned() + ".md");
|
let path = self
|
||||||
|
.config
|
||||||
|
.dirs
|
||||||
|
.posts
|
||||||
|
.join(self.as_raw(&name).unwrap_or_else(|| unreachable!()));
|
||||||
|
|
||||||
let stat = match tokio::fs::metadata(&path).await {
|
let stat = match tokio::fs::metadata(&path).await {
|
||||||
Ok(value) => value,
|
Ok(value) => value,
|
||||||
Err(err) => match err.kind() {
|
Err(err) => match err.kind() {
|
||||||
io::ErrorKind::NotFound => {
|
io::ErrorKind::NotFound => {
|
||||||
if let Some(cache) = &self.cache {
|
if let Some(cache) = &self.cache {
|
||||||
cache.remove(name).await;
|
cache.remove(&name).await;
|
||||||
}
|
}
|
||||||
return Err(PostError::NotFound(name.to_string()));
|
return Err(PostError::NotFound(name));
|
||||||
}
|
}
|
||||||
_ => return Err(PostError::IoError(err)),
|
_ => return Err(PostError::IoError(err)),
|
||||||
},
|
},
|
||||||
|
@ -261,30 +270,30 @@ impl PostManager for MarkdownPosts {
|
||||||
let mtime = as_secs(&stat.modified()?);
|
let mtime = as_secs(&stat.modified()?);
|
||||||
|
|
||||||
if let Some(cache) = &self.cache
|
if let Some(cache) = &self.cache
|
||||||
&& let Some(hit) = cache.lookup(name, mtime, self.render_hash).await
|
&& let Some(CacheValue { meta, body, .. }) =
|
||||||
|
cache.lookup(&name, mtime, self.render_hash).await
|
||||||
{
|
{
|
||||||
ReturnedPost::Rendered(
|
ReturnedPost::Rendered {
|
||||||
hit.metadata,
|
meta,
|
||||||
hit.rendered,
|
body,
|
||||||
RenderStats::Cached(start.elapsed()),
|
perf: RenderStats::Cached(start.elapsed()),
|
||||||
)
|
}
|
||||||
} else {
|
} else {
|
||||||
let (metadata, rendered, stats) =
|
let (meta, body, stats) = self.parse_and_render(name, path).await?;
|
||||||
self.parse_and_render(name.to_string(), path).await?;
|
ReturnedPost::Rendered {
|
||||||
ReturnedPost::Rendered(
|
meta,
|
||||||
metadata,
|
body,
|
||||||
rendered,
|
perf: RenderStats::Rendered {
|
||||||
RenderStats::Rendered {
|
|
||||||
total: start.elapsed(),
|
total: start.elapsed(),
|
||||||
parsed: stats.0,
|
parsed: stats.0,
|
||||||
rendered: stats.1,
|
rendered: stats.1,
|
||||||
},
|
},
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if let ReturnedPost::Rendered(.., stats) = &post {
|
if let ReturnedPost::Rendered { perf, .. } = &post {
|
||||||
info!("rendered post in {:?}", stats);
|
info!("rendered post in {:?}", perf);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(post)
|
Ok(post)
|
||||||
|
@ -294,20 +303,29 @@ impl PostManager for MarkdownPosts {
|
||||||
if let Some(cache) = &self.cache {
|
if let Some(cache) = &self.cache {
|
||||||
cache
|
cache
|
||||||
.cleanup(|name| {
|
.cleanup(|name| {
|
||||||
std::fs::metadata(self.config.dirs.posts.join(name.to_owned() + ".md"))
|
std::fs::metadata(
|
||||||
.ok()
|
self.config
|
||||||
.and_then(|metadata| metadata.modified().ok())
|
.dirs
|
||||||
.map(|mtime| as_secs(&mtime))
|
.posts
|
||||||
|
.join(self.as_raw(name).unwrap_or_else(|| unreachable!())),
|
||||||
|
)
|
||||||
|
.ok()
|
||||||
|
.and_then(|metadata| metadata.modified().ok())
|
||||||
|
.map(|mtime| as_secs(&mtime))
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn as_raw(&self, name: &str) -> Result<Option<String>, PostError> {
|
fn is_raw(&self, name: &str) -> bool {
|
||||||
|
name.ends_with(".md")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_raw(&self, name: &str) -> Option<String> {
|
||||||
let mut buf = String::with_capacity(name.len() + 3);
|
let mut buf = String::with_capacity(name.len() + 3);
|
||||||
buf += name;
|
buf += name;
|
||||||
buf += ".md";
|
buf += ".md";
|
||||||
|
|
||||||
Ok(Some(buf))
|
Some(buf)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,9 +2,11 @@ pub mod blag;
|
||||||
pub mod cache;
|
pub mod cache;
|
||||||
pub mod markdown_posts;
|
pub mod markdown_posts;
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use axum::{async_trait, http::HeaderValue};
|
use axum::async_trait;
|
||||||
|
use axum::http::HeaderValue;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
@ -17,19 +19,19 @@ pub use markdown_posts::MarkdownPosts;
|
||||||
// TODO: replace String with Arc<str>
|
// TODO: replace String with Arc<str>
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
pub struct PostMetadata {
|
pub struct PostMetadata {
|
||||||
pub name: String,
|
pub name: Arc<str>,
|
||||||
pub title: String,
|
pub title: Arc<str>,
|
||||||
pub description: String,
|
pub description: Arc<str>,
|
||||||
pub author: String,
|
pub author: Arc<str>,
|
||||||
pub icon: Option<String>,
|
pub icon: Option<Arc<str>>,
|
||||||
pub icon_alt: Option<String>,
|
pub icon_alt: Option<Arc<str>>,
|
||||||
pub color: Option<String>,
|
pub color: Option<Arc<str>>,
|
||||||
pub created_at: Option<DateTime<Utc>>,
|
pub created_at: Option<DateTime<Utc>>,
|
||||||
pub modified_at: Option<DateTime<Utc>>,
|
pub modified_at: Option<DateTime<Utc>>,
|
||||||
pub tags: Vec<String>,
|
pub tags: Vec<Arc<str>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Debug)]
|
#[derive(Serialize, Debug, Clone)]
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
pub enum RenderStats {
|
pub enum RenderStats {
|
||||||
Cached(Duration),
|
Cached(Duration),
|
||||||
|
@ -39,13 +41,25 @@ pub enum RenderStats {
|
||||||
rendered: Duration,
|
rendered: Duration,
|
||||||
},
|
},
|
||||||
Fetched(Duration),
|
Fetched(Duration),
|
||||||
|
Other {
|
||||||
|
verb: Arc<str>,
|
||||||
|
time: Duration,
|
||||||
|
},
|
||||||
Unknown,
|
Unknown,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::large_enum_variant)] // Raw will be returned very rarely
|
#[allow(clippy::large_enum_variant)] // Raw will be returned very rarely
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
pub enum ReturnedPost {
|
pub enum ReturnedPost {
|
||||||
Rendered(PostMetadata, String, RenderStats),
|
Rendered {
|
||||||
Raw(Vec<u8>, HeaderValue),
|
meta: PostMetadata,
|
||||||
|
body: Arc<str>,
|
||||||
|
perf: RenderStats,
|
||||||
|
},
|
||||||
|
Raw {
|
||||||
|
buffer: Vec<u8>,
|
||||||
|
content_type: HeaderValue,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum Filter<'a> {
|
pub enum Filter<'a> {
|
||||||
|
@ -57,7 +71,7 @@ impl Filter<'_> {
|
||||||
match self {
|
match self {
|
||||||
Filter::Tags(tags) => tags
|
Filter::Tags(tags) => tags
|
||||||
.iter()
|
.iter()
|
||||||
.any(|tag| meta.tags.iter().any(|meta_tag| meta_tag == tag)),
|
.any(|tag| meta.tags.iter().any(|meta_tag| &**meta_tag == *tag)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -93,7 +107,7 @@ pub trait PostManager {
|
||||||
&self,
|
&self,
|
||||||
filters: &[Filter<'_>],
|
filters: &[Filter<'_>],
|
||||||
query: &IndexMap<String, Value>,
|
query: &IndexMap<String, Value>,
|
||||||
) -> Result<Vec<(PostMetadata, String, RenderStats)>, PostError>;
|
) -> Result<Vec<(PostMetadata, Arc<str>, RenderStats)>, PostError>;
|
||||||
|
|
||||||
async fn get_max_n_post_metadata_with_optional_tag_sorted(
|
async fn get_max_n_post_metadata_with_optional_tag_sorted(
|
||||||
&self,
|
&self,
|
||||||
|
@ -119,25 +133,30 @@ pub trait PostManager {
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
async fn get_post_metadata(
|
async fn get_post_metadata(
|
||||||
&self,
|
&self,
|
||||||
name: &str,
|
name: Arc<str>,
|
||||||
query: &IndexMap<String, Value>,
|
query: &IndexMap<String, Value>,
|
||||||
) -> Result<PostMetadata, PostError> {
|
) -> Result<PostMetadata, PostError> {
|
||||||
match self.get_post(name, query).await? {
|
match self.get_post(name.clone(), query).await? {
|
||||||
ReturnedPost::Rendered(metadata, ..) => Ok(metadata),
|
ReturnedPost::Rendered { meta, .. } => Ok(meta),
|
||||||
ReturnedPost::Raw(..) => Err(PostError::NotFound(name.to_string())),
|
ReturnedPost::Raw { .. } => Err(PostError::NotFound(name)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_post(
|
async fn get_post(
|
||||||
&self,
|
&self,
|
||||||
name: &str,
|
name: Arc<str>,
|
||||||
query: &IndexMap<String, Value>,
|
query: &IndexMap<String, Value>,
|
||||||
) -> Result<ReturnedPost, PostError>;
|
) -> Result<ReturnedPost, PostError>;
|
||||||
|
|
||||||
async fn cleanup(&self) {}
|
async fn cleanup(&self) {}
|
||||||
|
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
async fn as_raw(&self, name: &str) -> Result<Option<String>, PostError> {
|
fn is_raw(&self, name: &str) -> bool {
|
||||||
Ok(None)
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
fn as_raw(&self, name: &str) -> Option<String> {
|
||||||
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue