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 std::time::{Duration, Instant};
6use tracing::warn;
7
8const ICON_FAILURE_TTL: Duration = Duration::from_secs(600);
10
11pub struct IconCacheService {
15 memory_cache: DashMap<String, Vec<u8>>,
17 cache_dir: RwLock<String>,
19 cached_file_hashes: DashSet<String>,
21 failed_at: DashMap<String, Instant>,
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 failed_at: DashMap::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 }
49
50 pub async fn preload_l1(&self) {
55 let cache_dir = self.cache_dir.read().clone();
56 let keys: Vec<String> = self
57 .cached_file_hashes
58 .iter()
59 .map(|entry| entry.clone())
60 .collect();
61 let mut handles = Vec::with_capacity(keys.len());
62 for key in keys {
63 let dir = cache_dir.clone();
64 let key_clone = key.clone();
65 handles.push(tokio::spawn(async move {
66 let data = tokio::fs::read(Path::new(&dir).join(&key_clone)).await.ok();
67 (key, data)
68 }));
69 }
70 for handle in handles {
71 if let Ok((key, Some(data))) = handle.await {
72 self.memory_cache.insert(key, data);
73 }
74 }
75 let (count, bytes): (usize, usize) = self
76 .memory_cache
77 .iter()
78 .map(|e| (1, e.value().len()))
79 .fold((0, 0), |(c, b), (cc, bb)| (c + cc, b + bb));
80 tracing::info!(
81 "图标缓存预载完成: {} 个, {:.1} KB",
82 count,
83 bytes as f64 / 1024.0
84 );
85 }
86
87 pub fn get_l1(&self, key: &str) -> Option<Vec<u8>> {
93 self.memory_cache
94 .get(key)
95 .map(|entry| entry.value().clone())
96 }
97
98 pub fn set_l1(&self, key: &str, data: Vec<u8>) {
102 self.memory_cache.insert(key.to_string(), data);
103 }
104
105 pub fn clear_l1(&self) {
109 self.memory_cache.clear();
110 }
111
112 pub fn remove_l1(&self, key: &str) {
116 self.memory_cache.remove(key);
117 }
118
119 pub fn contains_l2(&self, key: &str) -> bool {
125 self.cached_file_hashes.contains(key)
126 }
127
128 pub fn mark_failed(&self, key: &str) {
133 self.failed_at.insert(key.to_string(), Instant::now());
134 }
135
136 pub fn is_recently_failed(&self, key: &str) -> bool {
140 let Some(entry) = self.failed_at.get(key) else {
141 return false;
142 };
143 let elapsed = entry.elapsed();
144 drop(entry);
145 if elapsed < ICON_FAILURE_TTL {
146 return true;
147 }
148 self.failed_at.remove(key);
149 false
150 }
151
152 pub fn clear_failed(&self, key: &str) {
155 self.failed_at.remove(key);
156 }
157
158 pub async fn get_l2(&self, key: &str) -> Option<Vec<u8>> {
162 let cache_dir = self.cache_dir.read().clone();
163 let icon_path = Path::new(&cache_dir).join(key);
164 tokio::fs::read(&icon_path).await.ok()
165 }
166
167 pub async fn set_l2(&self, key: &str, data: Vec<u8>) {
172 let cache_dir = self.cache_dir.read().clone();
173 let icon_path = Path::new(&cache_dir).join(key);
174 let key_owned = key.to_string();
175
176 if let Err(e) = tokio::fs::write(&icon_path, data).await {
177 warn!("写入图标缓存失败 {:?}: {}", icon_path, e);
178 } else {
179 self.cached_file_hashes.insert(key_owned);
180 }
181 }
182
183 pub fn update_cache_dir(&self, new_cache_dir: &str) {
189 self.memory_cache.clear();
190
191 {
192 let mut cache_dir = self.cache_dir.write();
193 *cache_dir = new_cache_dir.to_string();
194 }
195
196 self.cached_file_hashes.clear();
197 self.failed_at.clear();
198 let hashes = Self::scan_cached_icons(new_cache_dir);
199 for hash in hashes {
200 self.cached_file_hashes.insert(hash);
201 }
202 }
203
204 fn scan_cached_icons(cache_dir: &str) -> DashSet<String> {
208 let result = DashSet::new();
209 match DirUtils::read_dir_or_create(cache_dir) {
210 Ok(entries) => {
211 for entry in entries.flatten() {
212 let file_name = entry.file_name();
213 let file_name = file_name.to_string_lossy();
214 if !file_name.ends_with(".webp") {
215 continue;
216 }
217 result.insert(file_name.into_owned());
218 }
219 }
220 Err(e) => warn!("扫描图标缓存目录失败: {}", e),
221 }
222 result
223 }
224}