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"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223"
|
checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "arc-swap"
|
||||||
|
version = "1.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "arg_enum_proc_macro"
|
name = "arg_enum_proc_macro"
|
||||||
version = "0.3.4"
|
version = "0.3.4"
|
||||||
|
@ -403,6 +409,16 @@ version = "1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
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]]
|
[[package]]
|
||||||
name = "cookie"
|
name = "cookie"
|
||||||
version = "0.18.1"
|
version = "0.18.1"
|
||||||
|
@ -2015,6 +2031,23 @@ dependencies = [
|
||||||
"crossbeam-utils",
|
"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]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.5.10"
|
version = "0.5.10"
|
||||||
|
@ -2326,6 +2359,12 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sha1_smol"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sha2"
|
name = "sha2"
|
||||||
version = "0.10.8"
|
version = "0.10.8"
|
||||||
|
@ -2571,6 +2610,7 @@ dependencies = [
|
||||||
"bb8-postgres",
|
"bb8-postgres",
|
||||||
"bitflags 2.9.0",
|
"bitflags 2.9.0",
|
||||||
"pathbufd",
|
"pathbufd",
|
||||||
|
"redis",
|
||||||
"rusqlite",
|
"rusqlite",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
|
|
@ -19,7 +19,7 @@ axum = { version = "0.8.1", features = ["macros"] }
|
||||||
tokio = { version = "1.44.1", features = ["macros", "rt-multi-thread"] }
|
tokio = { version = "1.44.1", features = ["macros", "rt-multi-thread"] }
|
||||||
axum-extra = { version = "0.10.0", features = ["cookie", "multipart"] }
|
axum-extra = { version = "0.10.0", features = ["cookie", "multipart"] }
|
||||||
tetratto-shared = { path = "../shared" }
|
tetratto-shared = { path = "../shared" }
|
||||||
tetratto-core = { path = "../core", default-features = false }
|
tetratto-core = { path = "../core", features = ["redis"], default-features = false }
|
||||||
tetratto-l10n = { path = "../l10n" }
|
tetratto-l10n = { path = "../l10n" }
|
||||||
|
|
||||||
image = "0.25.5"
|
image = "0.25.5"
|
||||||
|
|
|
@ -6,7 +6,8 @@ edition = "2024"
|
||||||
[features]
|
[features]
|
||||||
postgres = ["dep:tokio-postgres", "dep:bb8-postgres"]
|
postgres = ["dep:tokio-postgres", "dep:bb8-postgres"]
|
||||||
sqlite = ["dep:rusqlite"]
|
sqlite = ["dep:rusqlite"]
|
||||||
default = ["sqlite"]
|
redis = ["dep:redis"]
|
||||||
|
default = ["sqlite", "redis"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
pathbufd = "0.1.4"
|
pathbufd = "0.1.4"
|
||||||
|
@ -16,6 +17,8 @@ tetratto-shared = { path = "../shared" }
|
||||||
tetratto-l10n = { path = "../l10n" }
|
tetratto-l10n = { path = "../l10n" }
|
||||||
serde_json = "1.0.140"
|
serde_json = "1.0.140"
|
||||||
|
|
||||||
|
redis = { version = "0.29.2", optional = true }
|
||||||
|
|
||||||
rusqlite = { version = "0.34.0", optional = true }
|
rusqlite = { version = "0.34.0", optional = true }
|
||||||
|
|
||||||
tokio-postgres = { version = "0.7.13", 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 super::*;
|
||||||
|
use crate::cache::Cache;
|
||||||
use crate::model::{
|
use crate::model::{
|
||||||
Error, Result,
|
Error, Result,
|
||||||
auth::{Token, User},
|
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_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);
|
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.
|
/// Get a user given just their auth token.
|
||||||
///
|
///
|
||||||
|
@ -130,8 +131,11 @@ impl DataManager {
|
||||||
return Err(Error::DatabaseError(e.to_string()));
|
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(())
|
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) => {
|
($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_> {
|
pub async fn $name(&self, selector: $selector_t) -> Result<$returns_> {
|
||||||
let conn = match self.connect().await {
|
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) => {
|
($name:ident()@$select_fn:ident:$permission:ident -> $query:literal) => {
|
||||||
pub async fn $name(&self, id: usize, user: User) -> Result<()> {
|
pub async fn $name(&self, id: usize, user: User) -> Result<()> {
|
||||||
let page = self.$select_fn(id).await?;
|
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) => {
|
($name:ident($x:ty)@$select_fn:ident:$permission:ident -> $query:literal) => {
|
||||||
pub async fn $name(&self, id: usize, user: User, x: $x) -> Result<()> {
|
pub async fn $name(&self, id: usize, user: User, x: $x) -> Result<()> {
|
||||||
let y = self.$select_fn(id).await?;
|
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) => {
|
($name:ident($x:ty)@$select_fn:ident:$permission:ident -> $query:literal --serde) => {
|
||||||
pub async fn $name(&self, id: usize, user: User, x: $x) -> Result<()> {
|
pub async fn $name(&self, id: usize, user: User, x: $x) -> Result<()> {
|
||||||
let y = self.$select_fn(id).await?;
|
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) => {
|
($name:ident($x:ty) -> $query:literal) => {
|
||||||
pub async fn $name(&self, id: usize, x: $x) -> Result<()> {
|
pub async fn $name(&self, id: usize, x: $x) -> Result<()> {
|
||||||
let conn = match self.connect().await {
|
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) => {
|
($name:ident($x:ty) -> $query:literal --serde) => {
|
||||||
pub async fn $name(&self, id: usize, x: $x) -> Result<()> {
|
pub async fn $name(&self, id: usize, x: $x) -> Result<()> {
|
||||||
let conn = match self.connect().await {
|
let conn = match self.connect().await {
|
||||||
|
@ -172,4 +326,27 @@ macro_rules! auto_method {
|
||||||
Ok(())
|
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 crate::config::Config;
|
||||||
use bb8_postgres::{
|
use bb8_postgres::{
|
||||||
PostgresConnectionManager,
|
PostgresConnectionManager,
|
||||||
|
@ -13,13 +20,15 @@ pub type Connection<'a> = PooledConnection<'a, PostgresConnectionManager<NoTls>>
|
||||||
pub struct DataManager(
|
pub struct DataManager(
|
||||||
pub Config,
|
pub Config,
|
||||||
pub HashMap<String, LangFile>,
|
pub HashMap<String, LangFile>,
|
||||||
|
#[cfg(feature = "redis")] pub RedisCache,
|
||||||
|
#[cfg(not(feature = "redis"))] pub NoCache,
|
||||||
pub Pool<PostgresConnectionManager<NoTls>>,
|
pub Pool<PostgresConnectionManager<NoTls>>,
|
||||||
);
|
);
|
||||||
|
|
||||||
impl DataManager {
|
impl DataManager {
|
||||||
/// Obtain a connection to the staging database.
|
/// Obtain a connection to the staging database.
|
||||||
pub(crate) async fn connect(&self) -> Result<Connection> {
|
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).
|
/// Create a new [`DataManager`] (and init database).
|
||||||
|
@ -36,7 +45,15 @@ impl DataManager {
|
||||||
);
|
);
|
||||||
|
|
||||||
let pool = Pool::builder().max_size(15).build(manager).await.unwrap();
|
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 crate::config::Config;
|
||||||
use rusqlite::{Connection, Result};
|
use rusqlite::{Connection, Result};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use tetratto_l10n::{LangFile, read_langs};
|
use tetratto_l10n::{LangFile, read_langs};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[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 {
|
impl DataManager {
|
||||||
/// Obtain a connection to the staging database.
|
/// Obtain a connection to the staging database.
|
||||||
|
@ -14,7 +26,14 @@ impl DataManager {
|
||||||
|
|
||||||
/// Create a new [`DataManager`] (and init database).
|
/// Create a new [`DataManager`] (and init database).
|
||||||
pub async fn new(config: Config) -> Result<Self> {
|
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?;
|
let conn = this.connect().await?;
|
||||||
conn.pragma_update(None, "journal_mode", "WAL").unwrap();
|
conn.pragma_update(None, "journal_mode", "WAL").unwrap();
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::cache::Cache;
|
||||||
use crate::model::auth::User;
|
use crate::model::auth::User;
|
||||||
use crate::model::{Error, Result, journal::JournalPage, permissions::FinePermission};
|
use crate::model::{Error, Result, journal::JournalPage, permissions::FinePermission};
|
||||||
use crate::{auto_method, execute, get, query_row};
|
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.
|
/// Create a new journal page in the database.
|
||||||
///
|
///
|
||||||
|
@ -77,9 +78,9 @@ impl DataManager {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
auto_method!(delete_page()@get_page_by_id:MANAGE_JOURNAL_PAGES -> "DELETE FROM pages WHERE id = $1");
|
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");
|
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");
|
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);
|
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);
|
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 config;
|
||||||
pub mod database;
|
pub mod database;
|
||||||
pub mod model;
|
pub mod model;
|
||||||
|
|
|
@ -12,10 +12,10 @@ pub fn unix_epoch_timestamp() -> u128 {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a [`i64`] timestamp from the given `year` epoch
|
/// 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 now = Utc::now().timestamp_millis();
|
||||||
let then = Utc
|
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()
|
.unwrap()
|
||||||
.timestamp_millis();
|
.timestamp_millis();
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue