gitfetch_rs/cache/
sqlite.rs

1use 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}