openjd_snapshots/
s3_check_cache.rs1use std::path::{Path, PathBuf};
6use std::sync::Mutex;
7use std::time::{SystemTime, UNIX_EPOCH};
8
9const ENTRY_EXPIRY_DAYS: u64 = 30;
10
11pub struct S3CheckCache {
12 conn: Mutex<rusqlite::Connection>,
13}
14
15impl S3CheckCache {
16 pub fn new(cache_dir: impl AsRef<Path>) -> crate::Result<Self> {
17 let dir = cache_dir.as_ref();
18 std::fs::create_dir_all(dir)?;
19 let db_path = dir.join("s3_check_cache.db");
20 let conn = rusqlite::Connection::open(&db_path)
21 .map_err(|e| crate::SnapshotError::Cache(e.to_string()))?;
22 conn.pragma_update(None, "journal_mode", "WAL")
23 .map_err(|e| crate::SnapshotError::Cache(e.to_string()))?;
24 conn.execute_batch(
25 "CREATE TABLE IF NOT EXISTS s3checkV1(
26 s3_key text primary key,
27 last_seen_time timestamp
28 );",
29 )
30 .map_err(|e| crate::SnapshotError::Cache(e.to_string()))?;
31
32 let cutoff = SystemTime::now()
34 .duration_since(UNIX_EPOCH)
35 .unwrap_or_default()
36 .as_secs_f64()
37 - (ENTRY_EXPIRY_DAYS as f64 * 86400.0);
38 conn.execute(
39 "DELETE FROM s3checkV1 WHERE CAST(last_seen_time AS REAL) < ?1",
40 rusqlite::params![cutoff],
41 )
42 .map_err(|e| crate::SnapshotError::Cache(e.to_string()))?;
43
44 Ok(Self {
45 conn: Mutex::new(conn),
46 })
47 }
48
49 pub fn open_default() -> crate::Result<Self> {
50 let home = std::env::var("HOME").map_err(|_| {
51 crate::SnapshotError::Cache("$HOME is not set, cannot locate default cache".into())
52 })?;
53 Self::new(PathBuf::from(home).join(".deadline/job_attachments"))
54 }
55
56 pub fn get_entry(&self, s3_key: &str) -> Option<String> {
58 let conn = self.conn.lock().unwrap();
59 let result: String = conn
60 .query_row(
61 "SELECT last_seen_time FROM s3checkV1 WHERE s3_key = ?1",
62 rusqlite::params![s3_key],
63 |row| match row.get_ref(0)?.as_str() {
64 Ok(s) => Ok(s.to_string()),
65 Err(_) => {
66 let f: f64 = row.get(0)?;
67 Ok(f.to_string())
68 }
69 },
70 )
71 .ok()?;
72
73 let last_seen: f64 = result.parse().ok()?;
74 let now = SystemTime::now()
75 .duration_since(UNIX_EPOCH)
76 .ok()?
77 .as_secs_f64();
78 if (now - last_seen) / 86400.0 < ENTRY_EXPIRY_DAYS as f64 {
79 Some(result)
80 } else {
81 None
82 }
83 }
84
85 pub fn put_entry(&self, s3_key: &str) -> crate::Result<()> {
87 let now = SystemTime::now()
88 .duration_since(UNIX_EPOCH)
89 .map_err(|e| crate::SnapshotError::Cache(e.to_string()))?
90 .as_secs_f64();
91 let time_str = rusqlite::types::Value::Text(now.to_string());
92 let conn = self.conn.lock().unwrap();
93 conn.execute(
94 "INSERT OR REPLACE INTO s3checkV1 VALUES(?1, ?2)",
95 rusqlite::params![s3_key, time_str],
96 )
97 .map_err(|e| crate::SnapshotError::Cache(e.to_string()))?;
98 Ok(())
99 }
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105 use tempfile::TempDir;
106
107 #[test]
108 fn put_and_get() {
109 let tmp = TempDir::new().unwrap();
110 let cache = S3CheckCache::new(tmp.path()).unwrap();
111 cache.put_entry("bucket/Data/abc123.xxh128").unwrap();
112 assert!(cache.get_entry("bucket/Data/abc123.xxh128").is_some());
113 }
114
115 #[test]
116 fn missing_entry_returns_none() {
117 let tmp = TempDir::new().unwrap();
118 let cache = S3CheckCache::new(tmp.path()).unwrap();
119 assert!(cache.get_entry("nonexistent").is_none());
120 }
121
122 #[test]
123 fn expired_entry_returns_none() {
124 let tmp = TempDir::new().unwrap();
125 let cache = S3CheckCache::new(tmp.path()).unwrap();
126 let old_time = SystemTime::now()
127 .duration_since(UNIX_EPOCH)
128 .unwrap()
129 .as_secs_f64()
130 - (31.0 * 86400.0);
131 let time_str = rusqlite::types::Value::Text(old_time.to_string());
132 let conn = cache.conn.lock().unwrap();
133 conn.execute(
134 "INSERT OR REPLACE INTO s3checkV1 VALUES(?1, ?2)",
135 rusqlite::params!["old_key", time_str],
136 )
137 .unwrap();
138 drop(conn);
139 assert!(cache.get_entry("old_key").is_none());
140 }
141
142 #[test]
143 fn recent_entry_not_expired() {
144 let tmp = TempDir::new().unwrap();
145 let cache = S3CheckCache::new(tmp.path()).unwrap();
146 let recent_time = SystemTime::now()
147 .duration_since(UNIX_EPOCH)
148 .unwrap()
149 .as_secs_f64()
150 - 86400.0;
151 let time_str = rusqlite::types::Value::Text(recent_time.to_string());
152 let conn = cache.conn.lock().unwrap();
153 conn.execute(
154 "INSERT OR REPLACE INTO s3checkV1 VALUES(?1, ?2)",
155 rusqlite::params!["recent_key", time_str],
156 )
157 .unwrap();
158 drop(conn);
159 assert!(cache.get_entry("recent_key").is_some());
160 }
161}