From 7ecc8c4270eb4a477f18630b1542e062e2026378 Mon Sep 17 00:00:00 2001 From: Radical Date: Wed, 7 May 2025 20:32:32 +0200 Subject: [PATCH] feat: add redis caching --- Cargo.toml | 1 + src/config.rs | 42 ++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 7 ++++++- src/utils.rs | 27 +++++++++++++++++++++++++++ 4 files changed, 76 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index aca7977..4e2f58d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" simple_logger = "5.0.0" sqlx = { version = "0.8", features = ["runtime-tokio", "tls-native-tls", "postgres"] } +redis = { version = "0.30", features= ["tokio-comp"] } toml = "0.8" url = { version = "2.5", features = ["serde"] } uuid = { version = "1.16", features = ["serde", "v7"] } diff --git a/src/config.rs b/src/config.rs index a2a6192..65a5965 100644 --- a/src/config.rs +++ b/src/config.rs @@ -7,6 +7,7 @@ use tokio::fs::read_to_string; #[derive(Debug, Deserialize)] pub struct ConfigBuilder { database: Database, + cache_database: CacheDatabase, web: Option, } @@ -19,6 +20,15 @@ pub struct Database { port: u16, } +#[derive(Debug, Deserialize, Clone)] +pub struct CacheDatabase { + username: Option, + password: Option, + host: String, + database: Option, + port: u16, +} + #[derive(Debug, Deserialize)] struct WebBuilder { url: Option, @@ -51,6 +61,7 @@ impl ConfigBuilder { Config { database: self.database, + cache_database: self.cache_database, web, } } @@ -59,6 +70,7 @@ impl ConfigBuilder { #[derive(Debug, Clone)] pub struct Config { pub database: Database, + pub cache_database: CacheDatabase, pub web: Web, } @@ -78,3 +90,33 @@ impl Database { .port(self.port) } } + +impl CacheDatabase { + pub fn url(&self) -> String { + let mut url = String::from("redis://"); + + if let Some(username) = &self.username { + url += username; + } + + if let Some(password) = &self.password { + url += ":"; + url += password; + } + + if self.username.is_some() || self.password.is_some() { + url += "@"; + } + + url += &self.host; + url += ":"; + url += &self.port.to_string(); + + if let Some(database) = &self.database { + url += "/"; + url += database; + } + + url + } +} diff --git a/src/main.rs b/src/main.rs index 48b1c4a..c93e760 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,9 +2,10 @@ use actix_cors::Cors; use actix_web::{App, HttpServer, web}; use argon2::Argon2; use clap::Parser; +use redis::aio::MultiplexedConnection; use simple_logger::SimpleLogger; use sqlx::{PgPool, Pool, Postgres}; -use std::time::SystemTime; +use std::{cell::Cell, time::SystemTime}; mod config; use config::{Config, ConfigBuilder}; mod api; @@ -23,6 +24,7 @@ struct Args { #[derive(Clone)] struct Data { pub pool: Pool, + pub cache_pool: redis::Client, pub _config: Config, pub argon2: Argon2<'static>, pub start_time: SystemTime, @@ -44,6 +46,8 @@ async fn main() -> Result<(), Error> { let pool = PgPool::connect_with(config.database.connect_options()).await?; + let cache_pool = redis::Client::open(config.cache_database.url())?; + /* TODO: Figure out if a table should be used here and if not then what. Also figure out if these should be different types from what they currently are and if we should add more "constraints" @@ -81,6 +85,7 @@ async fn main() -> Result<(), Error> { let data = Data { pool, + cache_pool, _config: config, // TODO: Possibly implement "pepper" into this (thinking it could generate one if it doesnt exist and store it on disk) argon2: Argon2::default(), diff --git a/src/utils.rs b/src/utils.rs index 6571fab..5d6b51c 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,6 +1,11 @@ use actix_web::{cookie::{time::Duration, Cookie, SameSite}, http::header::HeaderMap, HttpResponse}; use getrandom::fill; use hex::encode; +use redis::{AsyncCommands, RedisError}; +use serde::Serialize; +use serde_json::json; + +use crate::Data; pub fn get_auth_header(headers: &HeaderMap) -> Result<&str, HttpResponse> { let auth_token = headers.get(actix_web::http::header::AUTHORIZATION); @@ -46,3 +51,25 @@ pub fn generate_refresh_token() -> Result { Ok(encode(buf)) } +impl Data { + pub async fn set_cache_key(&self, key: String, value: impl Serialize, expire: u32) -> Result<(), RedisError> { + let mut conn = self.cache_pool.get_multiplexed_tokio_connection().await?; + + let key_encoded = encode(key); + + let value_json = json!(value).to_string(); + + redis::cmd("SET",).arg(&[key_encoded.clone(), value_json]).exec_async(&mut conn).await?; + + redis::cmd("EXPIRE").arg(&[key_encoded, expire.to_string()]).exec_async(&mut conn).await + } + + pub async fn get_cache_key(&self, key: String) -> Result { + let mut conn = self.cache_pool.get_multiplexed_tokio_connection().await?; + + let key_encoded = encode(key); + + redis::cmd("GET").arg(key_encoded).query_async(&mut conn).await + } +} +