1use crate::config;
8use crate::error::{MtgjsonError, Result};
9use crate::ProgressCallback;
10use flate2::read::GzDecoder;
11use reqwest::blocking::Client;
12use std::fs;
13use std::io::{BufReader, Read, Write};
14use std::path::{Path, PathBuf};
15use std::time::Duration;
16
17pub struct CacheManager {
22 pub cache_dir: PathBuf,
24 pub offline: bool,
26 timeout: Duration,
27 client: Option<Client>,
28 remote_ver: Option<String>,
29 on_progress: Option<ProgressCallback>,
30}
31
32impl CacheManager {
33 pub fn new(
38 cache_dir: Option<PathBuf>,
39 offline: bool,
40 timeout: Duration,
41 on_progress: Option<ProgressCallback>,
42 ) -> Result<Self> {
43 let dir = cache_dir.unwrap_or_else(config::default_cache_dir);
44 fs::create_dir_all(&dir)?;
45 Ok(Self {
46 cache_dir: dir,
47 offline,
48 timeout,
49 client: None,
50 remote_ver: None,
51 on_progress,
52 })
53 }
54
55 pub fn client(&mut self) -> &Client {
57 if self.client.is_none() {
58 self.client = Some(
59 Client::builder()
60 .timeout(self.timeout)
61 .redirect(reqwest::redirect::Policy::limited(10))
62 .build()
63 .expect("failed to build HTTP client"),
64 );
65 }
66 self.client.as_ref().unwrap()
67 }
68
69 fn local_version(&self) -> Option<String> {
71 let version_file = self.cache_dir.join("version.txt");
72 if version_file.exists() {
73 fs::read_to_string(&version_file)
74 .ok()
75 .map(|s| s.trim().to_string())
76 } else {
77 None
78 }
79 }
80
81 fn save_version(&self, version: &str) {
83 let version_file = self.cache_dir.join("version.txt");
84 let _ = fs::write(version_file, version);
85 }
86
87 pub fn remote_version(&mut self) -> Result<Option<String>> {
92 if self.remote_ver.is_some() {
93 return Ok(self.remote_ver.clone());
94 }
95 if self.offline {
96 return Ok(None);
97 }
98 let client = self.client().clone();
99 match client.get(config::META_URL).send() {
100 Ok(resp) => {
101 let resp = resp.error_for_status()?;
102 let data: serde_json::Value = resp.json()?;
103 let version = data
105 .get("data")
106 .and_then(|d| d.get("version"))
107 .and_then(|v| v.as_str())
108 .or_else(|| {
109 data.get("meta")
110 .and_then(|m| m.get("version"))
111 .and_then(|v| v.as_str())
112 })
113 .map(|s| s.to_string());
114 self.remote_ver = version.clone();
115 Ok(version)
116 }
117 Err(e) => {
118 eprintln!("Failed to fetch MTGJSON version from CDN: {}", e);
119 Ok(None)
120 }
121 }
122 }
123
124 pub fn is_stale(&mut self) -> Result<bool> {
129 let local = self.local_version();
130 match local {
131 None => Ok(true),
132 Some(local_ver) => {
133 let remote = self.remote_version()?;
134 match remote {
135 None => Ok(false), Some(remote_ver) => Ok(local_ver != remote_ver),
137 }
138 }
139 }
140 }
141
142 fn download_file(&mut self, filename: &str, dest: &Path) -> Result<()> {
147 let url = format!("{}/{}", config::CDN_BASE, filename);
148 eprintln!("Downloading {}", url);
149
150 if let Some(parent) = dest.parent() {
151 fs::create_dir_all(parent)?;
152 }
153
154 let tmp_dest = dest.with_extension(format!(
155 "{}.tmp",
156 dest.extension()
157 .and_then(|e| e.to_str())
158 .unwrap_or("")
159 ));
160
161 let client = self.client().clone();
162 let on_progress = self.on_progress.clone();
163 let fname = filename.to_string();
164
165 let result = (|| -> Result<()> {
166 let mut resp = client.get(&url).send()?.error_for_status()?;
167 let total = resp.content_length().unwrap_or(0);
168
169 if on_progress.is_some() {
170 let mut file = fs::File::create(&tmp_dest)?;
172 let mut downloaded: u64 = 0;
173 let mut buf = [0u8; 8192];
174 loop {
175 let n = resp.read(&mut buf)?;
176 if n == 0 {
177 break;
178 }
179 file.write_all(&buf[..n])?;
180 downloaded += n as u64;
181 if let Some(ref cb) = on_progress {
182 cb(&fname, downloaded, total);
183 }
184 }
185 } else {
186 let bytes = resp.bytes()?;
188 fs::write(&tmp_dest, &bytes)?;
189 }
190
191 fs::rename(&tmp_dest, dest)?;
192 Ok(())
193 })();
194
195 if result.is_err() {
196 let _ = fs::remove_file(&tmp_dest);
198 }
199
200 result
201 }
202
203 pub fn ensure_parquet(&mut self, view_name: &str) -> Result<PathBuf> {
213 let parquet_files = config::parquet_files();
214 let filename = parquet_files.get(view_name).ok_or_else(|| {
215 MtgjsonError::NotFound(format!("Unknown parquet view: {}", view_name))
216 })?;
217
218 let local_path = self.cache_dir.join(filename);
219
220 if !local_path.exists() || self.is_stale()? {
221 if self.offline {
222 if local_path.exists() {
223 return Ok(local_path);
224 }
225 return Err(MtgjsonError::NotFound(format!(
226 "Parquet file {} not cached and offline mode is enabled",
227 filename
228 )));
229 }
230 self.download_file(filename, &local_path)?;
231 if let Ok(Some(version)) = self.remote_version() {
233 self.save_version(&version);
234 }
235 }
236
237 Ok(local_path)
238 }
239
240 pub fn ensure_json(&mut self, name: &str) -> Result<PathBuf> {
250 let json_files = config::json_files();
251 let filename = json_files.get(name).ok_or_else(|| {
252 MtgjsonError::NotFound(format!("Unknown JSON file: {}", name))
253 })?;
254
255 let local_path = self.cache_dir.join(filename);
256
257 if !local_path.exists() || self.is_stale()? {
258 if self.offline {
259 if local_path.exists() {
260 return Ok(local_path);
261 }
262 return Err(MtgjsonError::NotFound(format!(
263 "JSON file {} not cached and offline mode is enabled",
264 filename
265 )));
266 }
267 self.download_file(filename, &local_path)?;
268 if let Ok(Some(version)) = self.remote_version() {
270 self.save_version(&version);
271 }
272 }
273
274 Ok(local_path)
275 }
276
277 pub fn load_json(&mut self, name: &str) -> Result<serde_json::Value> {
282 let path = self.ensure_json(name)?;
283
284 let parse_result = if path.extension().and_then(|e| e.to_str()) == Some("gz") {
285 let file = fs::File::open(&path)?;
286 let reader = BufReader::new(file);
287 let decoder = GzDecoder::new(reader);
288 let mut buf_reader = BufReader::new(decoder);
289 let mut contents = String::new();
290 buf_reader.read_to_string(&mut contents)?;
291 serde_json::from_str(&contents).map_err(MtgjsonError::from)
292 } else {
293 let contents = fs::read_to_string(&path)?;
294 serde_json::from_str(&contents).map_err(MtgjsonError::from)
295 };
296
297 match parse_result {
298 Ok(value) => Ok(value),
299 Err(e) => {
300 eprintln!(
301 "Corrupt cache file {}: {} -- removing",
302 path.display(),
303 e
304 );
305 let _ = fs::remove_file(&path);
306 Err(MtgjsonError::NotFound(format!(
307 "Cache file '{}' was corrupt and has been removed. \
308 Retry to re-download. Original error: {}",
309 path.file_name()
310 .and_then(|n| n.to_str())
311 .unwrap_or("unknown"),
312 e
313 )))
314 }
315 }
316 }
317
318 pub fn clear(&self) -> Result<()> {
320 if self.cache_dir.exists() {
321 fs::remove_dir_all(&self.cache_dir)?;
322 fs::create_dir_all(&self.cache_dir)?;
323 }
324 Ok(())
325 }
326
327 pub fn close(&mut self) {
329 self.client = None;
330 }
331}