gitfetch_rs/cache/
sqlite.rs1use anyhow::Result;
2use chrono::{DateTime, Duration, Utc};
3use rusqlite::{Connection, params};
4
5pub struct CacheManager {
6 conn: Connection,
7 cache_expiry_minutes: i64,
8}
9
10impl CacheManager {
11 pub fn new(cache_expiry_minutes: u32) -> Result<Self> {
12 let project_dirs = directories::ProjectDirs::from("com", "gitfetch", "gitfetch")
13 .ok_or_else(|| anyhow::anyhow!("Could not determine cache directory"))?;
14
15 let cache_dir = project_dirs.data_local_dir();
16 std::fs::create_dir_all(cache_dir)?;
17
18 let db_path = cache_dir.join("cache.db");
19 let conn = Connection::open(db_path)?;
20
21 conn.execute(
22 "CREATE TABLE IF NOT EXISTS users (
23 username TEXT PRIMARY KEY,
24 user_data TEXT NOT NULL,
25 stats_data TEXT NOT NULL,
26 cached_at TEXT NOT NULL
27 )",
28 [],
29 )?;
30
31 conn.execute(
32 "CREATE INDEX IF NOT EXISTS idx_cached_at ON users(cached_at)",
33 [],
34 )?;
35
36 Ok(Self {
37 conn,
38 cache_expiry_minutes: cache_expiry_minutes as i64,
39 })
40 }
41
42 pub fn get_cached_user_data(&self, username: &str) -> Result<Option<serde_json::Value>> {
43 let mut stmt = self
44 .conn
45 .prepare("SELECT user_data, cached_at FROM users WHERE username = ?")?;
46
47 let result = stmt.query_row(params![username], |row| {
48 let user_data: String = row.get(0)?;
49 let cached_at: String = row.get(1)?;
50 Ok((user_data, cached_at))
51 });
52
53 match result {
54 Ok((user_data, cached_at)) => {
55 if self.is_cache_expired(&cached_at)? {
56 return Ok(None);
57 }
58 let data = serde_json::from_str(&user_data)?;
59 Ok(Some(data))
60 }
61 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
62 Err(e) => Err(e.into()),
63 }
64 }
65
66 pub fn get_cached_stats(&self, username: &str) -> Result<Option<serde_json::Value>> {
67 let mut stmt = self
68 .conn
69 .prepare("SELECT stats_data, cached_at FROM users WHERE username = ?")?;
70
71 let result = stmt.query_row(params![username], |row| {
72 let stats_data: String = row.get(0)?;
73 let cached_at: String = row.get(1)?;
74 Ok((stats_data, cached_at))
75 });
76
77 match result {
78 Ok((stats_data, cached_at)) => {
79 if self.is_cache_expired(&cached_at)? {
80 return Ok(None);
81 }
82 let data = serde_json::from_str(&stats_data)?;
83 Ok(Some(data))
84 }
85 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
86 Err(e) => Err(e.into()),
87 }
88 }
89
90 pub fn cache_user_data(
91 &self,
92 username: &str,
93 user_data: &serde_json::Value,
94 stats: &serde_json::Value,
95 ) -> Result<()> {
96 let now = Utc::now().to_rfc3339();
97 let user_data_str = serde_json::to_string(user_data)?;
98 let stats_str = serde_json::to_string(stats)?;
99
100 self.conn.execute(
101 "INSERT OR REPLACE INTO users (username, user_data, stats_data, cached_at)
102 VALUES (?1, ?2, ?3, ?4)",
103 params![username, user_data_str, stats_str, now],
104 )?;
105
106 Ok(())
107 }
108
109 fn is_cache_expired(&self, cached_at: &str) -> Result<bool> {
110 let cached_time = DateTime::parse_from_rfc3339(cached_at)?;
111 let expiry_time = Utc::now() - Duration::minutes(self.cache_expiry_minutes);
112 Ok(cached_time < expiry_time)
113 }
114
115 pub fn clear(&self) -> Result<()> {
116 self.conn.execute("DELETE FROM users", [])?;
117 Ok(())
118 }
119}