add: redis cache support
This commit is contained in:
parent
1d9a96ae69
commit
38dbf10130
13 changed files with 541 additions and 17 deletions
40
Cargo.lock
generated
40
Cargo.lock
generated
|
@ -59,6 +59,12 @@ version = "1.4.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223"
|
||||
|
||||
[[package]]
|
||||
name = "arc-swap"
|
||||
version = "1.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
|
||||
|
||||
[[package]]
|
||||
name = "arg_enum_proc_macro"
|
||||
version = "0.3.4"
|
||||
|
@ -403,6 +409,16 @@ version = "1.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
||||
|
||||
[[package]]
|
||||
name = "combine"
|
||||
version = "4.6.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cookie"
|
||||
version = "0.18.1"
|
||||
|
@ -2015,6 +2031,23 @@ dependencies = [
|
|||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redis"
|
||||
version = "0.29.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b110459d6e323b7cda23980c46c77157601199c9da6241552b284cd565a7a133"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"combine",
|
||||
"itoa",
|
||||
"num-bigint",
|
||||
"percent-encoding",
|
||||
"ryu",
|
||||
"sha1_smol",
|
||||
"socket2",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.10"
|
||||
|
@ -2326,6 +2359,12 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha1_smol"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d"
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.8"
|
||||
|
@ -2571,6 +2610,7 @@ dependencies = [
|
|||
"bb8-postgres",
|
||||
"bitflags 2.9.0",
|
||||
"pathbufd",
|
||||
"redis",
|
||||
"rusqlite",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
|
|
@ -19,7 +19,7 @@ axum = { version = "0.8.1", features = ["macros"] }
|
|||
tokio = { version = "1.44.1", features = ["macros", "rt-multi-thread"] }
|
||||
axum-extra = { version = "0.10.0", features = ["cookie", "multipart"] }
|
||||
tetratto-shared = { path = "../shared" }
|
||||
tetratto-core = { path = "../core", default-features = false }
|
||||
tetratto-core = { path = "../core", features = ["redis"], default-features = false }
|
||||
tetratto-l10n = { path = "../l10n" }
|
||||
|
||||
image = "0.25.5"
|
||||
|
|
|
@ -6,7 +6,8 @@ edition = "2024"
|
|||
[features]
|
||||
postgres = ["dep:tokio-postgres", "dep:bb8-postgres"]
|
||||
sqlite = ["dep:rusqlite"]
|
||||
default = ["sqlite"]
|
||||
redis = ["dep:redis"]
|
||||
default = ["sqlite", "redis"]
|
||||
|
||||
[dependencies]
|
||||
pathbufd = "0.1.4"
|
||||
|
@ -16,6 +17,8 @@ tetratto-shared = { path = "../shared" }
|
|||
tetratto-l10n = { path = "../l10n" }
|
||||
serde_json = "1.0.140"
|
||||
|
||||
redis = { version = "0.29.2", optional = true }
|
||||
|
||||
rusqlite = { version = "0.34.0", optional = true }
|
||||
|
||||
tokio-postgres = { version = "0.7.13", optional = true }
|
||||
|
|
77
crates/core/src/cache/mod.rs
vendored
Normal file
77
crates/core/src/cache/mod.rs
vendored
Normal file
|
@ -0,0 +1,77 @@
|
|||
#![allow(async_fn_in_trait)]
|
||||
use serde::{Serialize, de::DeserializeOwned};
|
||||
|
||||
pub const EXPIRE_AT: i64 = 3_600_000;
|
||||
|
||||
#[allow(type_alias_bounds)]
|
||||
pub type TimedObject<T: Serialize + DeserializeOwned> = (i64, T);
|
||||
|
||||
#[cfg(feature = "redis")]
|
||||
pub mod redis;
|
||||
|
||||
#[cfg(not(feature = "redis"))]
|
||||
pub mod no_cache;
|
||||
|
||||
/// A simple cache "database".
|
||||
pub trait Cache {
|
||||
type Item;
|
||||
type Client;
|
||||
|
||||
/// Create a new [`Cache`].
|
||||
async fn new() -> Self;
|
||||
/// Get a connection to the cache.
|
||||
async fn get_con(&self) -> Self::Client;
|
||||
|
||||
/// Get a cache object by its identifier
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `id` - `String` of the object's id
|
||||
async fn get(&self, id: Self::Item) -> Option<String>;
|
||||
/// Set a cache object by its identifier and content
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `id` - `String` of the object's id
|
||||
/// * `content` - `String` of the object's content
|
||||
async fn set(&self, id: Self::Item, content: Self::Item) -> bool;
|
||||
/// Update a cache object by its identifier and content
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `id` - `String` of the object's id
|
||||
/// * `content` - `String` of the object's content
|
||||
async fn update(&self, id: Self::Item, content: Self::Item) -> bool;
|
||||
/// Remove a cache object by its identifier
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `id` - `String` of the object's id
|
||||
async fn remove(&self, id: Self::Item) -> bool;
|
||||
/// Remove a cache object by its identifier('s start)
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `id` - `String` of the object's id('s start)
|
||||
async fn remove_starting_with(&self, id: Self::Item) -> bool;
|
||||
/// Increment a cache object by its identifier
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `id` - `String` of the object's id
|
||||
async fn incr(&self, id: Self::Item) -> bool;
|
||||
/// Decrement a cache object by its identifier
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `id` - `String` of the object's id
|
||||
async fn decr(&self, id: Self::Item) -> bool;
|
||||
|
||||
/// Get a cache object by its identifier
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `id` - `String` of the object's id
|
||||
async fn get_timed<T: Serialize + DeserializeOwned>(
|
||||
&self,
|
||||
id: Self::Item,
|
||||
) -> Option<TimedObject<T>>;
|
||||
/// Set a cache object by its identifier and content
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `id` - `String` of the object's id
|
||||
/// * `content` - `String` of the object's content
|
||||
async fn set_timed<T: Serialize + DeserializeOwned>(&self, id: Self::Item, content: T) -> bool;
|
||||
}
|
62
crates/core/src/cache/no_cache.rs
vendored
Normal file
62
crates/core/src/cache/no_cache.rs
vendored
Normal file
|
@ -0,0 +1,62 @@
|
|||
use serde::{Serialize, de::DeserializeOwned};
|
||||
|
||||
use super::{Cache, EXPIRE_AT, TimedObject};
|
||||
|
||||
pub const EPOCH_YEAR: u32 = 2025;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct NoCache {
|
||||
pub client: Option<u32>,
|
||||
}
|
||||
|
||||
impl Cache for NoCache {
|
||||
type Item = String;
|
||||
type Client = Option<u32>;
|
||||
|
||||
async fn new() -> Self {
|
||||
Self { client: None }
|
||||
}
|
||||
|
||||
async fn get_con(&self) -> Self::Client {
|
||||
None
|
||||
}
|
||||
|
||||
async fn get(&self, id: Self::Item) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
async fn set(&self, id: Self::Item, content: Self::Item) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
async fn update(&self, id: Self::Item, content: Self::Item) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
async fn remove(&self, id: Self::Item) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
async fn remove_starting_with(&self, id: Self::Item) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
async fn incr(&self, id: Self::Item) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
async fn decr(&self, id: Self::Item) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
async fn get_timed<T: Serialize + DeserializeOwned>(
|
||||
&self,
|
||||
id: Self::Item,
|
||||
) -> Option<TimedObject<T>> {
|
||||
None
|
||||
}
|
||||
|
||||
async fn set_timed<T: Serialize + DeserializeOwned>(&self, id: Self::Item, content: T) -> bool {
|
||||
None
|
||||
}
|
||||
}
|
123
crates/core/src/cache/redis.rs
vendored
Normal file
123
crates/core/src/cache/redis.rs
vendored
Normal file
|
@ -0,0 +1,123 @@
|
|||
use redis::Commands;
|
||||
use serde::{Serialize, de::DeserializeOwned};
|
||||
|
||||
use super::{Cache, EXPIRE_AT, TimedObject};
|
||||
|
||||
pub const EPOCH_YEAR: u32 = 2025;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RedisCache {
|
||||
pub client: redis::Client,
|
||||
}
|
||||
|
||||
impl Cache for RedisCache {
|
||||
type Item = String;
|
||||
type Client = redis::Connection;
|
||||
|
||||
async fn new() -> Self {
|
||||
Self {
|
||||
client: redis::Client::open("redis://127.0.0.1:6379").unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_con(&self) -> Self::Client {
|
||||
self.client.get_connection().unwrap()
|
||||
}
|
||||
|
||||
async fn get(&self, id: Self::Item) -> Option<String> {
|
||||
self.get_con().await.get(id).ok()
|
||||
}
|
||||
|
||||
async fn set(&self, id: Self::Item, content: Self::Item) -> bool {
|
||||
let mut c = self.get_con().await;
|
||||
let res: Result<String, redis::RedisError> = c.set(id, content);
|
||||
|
||||
res.is_ok()
|
||||
}
|
||||
|
||||
async fn update(&self, id: Self::Item, content: Self::Item) -> bool {
|
||||
self.set(id, content).await
|
||||
}
|
||||
|
||||
async fn remove(&self, id: Self::Item) -> bool {
|
||||
let mut c = self.get_con().await;
|
||||
let res: Result<String, redis::RedisError> = c.del(id);
|
||||
|
||||
res.is_ok()
|
||||
}
|
||||
|
||||
async fn remove_starting_with(&self, id: Self::Item) -> bool {
|
||||
let mut c = self.get_con().await;
|
||||
|
||||
// get keys
|
||||
let mut cmd = redis::cmd("DEL");
|
||||
let keys: Result<Vec<String>, redis::RedisError> = c.keys(id);
|
||||
|
||||
for key in keys.unwrap() {
|
||||
cmd.arg(key);
|
||||
}
|
||||
|
||||
// remove
|
||||
let res: Result<String, redis::RedisError> = cmd.query(&mut c);
|
||||
|
||||
res.is_ok()
|
||||
}
|
||||
|
||||
async fn incr(&self, id: Self::Item) -> bool {
|
||||
let mut c = self.get_con().await;
|
||||
let res: Result<String, redis::RedisError> = c.incr(id, 1);
|
||||
|
||||
res.is_ok()
|
||||
}
|
||||
|
||||
async fn decr(&self, id: Self::Item) -> bool {
|
||||
let mut c = self.get_con().await;
|
||||
let res: Result<String, redis::RedisError> = c.decr(id, 1);
|
||||
|
||||
res.is_ok()
|
||||
}
|
||||
|
||||
async fn get_timed<T: Serialize + DeserializeOwned>(
|
||||
&self,
|
||||
id: Self::Item,
|
||||
) -> Option<TimedObject<T>> {
|
||||
let mut c = self.get_con().await;
|
||||
let res: Result<String, redis::RedisError> = c.get(&id);
|
||||
|
||||
match res {
|
||||
Ok(d) => match serde_json::from_str::<TimedObject<T>>(&d) {
|
||||
Ok(d) => {
|
||||
// check time
|
||||
let now = tetratto_shared::epoch_timestamp(EPOCH_YEAR);
|
||||
|
||||
if now - d.0 >= EXPIRE_AT {
|
||||
// expired key, remove and return None
|
||||
self.remove(id).await;
|
||||
return None;
|
||||
}
|
||||
|
||||
// return
|
||||
Some(d)
|
||||
}
|
||||
Err(_) => None,
|
||||
},
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
async fn set_timed<T: Serialize + DeserializeOwned>(&self, id: Self::Item, content: T) -> bool {
|
||||
let mut c = self.get_con().await;
|
||||
let res: Result<String, redis::RedisError> = c.set(
|
||||
id,
|
||||
match serde_json::to_string::<TimedObject<T>>(&(
|
||||
tetratto_shared::epoch_timestamp(EPOCH_YEAR),
|
||||
content,
|
||||
)) {
|
||||
Ok(s) => s,
|
||||
Err(_) => return false,
|
||||
},
|
||||
);
|
||||
|
||||
res.is_ok()
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
use super::*;
|
||||
use crate::cache::Cache;
|
||||
use crate::model::{
|
||||
Error, Result,
|
||||
auth::{Token, User},
|
||||
|
@ -31,8 +32,8 @@ impl DataManager {
|
|||
}
|
||||
}
|
||||
|
||||
auto_method!(get_user_by_id(&str)@get_user_from_row -> "SELECT * FROM users WHERE id = $1" --name="user" --returns=User);
|
||||
auto_method!(get_user_by_username(&str)@get_user_from_row -> "SELECT * FROM users WHERE username = $1" --name="user" --returns=User);
|
||||
auto_method!(get_user_by_id(&str)@get_user_from_row -> "SELECT * FROM users WHERE id = $1" --name="user" --returns=User --cache-key-tmpl="atto.user:{}");
|
||||
auto_method!(get_user_by_username(&str)@get_user_from_row -> "SELECT * FROM users WHERE username = $1" --name="user" --returns=User --cache-key-tmpl="atto.user:{}");
|
||||
|
||||
/// Get a user given just their auth token.
|
||||
///
|
||||
|
@ -130,8 +131,11 @@ impl DataManager {
|
|||
return Err(Error::DatabaseError(e.to_string()));
|
||||
}
|
||||
|
||||
self.2.remove(format!("atto.user:{}", id)).await;
|
||||
self.2.remove(format!("atto.user:{}", user.username)).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
auto_method!(update_user_tokens(Vec<Token>) -> "UPDATE users SET tokens = $1 WHERE id = $2" --serde);
|
||||
auto_method!(update_user_tokens(Vec<Token>) -> "UPDATE users SET tokens = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.user:{}");
|
||||
}
|
||||
|
|
|
@ -39,6 +39,31 @@ macro_rules! auto_method {
|
|||
}
|
||||
};
|
||||
|
||||
($name:ident()@$select_fn:ident -> $query:literal --name=$name_:literal --returns=$returns_:tt --cache-key-tmpl=$cache_key_tmpl:literal) => {
|
||||
pub async fn $name(&self, id: usize) -> Result<$returns_> {
|
||||
let conn = match self.connect().await {
|
||||
Ok(c) => c,
|
||||
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
||||
};
|
||||
|
||||
let res = query_row!(&conn, $query, &[&id], |x| { Ok(Self::$select_fn(x)) });
|
||||
|
||||
if res.is_err() {
|
||||
return Err(Error::GeneralNotFound($name_.to_string()));
|
||||
}
|
||||
|
||||
let x = res.unwrap();
|
||||
self.2
|
||||
.set(
|
||||
format!($cache_key_tmpl, id),
|
||||
serde_json::to_string(&x).unwrap(),
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(x)
|
||||
}
|
||||
};
|
||||
|
||||
($name:ident($selector_t:ty)@$select_fn:ident -> $query:literal --name=$name_:literal --returns=$returns_:tt) => {
|
||||
pub async fn $name(&self, selector: $selector_t) -> Result<$returns_> {
|
||||
let conn = match self.connect().await {
|
||||
|
@ -56,6 +81,31 @@ macro_rules! auto_method {
|
|||
}
|
||||
};
|
||||
|
||||
($name:ident($selector_t:ty)@$select_fn:ident -> $query:literal --name=$name_:literal --returns=$returns_:tt --cache-key-tmpl=$cache_key_tmpl:literal) => {
|
||||
pub async fn $name(&self, selector: $selector_t) -> Result<$returns_> {
|
||||
let conn = match self.connect().await {
|
||||
Ok(c) => c,
|
||||
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
||||
};
|
||||
|
||||
let res = query_row!(&conn, $query, &[&selector], |x| { Ok(Self::$select_fn(x)) });
|
||||
|
||||
if res.is_err() {
|
||||
return Err(Error::GeneralNotFound($name_.to_string()));
|
||||
}
|
||||
|
||||
let x = res.unwrap();
|
||||
self.2
|
||||
.set(
|
||||
format!($cache_key_tmpl, selector),
|
||||
serde_json::to_string(&x).unwrap(),
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(x)
|
||||
}
|
||||
};
|
||||
|
||||
($name:ident()@$select_fn:ident:$permission:ident -> $query:literal) => {
|
||||
pub async fn $name(&self, id: usize, user: User) -> Result<()> {
|
||||
let page = self.$select_fn(id).await?;
|
||||
|
@ -81,6 +131,33 @@ macro_rules! auto_method {
|
|||
}
|
||||
};
|
||||
|
||||
($name:ident()@$select_fn:ident:$permission:ident -> $query:literal --cache-key-tmpl=$cache_key_tmpl:literal) => {
|
||||
pub async fn $name(&self, id: usize, user: User) -> Result<()> {
|
||||
let page = self.$select_fn(id).await?;
|
||||
|
||||
if user.id != page.owner {
|
||||
if !user.permissions.check(FinePermission::$permission) {
|
||||
return Err(Error::NotAllowed);
|
||||
}
|
||||
}
|
||||
|
||||
let conn = match self.connect().await {
|
||||
Ok(c) => c,
|
||||
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
||||
};
|
||||
|
||||
let res = execute!(&conn, $query, &[&id.to_string()]);
|
||||
|
||||
if let Err(e) = res {
|
||||
return Err(Error::DatabaseError(e.to_string()));
|
||||
}
|
||||
|
||||
self.2.remove(format!($cache_key_tmpl, id)).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
};
|
||||
|
||||
($name:ident($x:ty)@$select_fn:ident:$permission:ident -> $query:literal) => {
|
||||
pub async fn $name(&self, id: usize, user: User, x: $x) -> Result<()> {
|
||||
let y = self.$select_fn(id).await?;
|
||||
|
@ -106,6 +183,33 @@ macro_rules! auto_method {
|
|||
}
|
||||
};
|
||||
|
||||
($name:ident($x:ty)@$select_fn:ident:$permission:ident -> $query:literal --cache-key-tmpl=$cache_key_tmpl:literal) => {
|
||||
pub async fn $name(&self, id: usize, user: User, x: $x) -> Result<()> {
|
||||
let y = self.$select_fn(id).await?;
|
||||
|
||||
if user.id != y.owner {
|
||||
if !user.permissions.check(FinePermission::$permission) {
|
||||
return Err(Error::NotAllowed);
|
||||
}
|
||||
}
|
||||
|
||||
let conn = match self.connect().await {
|
||||
Ok(c) => c,
|
||||
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
||||
};
|
||||
|
||||
let res = execute!(&conn, $query, &[&x, &id.to_string()]);
|
||||
|
||||
if let Err(e) = res {
|
||||
return Err(Error::DatabaseError(e.to_string()));
|
||||
}
|
||||
|
||||
self.2.remove(format!($cache_key_tmpl, id)).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
};
|
||||
|
||||
($name:ident($x:ty)@$select_fn:ident:$permission:ident -> $query:literal --serde) => {
|
||||
pub async fn $name(&self, id: usize, user: User, x: $x) -> Result<()> {
|
||||
let y = self.$select_fn(id).await?;
|
||||
|
@ -135,6 +239,37 @@ macro_rules! auto_method {
|
|||
}
|
||||
};
|
||||
|
||||
($name:ident($x:ty)@$select_fn:ident:$permission:ident -> $query:literal --serde --cache-key-tmpl=$cache_key_tmpl:literal) => {
|
||||
pub async fn $name(&self, id: usize, user: User, x: $x) -> Result<()> {
|
||||
let y = self.$select_fn(id).await?;
|
||||
|
||||
if user.id != y.owner {
|
||||
if !user.permissions.check(FinePermission::$permission) {
|
||||
return Err(Error::NotAllowed);
|
||||
}
|
||||
}
|
||||
|
||||
let conn = match self.connect().await {
|
||||
Ok(c) => c,
|
||||
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
||||
};
|
||||
|
||||
let res = execute!(
|
||||
&conn,
|
||||
$query,
|
||||
&[&serde_json::to_string(&x).unwrap(), &id.to_string()]
|
||||
);
|
||||
|
||||
if let Err(e) = res {
|
||||
return Err(Error::DatabaseError(e.to_string()));
|
||||
}
|
||||
|
||||
self.2.remove(format!($cache_key_tmpl, id)).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
};
|
||||
|
||||
($name:ident($x:ty) -> $query:literal) => {
|
||||
pub async fn $name(&self, id: usize, x: $x) -> Result<()> {
|
||||
let conn = match self.connect().await {
|
||||
|
@ -152,6 +287,25 @@ macro_rules! auto_method {
|
|||
}
|
||||
};
|
||||
|
||||
($name:ident($x:ty) -> $query:literal --cache-key-tmpl=$cache_key_tmpl:literal) => {
|
||||
pub async fn $name(&self, id: usize, x: $x) -> Result<()> {
|
||||
let conn = match self.connect().await {
|
||||
Ok(c) => c,
|
||||
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
||||
};
|
||||
|
||||
let res = execute!(&conn, $query, &[&x, &id.to_string()]);
|
||||
|
||||
if let Err(e) = res {
|
||||
return Err(Error::DatabaseError(e.to_string()));
|
||||
}
|
||||
|
||||
self.2.remove(format!($cache_key_tmpl, id)).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
};
|
||||
|
||||
($name:ident($x:ty) -> $query:literal --serde) => {
|
||||
pub async fn $name(&self, id: usize, x: $x) -> Result<()> {
|
||||
let conn = match self.connect().await {
|
||||
|
@ -172,4 +326,27 @@ macro_rules! auto_method {
|
|||
Ok(())
|
||||
}
|
||||
};
|
||||
|
||||
($name:ident($x:ty) -> $query:literal --serde --cache-key-tmpl=$cache_key_tmpl:literal) => {
|
||||
pub async fn $name(&self, id: usize, x: $x) -> Result<()> {
|
||||
let conn = match self.connect().await {
|
||||
Ok(c) => c,
|
||||
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
||||
};
|
||||
|
||||
let res = execute!(
|
||||
&conn,
|
||||
$query,
|
||||
&[&serde_json::to_string(&x).unwrap(), &id.to_string()]
|
||||
);
|
||||
|
||||
if let Err(e) = res {
|
||||
return Err(Error::DatabaseError(e.to_string()));
|
||||
}
|
||||
|
||||
self.2.remove(format!($cache_key_tmpl, id)).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,3 +1,10 @@
|
|||
#[cfg(not(feature = "redis"))]
|
||||
use crate::cache::no_cache::NoCache;
|
||||
#[cfg(feature = "redis")]
|
||||
use crate::cache::redis::RedisCache;
|
||||
|
||||
use crate::cache::Cache;
|
||||
|
||||
use crate::config::Config;
|
||||
use bb8_postgres::{
|
||||
PostgresConnectionManager,
|
||||
|
@ -13,13 +20,15 @@ pub type Connection<'a> = PooledConnection<'a, PostgresConnectionManager<NoTls>>
|
|||
pub struct DataManager(
|
||||
pub Config,
|
||||
pub HashMap<String, LangFile>,
|
||||
#[cfg(feature = "redis")] pub RedisCache,
|
||||
#[cfg(not(feature = "redis"))] pub NoCache,
|
||||
pub Pool<PostgresConnectionManager<NoTls>>,
|
||||
);
|
||||
|
||||
impl DataManager {
|
||||
/// Obtain a connection to the staging database.
|
||||
pub(crate) async fn connect(&self) -> Result<Connection> {
|
||||
Ok(self.2.get().await.unwrap())
|
||||
Ok(self.3.get().await.unwrap())
|
||||
}
|
||||
|
||||
/// Create a new [`DataManager`] (and init database).
|
||||
|
@ -36,7 +45,15 @@ impl DataManager {
|
|||
);
|
||||
|
||||
let pool = Pool::builder().max_size(15).build(manager).await.unwrap();
|
||||
Ok(Self(config.clone(), read_langs(), pool))
|
||||
Ok(Self(
|
||||
config.clone(),
|
||||
read_langs(),
|
||||
#[cfg(feature = "redis")]
|
||||
RedisCache::new().await,
|
||||
#[cfg(not(feature = "redis"))]
|
||||
NoCache::new().await,
|
||||
pool,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,10 +1,22 @@
|
|||
#[cfg(not(feature = "redis"))]
|
||||
use crate::cache::no_cache::NoCache;
|
||||
#[cfg(feature = "redis")]
|
||||
use crate::cache::redis::RedisCache;
|
||||
|
||||
use crate::cache::Cache;
|
||||
|
||||
use crate::config::Config;
|
||||
use rusqlite::{Connection, Result};
|
||||
use std::collections::HashMap;
|
||||
use tetratto_l10n::{LangFile, read_langs};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DataManager(pub Config, pub HashMap<String, LangFile>);
|
||||
pub struct DataManager(
|
||||
pub Config,
|
||||
pub HashMap<String, LangFile>,
|
||||
#[cfg(feature = "redis")] pub RedisCache,
|
||||
#[cfg(not(feature = "redis"))] pub NoCache,
|
||||
);
|
||||
|
||||
impl DataManager {
|
||||
/// Obtain a connection to the staging database.
|
||||
|
@ -14,7 +26,14 @@ impl DataManager {
|
|||
|
||||
/// Create a new [`DataManager`] (and init database).
|
||||
pub async fn new(config: Config) -> Result<Self> {
|
||||
let this = Self(config.clone(), read_langs());
|
||||
let this = Self(
|
||||
config.clone(),
|
||||
read_langs(),
|
||||
#[cfg(feature = "redis")]
|
||||
RedisCache::new().await,
|
||||
#[cfg(not(feature = "redis"))]
|
||||
NoCache::new().await,
|
||||
);
|
||||
|
||||
let conn = this.connect().await?;
|
||||
conn.pragma_update(None, "journal_mode", "WAL").unwrap();
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use super::*;
|
||||
use crate::cache::Cache;
|
||||
use crate::model::auth::User;
|
||||
use crate::model::{Error, Result, journal::JournalPage, permissions::FinePermission};
|
||||
use crate::{auto_method, execute, get, query_row};
|
||||
|
@ -26,7 +27,7 @@ impl DataManager {
|
|||
}
|
||||
}
|
||||
|
||||
auto_method!(get_page_by_id()@get_page_from_row -> "SELECT * FROM pages WHERE id = $1" --name="journal page" --returns=JournalPage);
|
||||
auto_method!(get_page_by_id()@get_page_from_row -> "SELECT * FROM pages WHERE id = $1" --name="journal page" --returns=JournalPage --cache-key-tmpl="atto.page:{}");
|
||||
|
||||
/// Create a new journal page in the database.
|
||||
///
|
||||
|
@ -77,9 +78,9 @@ impl DataManager {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
auto_method!(delete_page()@get_page_by_id:MANAGE_JOURNAL_PAGES -> "DELETE FROM pages WHERE id = $1");
|
||||
auto_method!(update_page_title(String)@get_page_by_id:MANAGE_JOURNAL_PAGES -> "UPDATE pages SET title = $1 WHERE id = $2");
|
||||
auto_method!(update_page_prompt(String)@get_page_by_id:MANAGE_JOURNAL_PAGES -> "UPDATE pages SET prompt = $1 WHERE id = $2");
|
||||
auto_method!(update_page_read_access(String)@get_page_by_id:MANAGE_JOURNAL_PAGES -> "UPDATE pages SET read_access = $1 WHERE id = $2" --serde);
|
||||
auto_method!(update_page_write_access(String)@get_page_by_id:MANAGE_JOURNAL_PAGES -> "UPDATE pages SET write_access = $1 WHERE id = $2" --serde);
|
||||
auto_method!(delete_page()@get_page_by_id:MANAGE_JOURNAL_PAGES -> "DELETE FROM pages WHERE id = $1" --cache-key-tmpl="atto.page:{}");
|
||||
auto_method!(update_page_title(String)@get_page_by_id:MANAGE_JOURNAL_PAGES -> "UPDATE pages SET title = $1 WHERE id = $2" --cache-key-tmpl="atto.page:{}");
|
||||
auto_method!(update_page_prompt(String)@get_page_by_id:MANAGE_JOURNAL_PAGES -> "UPDATE pages SET prompt = $1 WHERE id = $2" --cache-key-tmpl="atto.page:{}");
|
||||
auto_method!(update_page_read_access(String)@get_page_by_id:MANAGE_JOURNAL_PAGES -> "UPDATE pages SET read_access = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.page:{}");
|
||||
auto_method!(update_page_write_access(String)@get_page_by_id:MANAGE_JOURNAL_PAGES -> "UPDATE pages SET write_access = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.page:{}");
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
pub mod cache;
|
||||
pub mod config;
|
||||
pub mod database;
|
||||
pub mod model;
|
||||
|
|
|
@ -12,10 +12,10 @@ pub fn unix_epoch_timestamp() -> u128 {
|
|||
}
|
||||
|
||||
/// Get a [`i64`] timestamp from the given `year` epoch
|
||||
pub fn epoch_timestamp(year: i32) -> i64 {
|
||||
pub fn epoch_timestamp(year: u32) -> i64 {
|
||||
let now = Utc::now().timestamp_millis();
|
||||
let then = Utc
|
||||
.with_ymd_and_hms(year, 1, 1, 0, 0, 0)
|
||||
.with_ymd_and_hms(year as i32, 1, 1, 0, 0, 0)
|
||||
.unwrap()
|
||||
.timestamp_millis();
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue