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