use anyhow::Result; use chrono::prelude::*; use diesel::prelude::*; use lazy_static::lazy_static; use r2d2_redis::{r2d2, redis, RedisConnectionManager}; use serde::{Deserialize, Serialize}; use crate::{config, db, markdown, web}; pub type RedisPool = r2d2::Pool; pub type ConnectionPool = r2d2::PooledConnection; pub type Connection = redis::Connection; pub fn establish_connection() -> Result { Ok(redis::Client::open(config::CONFIG.redis_url.as_str())?.get_connection()?) } pub fn pool() -> Result { Ok(r2d2::Pool::builder().build(RedisConnectionManager::new( config::CONFIG.redis_url.as_str(), )?)?) } lazy_static! { pub static ref POOL: RedisPool = pool().unwrap(); } pub mod keys { pub const POSTS: &str = "posts"; pub const POST: &str = "post:"; pub fn post(id: i32) -> String { format!("{}{}", POST, id) } pub const TAGS: &str = "tags"; pub const TAG_POSTS: &str = "tag_posts:"; pub fn tag_posts(id: i32) -> String { format!("{}{}", TAG_POSTS, id) } } pub fn clear(redis_conn: &mut Connection) -> Result<()> { redis::cmd("UNLINK").arg(keys::POSTS).query(redis_conn)?; for key in redis::cmd("KEYS") .arg(format!("{}*", keys::POST)) .query::>(redis_conn)? { redis::cmd("UNLINK").arg(key).query(redis_conn)?; } redis::cmd("UNLINK").arg(keys::TAGS).query(redis_conn)?; for key in redis::cmd("KEYS") .arg(format!("{}*", keys::TAG_POSTS)) .query::>(redis_conn)? { redis::cmd("UNLINK").arg(key).query(redis_conn)?; } Ok(()) } pub fn cache_posts( db_conn: &mut db::Connection, redis_conn: &mut Connection, ) -> Result> { if let Some(s) = redis::cmd("GET") .arg(keys::POSTS) .query::>(redis_conn)? { Ok(serde_json::from_str(&s)?) } else { let posts_cache: Vec = db::schema::posts::table .select(( db::schema::posts::id, db::schema::posts::name, db::schema::posts::slug, db::schema::posts::description, db::schema::posts::published_at, db::schema::posts::edited_at, )) .filter(db::schema::posts::active) .order(db::schema::posts::published_at.desc()) .load::<(i32, String, String, String, NaiveDate, Option)>(db_conn)? .into_iter() .map( |p: (i32, String, String, String, NaiveDate, Option)| -> Result<_> { let (id, name, slug, description, published_at, edited_at) = p; let tags = web::get_tags_by_post(id, db_conn)?; Ok(web::templates::PostIndexPost { name, slug, description, published_at, edited_at, tags, }) }, ) .collect::>()?; redis::cmd("SET") .arg(keys::POSTS) .arg(serde_json::to_string(&posts_cache)?) .query(redis_conn)?; redis::cmd("EXPIRE") .arg(keys::POSTS) .arg(config::CONFIG.cache_ttl) .query(redis_conn)?; Ok(posts_cache) } } #[derive(Serialize, Deserialize, Debug)] pub struct PostWithoutDescription { pub name: String, pub slug: String, pub published_at: NaiveDate, pub edited_at: Option, pub tags: Vec, pub content: String, } #[derive(Serialize, Deserialize, Debug)] pub struct PostWithoutContent { pub name: String, pub slug: String, pub description: String, pub published_at: NaiveDate, pub edited_at: Option, pub tags: Vec, } pub fn cache_post( id: i32, db_conn: &mut db::Connection, redis_conn: &mut Connection, ) -> Result { let key = keys::post(id); if let Some(s) = redis::cmd("GET") .arg(&key) .query::>(redis_conn)? { Ok(serde_json::from_str(&s)?) } else { let post = db::schema::posts::table .filter(db::schema::posts::id.eq(id)) .first::(db_conn)?; let post_cache = PostWithoutDescription { name: post.name, slug: post.slug, published_at: post.published_at, edited_at: post.edited_at, tags: web::get_tags_by_post(id, db_conn)?, content: markdown::to_html(&post.content), }; redis::cmd("SET") .arg(&key) .arg(serde_json::to_string(&post_cache)?) .query(redis_conn)?; redis::cmd("EXPIRE") .arg(key) .arg(config::CONFIG.cache_ttl) .query(redis_conn)?; Ok(post_cache) } } #[derive(Serialize, Deserialize, Debug)] pub struct PostsByTag { pub name: String, pub posts: Vec, } pub fn cache_tags( db_conn: &mut db::Connection, redis_conn: &mut Connection, ) -> Result> { if let Some(s) = redis::cmd("GET") .arg(keys::TAGS) .query::>(redis_conn)? { Ok(serde_json::from_str(&s)?) } else { let tags = db::schema::tags::table .select((db::schema::tags::id, db::schema::tags::name)) .order(db::schema::tags::name) .load::<(i32, String)>(db_conn)?; let posts_by_tag = tags .into_iter() .map(|(id, name)| -> Result<_> { let post_tag_post_ids = db::schema::post_tags::table .select(db::schema::post_tags::post_id) .filter(db::schema::post_tags::tag_id.eq(id)) .load::(db_conn)?; let posts = db::schema::posts::table .select(( db::schema::posts::id, db::schema::posts::name, db::schema::posts::slug, db::schema::posts::description, db::schema::posts::published_at, db::schema::posts::edited_at, )) .filter(db::schema::posts::active) .filter(db::schema::posts::id.eq_any(post_tag_post_ids)) .order(db::schema::posts::published_at.desc()) .load::<(i32, String, String, String, NaiveDate, Option)>(db_conn)?; Ok( PostsByTag { name, posts: posts .into_iter() .map( |p: ( i32, String, String, String, NaiveDate, Option, )| -> Result<_> { let (id, name, slug, description, published_at, edited_at) = p; let tags = web::get_tags_by_post(id, db_conn)?; Ok(PostWithoutContent { name, slug, description, published_at, edited_at, tags, }) }, ) .collect::>>()?, }, ) }) .collect::>>()?; redis::cmd("SET") .arg(keys::TAGS) .arg(serde_json::to_string(&posts_by_tag)?) .query(redis_conn)?; redis::cmd("EXPIRE") .arg(keys::TAGS) .arg(config::CONFIG.cache_ttl) .query(redis_conn)?; Ok(posts_by_tag) } } pub fn cache_tag_posts( id: i32, db_conn: &mut db::Connection, redis_conn: &mut Connection, ) -> Result> { let key = keys::tag_posts(id); if let Some(s) = redis::cmd("GET") .arg(&key) .query::>(redis_conn)? { Ok(serde_json::from_str(&s)?) } else { let post_tag_post_ids = db::schema::post_tags::table .select(db::schema::post_tags::post_id) .filter(db::schema::post_tags::tag_id.eq(id)) .load::(db_conn)?; let posts: Vec = db::schema::posts::table .select(( db::schema::posts::id, db::schema::posts::name, db::schema::posts::slug, db::schema::posts::description, db::schema::posts::published_at, db::schema::posts::edited_at, )) .filter(db::schema::posts::id.eq_any(post_tag_post_ids)) .filter(db::schema::posts::active) .order(db::schema::posts::published_at.desc()) .load::<(i32, String, String, String, NaiveDate, Option)>(db_conn)? .into_iter() .map( |p: (i32, String, String, String, NaiveDate, Option)| -> Result<_> { let (id, name, slug, description, published_at, edited_at) = p; let tags = web::get_tags_by_post(id, db_conn)?; Ok(web::templates::PostIndexPost { name, slug, description, published_at, edited_at, tags, }) }, ) .collect::>()?; redis::cmd("SET") .arg(&key) .arg(serde_json::to_string(&posts)?) .query(redis_conn)?; redis::cmd("EXPIRE") .arg(key) .arg(config::CONFIG.cache_ttl) .query(redis_conn)?; Ok(posts) } }