zerolaunch_plugin_api/services/icon/
icon_cache.rs1use crate::common::dir_utils::DirUtils;
2use dashmap::{DashMap, DashSet};
3use parking_lot::RwLock;
4use std::path::Path;
5use tracing::warn;
6
7const FAILURES_FILE: &str = "failures.json";
9
10pub struct IconCacheService {
14 memory_cache: DashMap<String, Vec<u8>>,
16 cache_dir: RwLock<String>,
18 cached_file_hashes: DashSet<String>,
20 failures: DashSet<String>,
23}
24
25impl IconCacheService {
26 pub fn new(cache_dir: String) -> Self {
30 Self {
31 memory_cache: DashMap::new(),
32 cache_dir: RwLock::new(cache_dir),
33 cached_file_hashes: DashSet::new(),
34 failures: DashSet::new(),
35 }
36 }
37
38 pub fn init(&self) {
43 let cache_dir = self.cache_dir.read().clone();
44 let hashes = Self::scan_cached_icons(&cache_dir);
45 for hash in hashes {
46 self.cached_file_hashes.insert(hash);
47 }
48 self.load_failures();
49 }
50
51 pub async fn preload_l1(&self) {
56 let cache_dir = self.cache_dir.read().clone();
57 let keys: Vec<String> = self
58 .cached_file_hashes
59 .iter()
60 .map(|entry| entry.clone())
61 .collect();
62 let mut handles = Vec::with_capacity(keys.len());
63 for key in keys {
64 let dir = cache_dir.clone();
65 let key_clone = key.clone();
66 handles.push(tokio::spawn(async move {
67 let data = tokio::fs::read(Path::new(&dir).join(&key_clone)).await.ok();
68 (key, data)
69 }));
70 }
71 for handle in handles {
72 if let Ok((key, Some(data))) = handle.await {
73 self.memory_cache.insert(key, data);
74 }
75 }
76 let (count, bytes): (usize, usize) = self
77 .memory_cache
78 .iter()
79 .map(|e| (1, e.value().len()))
80 .fold((0, 0), |(c, b), (cc, bb)| (c + cc, b + bb));
81 tracing::info!(
82 "图标缓存预载完成: {} 个, {:.1} KB",
83 count,
84 bytes as f64 / 1024.0
85 );
86 }
87
88 pub fn get_l1(&self, key: &str) -> Option<Vec<u8>> {
94 self.memory_cache
95 .get(key)
96 .map(|entry| entry.value().clone())
97 }
98
99 pub fn set_l1(&self, key: &str, data: Vec<u8>) {
103 self.memory_cache.insert(key.to_string(), data);
104 }
105
106 pub fn clear_l1(&self) {
110 self.memory_cache.clear();
111 }
112
113 pub fn remove_l1(&self, key: &str) {
117 self.memory_cache.remove(key);
118 }
119
120 pub fn contains_l2(&self, key: &str) -> bool {
126 self.cached_file_hashes.contains(key)
127 }
128
129 fn load_failures(&self) {
136 self.failures.clear();
137 let path = Path::new(&*self.cache_dir.read()).join(FAILURES_FILE);
138 let bytes = match std::fs::read(&path) {
139 Ok(bytes) => bytes,
140 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return,
141 Err(e) => {
142 warn!("读取图标失败记录失败: {}", e);
143 return;
144 }
145 };
146 match serde_json::from_slice::<Vec<String>>(&bytes) {
147 Ok(keys) => {
148 for key in keys {
149 self.failures.insert(key);
150 }
151 }
152 Err(e) => warn!("解析图标失败记录失败: {}", e),
153 }
154 }
155
156 fn save_failures(&self) {
160 let path = Path::new(&*self.cache_dir.read()).join(FAILURES_FILE);
161 let mut keys: Vec<String> = self.failures.iter().map(|entry| entry.clone()).collect();
162 keys.sort();
163 match serde_json::to_vec(&keys) {
164 Ok(bytes) => {
165 if let Err(e) = std::fs::write(&path, bytes) {
166 warn!("写入图标失败记录失败: {}", e);
167 }
168 }
169 Err(e) => warn!("序列化图标失败记录失败: {}", e),
170 }
171 }
172
173 pub fn should_skip(&self, key: &str) -> bool {
177 self.failures.contains(key)
178 }
179
180 pub fn mark_failed(&self, key: &str) {
184 if self.failures.insert(key.to_string()) {
185 self.save_failures();
186 }
187 }
188
189 pub fn clear_failed(&self, key: &str) {
193 if self.failures.remove(key).is_some() {
194 self.save_failures();
195 }
196 }
197
198 pub fn clear_all_failed(&self) {
202 if self.failures.is_empty() {
203 return;
204 }
205 self.failures.clear();
206 self.save_failures();
207 }
208
209 pub async fn get_l2(&self, key: &str) -> Option<Vec<u8>> {
213 let cache_dir = self.cache_dir.read().clone();
214 let icon_path = Path::new(&cache_dir).join(key);
215 tokio::fs::read(&icon_path).await.ok()
216 }
217
218 pub async fn set_l2(&self, key: &str, data: Vec<u8>) {
223 let cache_dir = self.cache_dir.read().clone();
224 let icon_path = Path::new(&cache_dir).join(key);
225 let key_owned = key.to_string();
226
227 if let Err(e) = tokio::fs::write(&icon_path, data).await {
228 warn!("写入图标缓存失败 {:?}: {}", icon_path, e);
229 } else {
230 self.cached_file_hashes.insert(key_owned);
231 }
232 }
233
234 pub fn update_cache_dir(&self, new_cache_dir: &str) {
240 self.memory_cache.clear();
241
242 {
243 let mut cache_dir = self.cache_dir.write();
244 *cache_dir = new_cache_dir.to_string();
245 }
246
247 self.cached_file_hashes.clear();
248 let hashes = Self::scan_cached_icons(new_cache_dir);
249 for hash in hashes {
250 self.cached_file_hashes.insert(hash);
251 }
252 self.load_failures();
253 }
254
255 fn scan_cached_icons(cache_dir: &str) -> DashSet<String> {
259 let result = DashSet::new();
260 match DirUtils::read_dir_or_create(cache_dir) {
261 Ok(entries) => {
262 for entry in entries.flatten() {
263 let file_name = entry.file_name();
264 let file_name = file_name.to_string_lossy();
265 if !file_name.ends_with(".webp") {
266 continue;
267 }
268 result.insert(file_name.into_owned());
269 }
270 }
271 Err(e) => warn!("扫描图标缓存目录失败: {}", e),
272 }
273 result
274 }
275}