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
7pub struct IconCacheService {
11 memory_cache: DashMap<String, Vec<u8>>,
13 cache_dir: RwLock<String>,
15 cached_file_hashes: DashSet<String>,
17}
18
19impl IconCacheService {
20 pub fn new(cache_dir: String) -> Self {
24 Self {
25 memory_cache: DashMap::new(),
26 cache_dir: RwLock::new(cache_dir),
27 cached_file_hashes: DashSet::new(),
28 }
29 }
30
31 pub fn init(&self) {
36 let cache_dir = self.cache_dir.read().clone();
37 let hashes = Self::scan_cached_icons(&cache_dir);
38 for hash in hashes {
39 self.cached_file_hashes.insert(hash);
40 }
41 }
42
43 pub async fn preload_l1(&self) {
48 let cache_dir = self.cache_dir.read().clone();
49 let keys: Vec<String> = self
50 .cached_file_hashes
51 .iter()
52 .map(|entry| entry.clone())
53 .collect();
54 let mut handles = Vec::with_capacity(keys.len());
55 for key in keys {
56 let dir = cache_dir.clone();
57 let key_clone = key.clone();
58 handles.push(tokio::spawn(async move {
59 let data = tokio::fs::read(Path::new(&dir).join(&key_clone)).await.ok();
60 (key, data)
61 }));
62 }
63 for handle in handles {
64 if let Ok((key, Some(data))) = handle.await {
65 self.memory_cache.insert(key, data);
66 }
67 }
68 let (count, bytes): (usize, usize) = self
69 .memory_cache
70 .iter()
71 .map(|e| (1, e.value().len()))
72 .fold((0, 0), |(c, b), (cc, bb)| (c + cc, b + bb));
73 tracing::info!(
74 "图标缓存预载完成: {} 个, {:.1} KB",
75 count,
76 bytes as f64 / 1024.0
77 );
78 }
79
80 pub fn get_l1(&self, key: &str) -> Option<Vec<u8>> {
86 self.memory_cache
87 .get(key)
88 .map(|entry| entry.value().clone())
89 }
90
91 pub fn set_l1(&self, key: &str, data: Vec<u8>) {
95 self.memory_cache.insert(key.to_string(), data);
96 }
97
98 pub fn clear_l1(&self) {
102 self.memory_cache.clear();
103 }
104
105 pub fn remove_l1(&self, key: &str) {
109 self.memory_cache.remove(key);
110 }
111
112 pub fn contains_l2(&self, key: &str) -> bool {
118 self.cached_file_hashes.contains(key)
119 }
120
121 pub async fn get_l2(&self, key: &str) -> Option<Vec<u8>> {
125 let cache_dir = self.cache_dir.read().clone();
126 let icon_path = Path::new(&cache_dir).join(key);
127 tokio::fs::read(&icon_path).await.ok()
128 }
129
130 pub async fn set_l2(&self, key: &str, data: Vec<u8>) {
135 let cache_dir = self.cache_dir.read().clone();
136 let icon_path = Path::new(&cache_dir).join(key);
137 let key_owned = key.to_string();
138
139 if let Err(e) = tokio::fs::write(&icon_path, data).await {
140 warn!("写入图标缓存失败 {:?}: {}", icon_path, e);
141 } else {
142 self.cached_file_hashes.insert(key_owned);
143 }
144 }
145
146 pub fn update_cache_dir(&self, new_cache_dir: &str) {
152 self.memory_cache.clear();
153
154 {
155 let mut cache_dir = self.cache_dir.write();
156 *cache_dir = new_cache_dir.to_string();
157 }
158
159 self.cached_file_hashes.clear();
160 let hashes = Self::scan_cached_icons(new_cache_dir);
161 for hash in hashes {
162 self.cached_file_hashes.insert(hash);
163 }
164 }
165
166 fn scan_cached_icons(cache_dir: &str) -> DashSet<String> {
170 let result = DashSet::new();
171 match DirUtils::read_dir_or_create(cache_dir) {
172 Ok(entries) => {
173 for entry in entries.flatten() {
174 let file_name = entry.file_name();
175 let file_name = file_name.to_string_lossy();
176 if !file_name.ends_with(".webp") {
177 continue;
178 }
179 result.insert(file_name.into_owned());
180 }
181 }
182 Err(e) => warn!("扫描图标缓存目录失败: {}", e),
183 }
184 result
185 }
186}