ant_node/upgrade/
binary_cache.rs1use crate::error::{Error, Result};
13use fs2::FileExt;
14use serde::{Deserialize, Serialize};
15use sha2::{Digest, Sha256};
16use std::fs::{self, File};
17use std::io::{Read, Write};
18use std::path::PathBuf;
19use tracing::{debug, warn};
20
21#[derive(Clone)]
23pub struct BinaryCache {
24 cache_dir: PathBuf,
26}
27
28#[derive(Serialize, Deserialize)]
30struct CachedBinaryMeta {
31 version: String,
33 sha256: String,
35 cached_at_epoch_secs: u64,
37}
38
39impl BinaryCache {
40 #[must_use]
42 pub fn new(cache_dir: PathBuf) -> Self {
43 Self { cache_dir }
44 }
45
46 #[must_use]
48 pub fn cached_binary_path(&self, version: &str) -> PathBuf {
49 let name = if cfg!(windows) {
50 format!("ant-node-{version}.exe")
51 } else {
52 format!("ant-node-{version}")
53 };
54 self.cache_dir.join(name)
55 }
56
57 #[must_use]
60 pub fn get_verified(&self, version: &str) -> Option<PathBuf> {
61 let bin_path = self.cached_binary_path(version);
62 let meta_path = self.meta_path(version);
63
64 let meta_data = fs::read_to_string(&meta_path).ok()?;
65 let meta: CachedBinaryMeta = serde_json::from_str(&meta_data).ok()?;
66
67 if meta.version != version {
68 debug!("Binary cache version mismatch in metadata");
69 return None;
70 }
71
72 let actual_hash = sha256_file(&bin_path).ok()?;
73 if actual_hash != meta.sha256 {
74 warn!(
75 "Binary cache SHA-256 mismatch for version {version} (expected {}, got {})",
76 meta.sha256, actual_hash
77 );
78 return None;
79 }
80
81 Some(bin_path)
82 }
83
84 pub fn store(&self, version: &str, source_path: &std::path::Path) -> Result<()> {
95 let hash = sha256_file(source_path)?;
96
97 let dest = self.cached_binary_path(version);
98 let meta_path = self.meta_path(version);
99
100 let tmp_bin = self.cache_dir.join(format!(".ant-node-{version}.tmp"));
103 fs::copy(source_path, &tmp_bin)?;
104 let _ = fs::remove_file(&dest);
105 fs::rename(&tmp_bin, &dest)?;
106
107 let now = std::time::SystemTime::now()
108 .duration_since(std::time::UNIX_EPOCH)
109 .map_err(|e| Error::Upgrade(format!("System clock error: {e}")))?
110 .as_secs();
111
112 let meta = CachedBinaryMeta {
113 version: version.to_string(),
114 sha256: hash,
115 cached_at_epoch_secs: now,
116 };
117
118 let meta_json = serde_json::to_string(&meta)
119 .map_err(|e| Error::Upgrade(format!("Failed to serialize binary cache meta: {e}")))?;
120
121 let tmp_meta = self.cache_dir.join(format!(".ant-node-{version}.meta.tmp"));
123 let mut f = File::create(&tmp_meta)?;
124 f.write_all(meta_json.as_bytes())?;
125 f.sync_all()?;
126 drop(f);
127 let _ = fs::remove_file(&meta_path);
128 fs::rename(&tmp_meta, &meta_path)?;
129
130 debug!("Cached binary for version {version} at {}", dest.display());
131 Ok(())
132 }
133
134 pub fn acquire_download_lock(&self) -> Result<DownloadLockGuard> {
149 let lock_path = self.cache_dir.join("download.lock");
150 let lock = File::create(&lock_path)
151 .map_err(|e| Error::Upgrade(format!("Failed to create download lock: {e}")))?;
152 lock.lock_exclusive()
153 .map_err(|e| Error::Upgrade(format!("Failed to acquire download lock: {e}")))?;
154 Ok(DownloadLockGuard { _file: lock })
155 }
156
157 fn meta_path(&self, version: &str) -> PathBuf {
160 let name = if cfg!(windows) {
161 format!("ant-node-{version}.exe.meta.json")
162 } else {
163 format!("ant-node-{version}.meta.json")
164 };
165 self.cache_dir.join(name)
166 }
167}
168
169pub struct DownloadLockGuard {
173 _file: File,
174}
175
176fn sha256_file(path: &std::path::Path) -> Result<String> {
178 let mut file = File::open(path)?;
179 let mut hasher = Sha256::new();
180 let mut buf = [0u8; 8192];
181 loop {
182 let n = file
183 .read(&mut buf)
184 .map_err(|e| Error::Upgrade(format!("Failed to read file for hashing: {e}")))?;
185 if n == 0 {
186 break;
187 }
188 hasher.update(&buf[..n]);
189 }
190 Ok(hex::encode(hasher.finalize()))
191}
192
193#[cfg(test)]
198#[allow(clippy::unwrap_used, clippy::expect_used)]
199mod tests {
200 use super::*;
201 use tempfile::TempDir;
202
203 #[test]
204 fn test_miss_returns_none() {
205 let tmp = TempDir::new().unwrap();
206 let cache = BinaryCache::new(tmp.path().to_path_buf());
207 assert!(cache.get_verified("1.0.0").is_none());
208 }
209
210 #[test]
211 fn test_store_and_get_verified() {
212 let tmp = TempDir::new().unwrap();
213 let cache = BinaryCache::new(tmp.path().to_path_buf());
214
215 let src = tmp.path().join("source-bin");
217 fs::write(&src, b"hello world binary").unwrap();
218
219 cache.store("1.2.3", &src).unwrap();
220
221 let result = cache.get_verified("1.2.3");
222 assert!(result.is_some());
223 let cached_path = result.unwrap();
224 assert_eq!(fs::read(&cached_path).unwrap(), b"hello world binary");
225 }
226
227 #[test]
228 fn test_sha256_mismatch_returns_none() {
229 let tmp = TempDir::new().unwrap();
230 let cache = BinaryCache::new(tmp.path().to_path_buf());
231
232 let src = tmp.path().join("source-bin");
234 fs::write(&src, b"original content").unwrap();
235 cache.store("1.0.0", &src).unwrap();
236
237 let cached = cache.cached_binary_path("1.0.0");
239 fs::write(&cached, b"corrupted content").unwrap();
240
241 assert!(cache.get_verified("1.0.0").is_none());
242 }
243
244 #[test]
245 fn test_missing_meta_returns_none() {
246 let tmp = TempDir::new().unwrap();
247 let cache = BinaryCache::new(tmp.path().to_path_buf());
248
249 let cached = cache.cached_binary_path("1.0.0");
251 fs::write(&cached, b"binary data").unwrap();
252
253 assert!(cache.get_verified("1.0.0").is_none());
254 }
255}