add cache ttl
This commit is contained in:
parent
74b61e67f5
commit
9b96b09ee0
9 changed files with 168 additions and 77 deletions
|
@ -43,8 +43,12 @@ port = 3000 # port to listen on
|
||||||
[cache]
|
[cache]
|
||||||
enable = true # save metadata and rendered posts into RAM
|
enable = true # save metadata and rendered posts into RAM
|
||||||
# highly recommended, only turn off if absolutely necessary
|
# highly recommended, only turn off if absolutely necessary
|
||||||
|
#ttl = 5 # how long should and item persist in cache,
|
||||||
|
# in milliseconds
|
||||||
|
# uncomment to enable
|
||||||
cleanup = true # clean cache, highly recommended
|
cleanup = true # clean cache, highly recommended
|
||||||
#cleanup_interval = 86400000 # clean the cache regularly instead of just at startup
|
#cleanup_interval = 86400000 # clean the cache regularly instead of
|
||||||
|
# just at startup, value in milliseconds
|
||||||
# uncomment to enable
|
# uncomment to enable
|
||||||
persistence = true # save the cache to on shutdown and load on startup
|
persistence = true # save the cache to on shutdown and load on startup
|
||||||
file = "cache" # file to save the cache to
|
file = "cache" # file to save the cache to
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::net::{IpAddr, Ipv6Addr};
|
use std::net::{IpAddr, Ipv6Addr};
|
||||||
|
use std::num::NonZeroU64;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use color_eyre::eyre::{bail, Context, Result};
|
use color_eyre::eyre::{self, bail, Context};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
use tracing::{error, info, instrument};
|
use tracing::{error, info, instrument};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::ranged_i128_visitor::RangedI128Visitor;
|
use crate::de::*;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
@ -31,8 +32,11 @@ pub struct RenderConfig {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub struct CacheConfig {
|
pub struct CacheConfig {
|
||||||
pub enable: bool,
|
pub enable: bool,
|
||||||
|
#[serde(deserialize_with = "check_millis")]
|
||||||
|
pub ttl: Option<NonZeroU64>,
|
||||||
pub cleanup: bool,
|
pub cleanup: bool,
|
||||||
pub cleanup_interval: Option<u64>,
|
#[serde(deserialize_with = "check_millis")]
|
||||||
|
pub cleanup_interval: Option<NonZeroU64>,
|
||||||
pub persistence: bool,
|
pub persistence: bool,
|
||||||
pub file: PathBuf,
|
pub file: PathBuf,
|
||||||
pub compress: bool,
|
pub compress: bool,
|
||||||
|
@ -198,6 +202,7 @@ impl Default for CacheConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
enable: true,
|
enable: true,
|
||||||
|
ttl: None,
|
||||||
cleanup: true,
|
cleanup: true,
|
||||||
cleanup_interval: None,
|
cleanup_interval: None,
|
||||||
persistence: true,
|
persistence: true,
|
||||||
|
@ -215,7 +220,7 @@ impl Default for BlagConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(name = "config")]
|
#[instrument(name = "config")]
|
||||||
pub async fn load() -> Result<Config> {
|
pub async fn load() -> eyre::Result<Config> {
|
||||||
let config_file = env::var(format!(
|
let config_file = env::var(format!(
|
||||||
"{}_CONFIG",
|
"{}_CONFIG",
|
||||||
env!("CARGO_BIN_NAME").to_uppercase().replace('-', "_")
|
env!("CARGO_BIN_NAME").to_uppercase().replace('-', "_")
|
||||||
|
@ -267,6 +272,13 @@ fn check_zstd_level_bounds<'de, D>(d: D) -> Result<i32, D::Error>
|
||||||
where
|
where
|
||||||
D: serde::Deserializer<'de>,
|
D: serde::Deserializer<'de>,
|
||||||
{
|
{
|
||||||
d.deserialize_i32(RangedI128Visitor::<1, 22>)
|
d.deserialize_i32(RangedI64Visitor::<1, 22>)
|
||||||
.map(|x| x as i32)
|
.map(|x| x as i32)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn check_millis<'de, D>(d: D) -> Result<Option<NonZeroU64>, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
d.deserialize_option(MillisVisitor)
|
||||||
|
}
|
||||||
|
|
86
src/de.rs
Normal file
86
src/de.rs
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
use std::num::NonZeroU64;
|
||||||
|
|
||||||
|
use serde::de::Error;
|
||||||
|
|
||||||
|
use serde::{
|
||||||
|
de::{Unexpected, Visitor},
|
||||||
|
Deserializer,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct RangedI64Visitor<const START: i64, const END: i64>;
|
||||||
|
impl<const START: i64, const END: i64> serde::de::Visitor<'_> for RangedI64Visitor<START, END> {
|
||||||
|
type Value = i64;
|
||||||
|
|
||||||
|
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
write!(formatter, "an integer between {START} and {END}")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_i32<E>(self, v: i32) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: serde::de::Error,
|
||||||
|
{
|
||||||
|
self.visit_i64(v as i64)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: serde::de::Error,
|
||||||
|
{
|
||||||
|
if v >= START && v <= END {
|
||||||
|
Ok(v)
|
||||||
|
} else {
|
||||||
|
Err(E::custom(format!(
|
||||||
|
"integer is out of bounds ({START}..{END})"
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_i128<E>(self, v: i128) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: serde::de::Error,
|
||||||
|
{
|
||||||
|
self.visit_i64(v as i64)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct U64Visitor;
|
||||||
|
impl Visitor<'_> for U64Visitor {
|
||||||
|
type Value = u64;
|
||||||
|
|
||||||
|
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
write!(formatter, "a non-negative integer")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: serde::de::Error,
|
||||||
|
{
|
||||||
|
Ok(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: serde::de::Error,
|
||||||
|
{
|
||||||
|
u64::try_from(v).map_err(|_| E::invalid_value(Unexpected::Signed(v), &self))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MillisVisitor;
|
||||||
|
impl<'de> Visitor<'de> for MillisVisitor {
|
||||||
|
type Value = Option<NonZeroU64>;
|
||||||
|
|
||||||
|
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
write!(formatter, "a positive integer")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let n = deserializer.deserialize_i64(U64Visitor)?;
|
||||||
|
NonZeroU64::new(n)
|
||||||
|
.ok_or(D::Error::invalid_value(Unexpected::Unsigned(n), &self))
|
||||||
|
.map(Some)
|
||||||
|
}
|
||||||
|
}
|
12
src/main.rs
12
src/main.rs
|
@ -2,12 +2,12 @@
|
||||||
|
|
||||||
mod app;
|
mod app;
|
||||||
mod config;
|
mod config;
|
||||||
|
mod de;
|
||||||
mod error;
|
mod error;
|
||||||
mod helpers;
|
mod helpers;
|
||||||
mod markdown_render;
|
mod markdown_render;
|
||||||
mod platform;
|
mod platform;
|
||||||
mod post;
|
mod post;
|
||||||
mod ranged_i128_visitor;
|
|
||||||
mod serve_dir_included;
|
mod serve_dir_included;
|
||||||
mod systemtime_as_secs;
|
mod systemtime_as_secs;
|
||||||
mod templates;
|
mod templates;
|
||||||
|
@ -32,7 +32,7 @@ use tracing_subscriber::layer::SubscriberExt;
|
||||||
use tracing_subscriber::{util::SubscriberInitExt, EnvFilter};
|
use tracing_subscriber::{util::SubscriberInitExt, EnvFilter};
|
||||||
|
|
||||||
use crate::app::AppState;
|
use crate::app::AppState;
|
||||||
use crate::post::cache::{load_cache, CacheGuard, CACHE_VERSION};
|
use crate::post::cache::{load_cache, Cache, CacheGuard, CACHE_VERSION};
|
||||||
use crate::post::{Blag, MarkdownPosts, PostManager};
|
use crate::post::{Blag, MarkdownPosts, PostManager};
|
||||||
use crate::templates::new_registry;
|
use crate::templates::new_registry;
|
||||||
use crate::templates::watcher::watch_templates;
|
use crate::templates::watcher::watch_templates;
|
||||||
|
@ -89,17 +89,17 @@ async fn main() -> eyre::Result<()> {
|
||||||
let mut cache = load_cache(&config.cache).await.unwrap_or_else(|err| {
|
let mut cache = load_cache(&config.cache).await.unwrap_or_else(|err| {
|
||||||
error!("failed to load cache: {}", err);
|
error!("failed to load cache: {}", err);
|
||||||
info!("using empty cache");
|
info!("using empty cache");
|
||||||
Default::default()
|
Cache::new(config.cache.ttl)
|
||||||
});
|
});
|
||||||
|
|
||||||
if cache.version() < CACHE_VERSION {
|
if cache.version() < CACHE_VERSION {
|
||||||
warn!("cache version changed, clearing cache");
|
warn!("cache version changed, clearing cache");
|
||||||
cache = Default::default();
|
cache = Cache::new(config.cache.ttl);
|
||||||
};
|
};
|
||||||
|
|
||||||
Some(cache)
|
Some(cache)
|
||||||
} else {
|
} else {
|
||||||
Some(Default::default())
|
Some(Cache::new(config.cache.ttl))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
@ -122,7 +122,7 @@ async fn main() -> eyre::Result<()> {
|
||||||
let token = cancellation_token.child_token();
|
let token = cancellation_token.child_token();
|
||||||
debug!("setting up cleanup task");
|
debug!("setting up cleanup task");
|
||||||
tasks.spawn(async move {
|
tasks.spawn(async move {
|
||||||
let mut interval = tokio::time::interval(Duration::from_millis(millis));
|
let mut interval = tokio::time::interval(Duration::from_millis(millis.into()));
|
||||||
loop {
|
loop {
|
||||||
select! {
|
select! {
|
||||||
_ = token.cancelled() => break Ok(()),
|
_ = token.cancelled() => break Ok(()),
|
||||||
|
|
|
@ -248,7 +248,7 @@ impl PostManager for Blag {
|
||||||
return Err(PostError::NotFound(name));
|
return Err(PostError::NotFound(name));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mtime = as_secs(&stat.modified()?);
|
let mtime = as_secs(stat.modified()?);
|
||||||
|
|
||||||
let query_json = serde_json::to_string(&query).expect("this should not fail");
|
let query_json = serde_json::to_string(&query).expect("this should not fail");
|
||||||
let mut hasher = DefaultHasher::new();
|
let mut hasher = DefaultHasher::new();
|
||||||
|
@ -314,7 +314,7 @@ impl PostManager for Blag {
|
||||||
)
|
)
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|metadata| metadata.modified().ok())
|
.and_then(|metadata| metadata.modified().ok())
|
||||||
.map(|mtime| as_secs(&mtime));
|
.map(as_secs);
|
||||||
|
|
||||||
match mtime {
|
match mtime {
|
||||||
Some(mtime) => mtime <= value.mtime,
|
Some(mtime) => mtime <= value.mtime,
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
use std::io::{Read, Write};
|
use std::io::{Read, Write};
|
||||||
|
use std::num::NonZeroU64;
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::time::SystemTime;
|
||||||
|
|
||||||
use crate::config::CacheConfig;
|
use crate::config::CacheConfig;
|
||||||
use crate::post::PostMetadata;
|
use crate::post::PostMetadata;
|
||||||
|
@ -14,20 +16,24 @@ use tracing::{debug, info, instrument, trace, Span};
|
||||||
/// do not persist cache if this version number changed
|
/// do not persist cache if this version number changed
|
||||||
pub const CACHE_VERSION: u16 = 5;
|
pub const CACHE_VERSION: u16 = 5;
|
||||||
|
|
||||||
|
fn now() -> u128 {
|
||||||
|
crate::systemtime_as_secs::as_millis(SystemTime::now())
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
pub struct CacheValue {
|
pub struct CacheValue {
|
||||||
pub meta: PostMetadata,
|
pub meta: PostMetadata,
|
||||||
pub body: Arc<str>,
|
pub body: Arc<str>,
|
||||||
pub mtime: u64,
|
pub mtime: u64,
|
||||||
|
/// when the item was inserted into cache, in milliseconds since epoch
|
||||||
|
pub cached_at: u128,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
pub struct Cache(HashMap<CacheKey, CacheValue>, u16);
|
pub struct Cache {
|
||||||
|
map: HashMap<CacheKey, CacheValue>,
|
||||||
impl Default for Cache {
|
version: u16,
|
||||||
fn default() -> Self {
|
ttl: Option<NonZeroU64>,
|
||||||
Self(Default::default(), CACHE_VERSION)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Hash, Eq, PartialEq, Clone, Debug)]
|
#[derive(Serialize, Deserialize, Hash, Eq, PartialEq, Clone, Debug)]
|
||||||
|
@ -38,15 +44,30 @@ pub struct CacheKey {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Cache {
|
impl Cache {
|
||||||
|
pub fn new(ttl: Option<NonZeroU64>) -> Self {
|
||||||
|
Cache {
|
||||||
|
map: Default::default(),
|
||||||
|
version: CACHE_VERSION,
|
||||||
|
ttl,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn up_to_date(&self, cached: &CacheValue, mtime: u64) -> bool {
|
||||||
|
mtime <= cached.mtime
|
||||||
|
&& self
|
||||||
|
.ttl
|
||||||
|
.is_some_and(|ttl| cached.cached_at + u64::from(ttl) as u128 >= now())
|
||||||
|
}
|
||||||
|
|
||||||
#[instrument(level = "debug", skip(self), fields(entry_mtime))]
|
#[instrument(level = "debug", skip(self), fields(entry_mtime))]
|
||||||
pub async fn lookup(&self, name: Arc<str>, mtime: u64, extra: u64) -> Option<CacheValue> {
|
pub async fn lookup(&self, name: Arc<str>, mtime: u64, extra: u64) -> Option<CacheValue> {
|
||||||
trace!("looking up in cache");
|
trace!("looking up in cache");
|
||||||
match self.0.get_async(&CacheKey { name, extra }).await {
|
match self.map.get_async(&CacheKey { name, extra }).await {
|
||||||
Some(entry) => {
|
Some(entry) => {
|
||||||
let cached = entry.get();
|
let cached = entry.get();
|
||||||
Span::current().record("entry_mtime", cached.mtime);
|
Span::current().record("entry_mtime", cached.mtime);
|
||||||
trace!("found in cache");
|
trace!("found in cache");
|
||||||
if mtime <= cached.mtime {
|
if self.up_to_date(cached, mtime) {
|
||||||
trace!("entry up-to-date");
|
trace!("entry up-to-date");
|
||||||
Some(cached.clone())
|
Some(cached.clone())
|
||||||
} else {
|
} else {
|
||||||
|
@ -67,11 +88,11 @@ impl Cache {
|
||||||
extra: u64,
|
extra: u64,
|
||||||
) -> Option<PostMetadata> {
|
) -> Option<PostMetadata> {
|
||||||
trace!("looking up metadata in cache");
|
trace!("looking up metadata in cache");
|
||||||
match self.0.get_async(&CacheKey { name, extra }).await {
|
match self.map.get_async(&CacheKey { name, extra }).await {
|
||||||
Some(entry) => {
|
Some(entry) => {
|
||||||
let cached = entry.get();
|
let cached = entry.get();
|
||||||
Span::current().record("entry_mtime", cached.mtime);
|
Span::current().record("entry_mtime", cached.mtime);
|
||||||
if mtime <= cached.mtime {
|
if self.up_to_date(cached, mtime) {
|
||||||
trace!("entry up-to-date");
|
trace!("entry up-to-date");
|
||||||
Some(cached.meta.clone())
|
Some(cached.meta.clone())
|
||||||
} else {
|
} else {
|
||||||
|
@ -96,13 +117,14 @@ impl Cache {
|
||||||
trace!("inserting into cache");
|
trace!("inserting into cache");
|
||||||
|
|
||||||
let r = self
|
let r = self
|
||||||
.0
|
.map
|
||||||
.upsert_async(
|
.upsert_async(
|
||||||
CacheKey { name, extra },
|
CacheKey { name, extra },
|
||||||
CacheValue {
|
CacheValue {
|
||||||
meta: metadata,
|
meta: metadata,
|
||||||
body: rendered,
|
body: rendered,
|
||||||
mtime,
|
mtime,
|
||||||
|
cached_at: now(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
@ -123,7 +145,7 @@ impl Cache {
|
||||||
pub async fn remove(&self, name: Arc<str>, extra: u64) -> Option<(CacheKey, CacheValue)> {
|
pub async fn remove(&self, name: Arc<str>, extra: u64) -> Option<(CacheKey, CacheValue)> {
|
||||||
trace!("removing from cache");
|
trace!("removing from cache");
|
||||||
|
|
||||||
let r = self.0.remove_async(&CacheKey { name, extra }).await;
|
let r = self.map.remove_async(&CacheKey { name, extra }).await;
|
||||||
|
|
||||||
debug!(
|
debug!(
|
||||||
"item {} cache",
|
"item {} cache",
|
||||||
|
@ -138,12 +160,12 @@ impl Cache {
|
||||||
|
|
||||||
#[instrument(level = "debug", name = "cleanup", skip_all)]
|
#[instrument(level = "debug", name = "cleanup", skip_all)]
|
||||||
pub async fn retain(&self, predicate: impl Fn(&CacheKey, &CacheValue) -> bool) {
|
pub async fn retain(&self, predicate: impl Fn(&CacheKey, &CacheValue) -> bool) {
|
||||||
let old_size = self.0.len();
|
let old_size = self.map.len();
|
||||||
let mut i = 0;
|
let mut i = 0;
|
||||||
|
|
||||||
// TODO: multithread
|
// TODO: multithread
|
||||||
// not urgent as this is run concurrently anyways
|
// not urgent as this is run concurrently anyways
|
||||||
self.0
|
self.map
|
||||||
.retain_async(|k, v| {
|
.retain_async(|k, v| {
|
||||||
if predicate(k, v) {
|
if predicate(k, v) {
|
||||||
true
|
true
|
||||||
|
@ -160,12 +182,12 @@ impl Cache {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn len(&self) -> usize {
|
pub fn len(&self) -> usize {
|
||||||
self.0.len()
|
self.map.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn version(&self) -> u16 {
|
pub fn version(&self) -> u16 {
|
||||||
self.1
|
self.version
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -125,7 +125,7 @@ impl MarkdownPosts {
|
||||||
.insert(
|
.insert(
|
||||||
name.clone(),
|
name.clone(),
|
||||||
metadata.clone(),
|
metadata.clone(),
|
||||||
as_secs(&modified),
|
as_secs(modified),
|
||||||
Arc::clone(&post),
|
Arc::clone(&post),
|
||||||
self.render_hash,
|
self.render_hash,
|
||||||
)
|
)
|
||||||
|
@ -184,7 +184,7 @@ impl PostManager for MarkdownPosts {
|
||||||
let stat = fs::metadata(&path).await?;
|
let stat = fs::metadata(&path).await?;
|
||||||
|
|
||||||
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: Arc<str> =
|
let name: Arc<str> =
|
||||||
String::from(path.file_stem().unwrap().to_string_lossy()).into();
|
String::from(path.file_stem().unwrap().to_string_lossy()).into();
|
||||||
|
|
||||||
|
@ -262,7 +262,7 @@ 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(CacheValue { meta, body, .. }) =
|
&& let Some(CacheValue { meta, body, .. }) =
|
||||||
|
@ -311,7 +311,7 @@ impl PostManager for MarkdownPosts {
|
||||||
)
|
)
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|metadata| metadata.modified().ok())
|
.and_then(|metadata| metadata.modified().ok())
|
||||||
.map(|mtime| as_secs(&mtime));
|
.map(as_secs);
|
||||||
|
|
||||||
match mtime {
|
match mtime {
|
||||||
Some(mtime) => mtime <= value.mtime,
|
Some(mtime) => mtime <= value.mtime,
|
||||||
|
|
|
@ -1,37 +0,0 @@
|
||||||
pub struct RangedI128Visitor<const START: i128, const END: i128>;
|
|
||||||
impl<const START: i128, const END: i128> serde::de::Visitor<'_>
|
|
||||||
for RangedI128Visitor<START, END>
|
|
||||||
{
|
|
||||||
type Value = i128;
|
|
||||||
|
|
||||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
||||||
write!(formatter, "an integer between {START} and {END}")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_i32<E>(self, v: i32) -> std::result::Result<Self::Value, E>
|
|
||||||
where
|
|
||||||
E: serde::de::Error,
|
|
||||||
{
|
|
||||||
self.visit_i128(v as i128)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_i64<E>(self, v: i64) -> std::prelude::v1::Result<Self::Value, E>
|
|
||||||
where
|
|
||||||
E: serde::de::Error,
|
|
||||||
{
|
|
||||||
self.visit_i128(v as i128)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_i128<E>(self, v: i128) -> std::prelude::v1::Result<Self::Value, E>
|
|
||||||
where
|
|
||||||
E: serde::de::Error,
|
|
||||||
{
|
|
||||||
if v >= START && v <= END {
|
|
||||||
Ok(v)
|
|
||||||
} else {
|
|
||||||
Err(E::custom(format!(
|
|
||||||
"integer is out of bounds ({START}..{END})"
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +1,13 @@
|
||||||
use std::time::SystemTime;
|
use std::time::SystemTime;
|
||||||
|
|
||||||
pub fn as_secs(t: &SystemTime) -> u64 {
|
pub fn as_secs(t: SystemTime) -> u64 {
|
||||||
match t.duration_since(SystemTime::UNIX_EPOCH) {
|
t.duration_since(SystemTime::UNIX_EPOCH)
|
||||||
Ok(duration) => duration,
|
.unwrap_or_else(|err| err.duration())
|
||||||
Err(err) => err.duration(),
|
.as_secs()
|
||||||
}
|
}
|
||||||
.as_secs()
|
|
||||||
|
pub fn as_millis(t: SystemTime) -> u128 {
|
||||||
|
t.duration_since(SystemTime::UNIX_EPOCH)
|
||||||
|
.unwrap_or_else(|err| err.duration())
|
||||||
|
.as_millis()
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue