Skip to main content

zerolaunch_plugin_api/services/icon/
icon_cache.rs

1use crate::common::dir_utils::DirUtils;
2use dashmap::{DashMap, DashSet};
3use parking_lot::RwLock;
4use std::path::Path;
5use tracing::warn;
6
7/// 图标缓存服务,提供 L1 内存缓存 + L2 文件缓存的原子操作原语。
8/// 本服务是纯缓存工具,不知道图标如何提取,也不知道 IconRequest/CacheLevel 等业务语义。
9/// 业务逻辑由 IconExtractor trait 的默认实现提供,本服务仅提供存取能力。
10pub struct IconCacheService {
11    /// L1: 内存缓存,key 为文件名(如 "abc123.webp"),value 为 WebP 字节数据
12    memory_cache: DashMap<String, Vec<u8>>,
13    /// L2: 文件缓存目录路径
14    cache_dir: RwLock<String>,
15    /// L2: 已缓存的文件名集合(启动时扫描填充,运行时追加)
16    cached_file_hashes: DashSet<String>,
17}
18
19impl IconCacheService {
20    /// 创建图标缓存服务实例。
21    /// 参数:cache_dir - L2 文件缓存目录路径。
22    /// 返回:初始化后的 IconCacheService(尚未扫描 L2 缓存,需调用 init())。
23    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    /// 初始化缓存服务,扫描 L2 文件缓存目录填充 cached_file_hashes。
32    /// 参数:无。
33    /// 返回:无。
34    /// 特性:必须在 new() 之后、首次缓存操作之前调用。
35    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    /// 预载 L2 文件缓存到 L1 内存。
44    /// 参数:无。
45    /// 返回:无。
46    /// 特性:并行读取,全部完成后返回;失败条目静默跳过(查询路径会回退到按需读 L2)。
47    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    // ===== L1 内存缓存操作 =====
81
82    /// 从 L1 内存缓存获取数据。
83    /// 参数:key - 缓存键(文件名,如 "abc123.webp")。
84    /// 返回:命中返回 Some(数据),未命中返回 None。
85    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    /// 写入 L1 内存缓存。
92    /// 参数:key - 缓存键;data - 图标 WebP 数据。
93    /// 返回:无。
94    pub fn set_l1(&self, key: &str, data: Vec<u8>) {
95        self.memory_cache.insert(key.to_string(), data);
96    }
97
98    /// 清空 L1 内存缓存。
99    /// 参数:无。
100    /// 返回:无。
101    pub fn clear_l1(&self) {
102        self.memory_cache.clear();
103    }
104
105    /// 移除 L1 内存缓存中的指定键(用于图标覆盖后强制刷新缓存)。
106    /// 参数:key - 缓存键(文件名,如 "abc123.webp")。
107    /// 返回:无。
108    pub fn remove_l1(&self, key: &str) {
109        self.memory_cache.remove(key);
110    }
111
112    // ===== L2 文件缓存操作 =====
113
114    /// 检查 L2 文件缓存中是否包含指定键。
115    /// 参数:key - 缓存键(文件名)。
116    /// 返回:存在返回 true。
117    pub fn contains_l2(&self, key: &str) -> bool {
118        self.cached_file_hashes.contains(key)
119    }
120
121    /// 从 L2 文件缓存读取数据。
122    /// 参数:key - 缓存键(文件名)。
123    /// 返回:成功返回 Some(数据),失败或不存在返回 None。
124    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    /// 异步写入 L2 文件缓存。
131    /// 参数:key - 缓存键(文件名);data - 图标 WebP 数据。
132    /// 返回:无。
133    /// 特性:写入操作在后台线程执行,不阻塞调用方;同时更新 cached_file_hashes。
134    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    // ===== 管理操作 =====
147
148    /// 更新图标缓存目录路径,同时清空 L1 内存缓存并重新扫描 L2。
149    /// 参数:new_cache_dir - 新的图标文件缓存目录路径。
150    /// 返回:无。
151    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    /// 扫描缓存目录,收集当前格式(.webp)文件名集合。
167    /// 参数:cache_dir - 缓存目录路径。
168    /// 返回:当前格式(.webp)文件名集合。
169    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}