gitfetch_rs/cache/
sqlite.rs

1use 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    // Add version column if it doesn't exist (for migration)
34    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    // Clear cache if version changed
45    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        // Check version match
71        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        // Check version match
100        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}