1use bevy::prelude::*;
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use std::io::{BufWriter, Read};
5use std::time::{Duration, SystemTime, UNIX_EPOCH};
6
7use crate::config::CacheConfig;
8use crate::error::CacheError;
9
10#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
12pub struct CacheEntry {
13 pub file_name: String,
15 pub created_at: u64,
17 pub modified_at: u64,
19 pub size_bytes: u64,
21 #[serde(default, skip_serializing_if = "Option::is_none")]
26 pub max_age_secs: Option<u64>,
27}
28
29#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Resource, Default)]
31pub struct CacheManifest {
32 pub entries: HashMap<String, CacheEntry>,
33}
34
35impl CacheManifest {
36 pub fn load_from_disk(config: &CacheConfig) -> Result<Self, CacheError> {
43 let path = config.manifest_fs_path();
44 match std::fs::read_to_string(&path) {
45 Ok(contents) => {
46 let manifest: CacheManifest = ron::from_str(&contents)?;
47 Ok(manifest)
48 }
49 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Self::default()),
50 Err(e) => Err(CacheError::Io(e)),
51 }
52 }
53
54 pub fn save_to_disk(&self, config: &CacheConfig) -> Result<(), CacheError> {
56 config.ensure_cache_dir()?;
57 let path = config.manifest_fs_path();
58 let pretty = ron::ser::PrettyConfig::default();
59 let serialized =
60 ron::ser::to_string_pretty(self, pretty).map_err(CacheError::RonSerialize)?;
61 std::fs::write(path, serialized)?;
62 Ok(())
63 }
64
65 pub fn store<R: Read>(
82 &mut self,
83 config: &CacheConfig,
84 key: &str,
85 extension: &str,
86 mut reader: R,
87 max_age: Option<Duration>,
88 ) -> Result<(), CacheError> {
89 config.validate_key(key)?;
90 config.ensure_cache_dir()?;
91
92 let file_name = format!("{key}.{extension}");
93 let fs_path = config.file_path(&file_name);
94 let file = std::fs::File::create(&fs_path)?;
95 let mut writer = BufWriter::new(file);
96 let size_bytes = std::io::copy(&mut reader, &mut writer)?;
97
98 let now = SystemTime::now()
99 .duration_since(UNIX_EPOCH)
100 .unwrap_or_default()
101 .as_secs();
102
103 let created_at = self
104 .entries
105 .get(key)
106 .map_or(now, |existing| existing.created_at);
107
108 self.entries.insert(
109 key.to_owned(),
110 CacheEntry {
111 file_name,
112 created_at,
113 modified_at: now,
114 size_bytes,
115 max_age_secs: max_age.map(|d| d.as_secs()),
116 },
117 );
118
119 Ok(())
120 }
121
122 pub fn remove(&mut self, config: &CacheConfig, key: &str) -> Result<(), CacheError> {
125 if let Some(entry) = self.entries.remove(key) {
126 let fs_path = config.file_path(&entry.file_name);
127 match std::fs::remove_file(&fs_path) {
128 Ok(()) => {}
129 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
130 Err(e) => return Err(CacheError::Io(e)),
131 }
132 }
133 Ok(())
134 }
135
136 pub fn contains(&self, key: &str) -> bool {
138 self.entries.contains_key(key)
139 }
140
141 pub fn get(&self, key: &str) -> Option<&CacheEntry> {
143 self.entries.get(key)
144 }
145
146 pub fn asset_path(&self, key: &str) -> Option<String> {
149 self.entries
150 .get(key)
151 .map(|entry| format!("cache://{}", entry.file_name))
152 }
153
154 pub fn is_cached(&self, config: &CacheConfig, key: &str) -> bool {
158 match self.entries.get(key) {
159 Some(entry) => config.file_path(&entry.file_name).exists(),
160 None => false,
161 }
162 }
163
164 pub fn total_size_bytes(&self) -> u64 {
168 self.entries.values().map(|e| e.size_bytes).sum()
169 }
170
171 pub fn load_cached<A: Asset>(
190 &self,
191 config: &CacheConfig,
192 key: &str,
193 asset_server: &AssetServer,
194 ) -> Result<Handle<A>, CacheError> {
195 let entry = self.entries.get(key).ok_or_else(|| {
196 CacheError::NotFound(format!("no cache entry for key '{key}'"))
197 })?;
198 if !config.file_path(&entry.file_name).exists() {
199 return Err(CacheError::NotFound(format!(
200 "cache file '{}' missing from disk for key '{key}'",
201 entry.file_name
202 )));
203 }
204 let path = format!("cache://{}", entry.file_name);
205 Ok(asset_server.load(path))
206 }
207
208 pub fn remove_expired(&mut self, config: &CacheConfig) -> usize {
218 let now = SystemTime::now()
219 .duration_since(UNIX_EPOCH)
220 .unwrap_or_default()
221 .as_secs();
222
223 let global_max = config.max_age.as_secs();
224 let expired_keys: Vec<String> = self
225 .entries
226 .iter()
227 .filter(|(_, entry)| {
228 let effective_max = match entry.max_age_secs {
229 Some(per_entry) => per_entry.max(global_max),
230 None => global_max,
231 };
232 now.saturating_sub(entry.modified_at) > effective_max
233 })
234 .map(|(key, _)| key.clone())
235 .collect();
236
237 let count = expired_keys.len();
238 for key in &expired_keys {
239 if let Some(entry) = self.entries.remove(key) {
240 let fs_path = config.file_path(&entry.file_name);
241 let _ = std::fs::remove_file(&fs_path);
242 }
243 }
244 count
245 }
246
247 pub fn enforce_max_entries(&mut self, config: &CacheConfig) -> usize {
251 let max = match config.max_entries {
252 Some(m) => m,
253 None => return 0,
254 };
255
256 if self.entries.len() <= max {
257 return 0;
258 }
259
260 let to_remove_count = self.entries.len() - max;
261
262 let mut by_age: Vec<(String, u64)> = self
263 .entries
264 .iter()
265 .map(|(k, e)| (k.clone(), e.modified_at))
266 .collect();
267 by_age.sort_by_key(|(_, ts)| *ts);
268
269 let mut removed = 0;
270 for (key, _) in by_age.into_iter().take(to_remove_count) {
271 if let Some(entry) = self.entries.remove(&key) {
272 let fs_path = config.file_path(&entry.file_name);
273 let _ = std::fs::remove_file(&fs_path);
274 removed += 1;
275 }
276 }
277 removed
278 }
279}