Skip to main content

cool_plugin/
installer.rs

1//! 插件安装器
2//!
3//! 对应 TypeScript 版本的 `service/info.ts` 中的 `install`, `data`, `check` 等方法
4//!
5//! 提供插件 ZIP 包解析、安装、卸载等功能
6
7use crate::exception::CoolCommException;
8use crate::plugin::PluginInfo;
9use serde::{Deserialize, Serialize};
10use std::fs;
11use std::path::{Path, PathBuf};
12use thiserror::Error;
13
14/// 插件安装错误
15#[derive(Error, Debug)]
16pub enum InstallerError {
17    #[error("插件信息不完整: {0}")]
18    IncompleteInfo(String),
19    #[error("IO 错误: {0}")]
20    Io(#[from] std::io::Error),
21    #[error("ZIP 错误: {0}")]
22    Zip(String),
23    #[error("序列化错误: {0}")]
24    Serialization(#[from] serde_json::Error),
25    #[error("通用错误: {0}")]
26    Common(#[from] CoolCommException),
27}
28
29pub type InstallerResult<T> = Result<T, InstallerError>;
30
31/// 插件数据
32///
33/// 对应 TypeScript 版本的 `data()` 方法返回的数据结构
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct PluginData {
36    /// 插件 JSON 信息
37    pub plugin_json: PluginInfo,
38    /// README 内容
39    pub readme: String,
40    /// Logo(Base64 编码)
41    pub logo: String,
42    /// 插件内容(JavaScript)
43    pub content: String,
44    /// TypeScript 内容
45    pub ts_content: String,
46}
47
48/// 插件检查结果
49///
50/// 对应 TypeScript 版本的 `check()` 方法返回的结果
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum CheckResult {
53    /// 类型 0:插件信息不完整
54    Incomplete = 0,
55    /// 类型 1:插件已存在,继续安装将覆盖
56    Exists = 1,
57    /// 类型 2:已存在同名 Hook 插件,多个相同的 Hook 插件只能同时开启一个
58    HookExists = 2,
59    /// 类型 3:检查通过
60    Ok = 3,
61}
62
63impl CheckResult {
64    /// 获取检查结果消息
65    pub fn message(&self) -> &'static str {
66        match self {
67            CheckResult::Incomplete => "插件信息不完整",
68            CheckResult::Exists => "插件已存在,继续安装将覆盖",
69            CheckResult::HookExists => {
70                "已存在同名Hook插件,你可以继续安装,但是多个相同的Hook插件只能同时开启一个"
71            }
72            CheckResult::Ok => "检查通过",
73        }
74    }
75}
76
77/// 插件安装器
78///
79/// 提供插件安装、卸载、检查等功能
80pub struct PluginInstaller {
81    /// 插件存储目录
82    plugin_dir: PathBuf,
83}
84
85impl PluginInstaller {
86    /// 创建新的插件安装器
87    ///
88    /// # 参数
89    ///
90    /// * `plugin_dir` - 插件存储目录路径
91    pub fn new(plugin_dir: impl AsRef<Path>) -> Self {
92        Self {
93            plugin_dir: plugin_dir.as_ref().to_path_buf(),
94        }
95    }
96
97    /// 从 ZIP 文件提取插件数据
98    ///
99    /// 对应 TypeScript 版本的 `data()` 方法
100    ///
101    /// # 参数
102    ///
103    /// * `zip_path` - ZIP 文件路径
104    ///
105    /// # 返回
106    ///
107    /// 返回插件数据,包括 plugin.json、readme、logo、content、tsContent
108    ///
109    /// # 注意
110    ///
111    /// 此方法需要 `zip` feature 启用,否则会返回错误
112    #[cfg(feature = "zip")]
113    pub fn extract_plugin_data(&self, zip_path: impl AsRef<Path>) -> InstallerResult<PluginData> {
114        use std::fs::File;
115        use std::io::Read;
116        use zip::ZipArchive;
117
118        let file = File::open(zip_path.as_ref())?;
119        let mut archive = ZipArchive::new(file)
120            .map_err(|e| InstallerError::Zip(format!("无法打开 ZIP 文件: {}", e)))?;
121
122        // 辅助函数:从 ZIP 中读取文件内容
123        let mut get_file_content = |entry_name: &str, encoding: &str| -> InstallerResult<String> {
124            let mut file = archive.by_name(entry_name).map_err(|_| {
125                InstallerError::IncompleteInfo(format!("文件 {} 不存在", entry_name))
126            })?;
127
128            let mut buffer = Vec::new();
129            file.read_to_end(&mut buffer)
130                .map_err(|e| InstallerError::Io(e))?;
131
132            match encoding {
133                "base64" => {
134                    use base64::Engine;
135                    Ok(base64::engine::general_purpose::STANDARD.encode(&buffer))
136                }
137                "utf-8" | _ => String::from_utf8(buffer)
138                    .map_err(|e| InstallerError::IncompleteInfo(format!("UTF-8 解码失败: {}", e))),
139            }
140        };
141
142        // 读取 plugin.json
143        let plugin_json_str = get_file_content("plugin.json", "utf-8")?;
144        let plugin_json: PluginInfo = serde_json::from_str(&plugin_json_str)
145            .map_err(|e| InstallerError::IncompleteInfo(format!("plugin.json 解析失败: {}", e)))?;
146
147        // 读取 readme
148        let readme = get_file_content(&plugin_json.readme, "utf-8")
149            .map_err(|_| InstallerError::IncompleteInfo("readme 文件不存在".to_string()))?;
150
151        // 读取 logo(Base64 编码)
152        let logo = get_file_content(&plugin_json.logo, "base64")
153            .map_err(|_| InstallerError::IncompleteInfo("logo 文件不存在".to_string()))?;
154
155        // 读取 content(JavaScript)
156        let content = get_file_content("src/index.js", "utf-8")
157            .map_err(|_| InstallerError::IncompleteInfo("src/index.js 文件不存在".to_string()))?;
158
159        // 读取 tsContent(TypeScript)
160        let ts_content = get_file_content("source/index.ts", "utf-8").map_err(|_| {
161            InstallerError::IncompleteInfo("source/index.ts 文件不存在".to_string())
162        })?;
163
164        Ok(PluginData {
165            plugin_json,
166            readme,
167            logo,
168            content,
169            ts_content,
170        })
171    }
172
173    /// 从 ZIP 文件提取插件数据(无 zip feature 版本)
174    ///
175    /// 当未启用 `zip` feature 时,此方法会返回错误
176    #[cfg(not(feature = "zip"))]
177    pub fn extract_plugin_data(&self, _zip_path: impl AsRef<Path>) -> InstallerResult<PluginData> {
178        Err(InstallerError::Zip(
179            "ZIP 功能未启用,请在 Cargo.toml 中添加 zip 依赖并启用 zip feature".to_string(),
180        ))
181    }
182
183    /// 检查插件
184    ///
185    /// 对应 TypeScript 版本的 `check()` 方法
186    ///
187    /// # 参数
188    ///
189    /// * `zip_path` - ZIP 文件路径
190    /// * `existing_plugins` - 已存在的插件列表(key -> (is_hook, is_enabled))
191    ///
192    /// # 返回
193    ///
194    /// 返回检查结果
195    pub fn check_plugin(
196        &self,
197        zip_path: impl AsRef<Path>,
198        existing_plugins: &std::collections::HashMap<String, (bool, bool)>,
199    ) -> InstallerResult<(CheckResult, Option<String>)> {
200        // 尝试提取插件数据
201        let data = match self.extract_plugin_data(&zip_path) {
202            Ok(d) => d,
203            Err(e) => {
204                return Ok((
205                    CheckResult::Incomplete,
206                    Some(format!("插件信息不完整: {}", e)),
207                ));
208            }
209        };
210
211        let key = &data.plugin_json.key;
212
213        // 检查插件 key 不能为 "plugin"
214        if key == "plugin" {
215            return Err(InstallerError::Common(CoolCommException::new(
216                "插件key不能为plugin,请更换其他key",
217            )));
218        }
219
220        // 检查是否已存在
221        if let Some((is_hook, is_enabled)) = existing_plugins.get(key) {
222            if !is_hook {
223                // 非 Hook 插件已存在
224                return Ok((CheckResult::Exists, None));
225            } else if *is_enabled {
226                // Hook 插件已存在且已启用
227                return Ok((CheckResult::HookExists, None));
228            }
229        }
230
231        Ok((CheckResult::Ok, None))
232    }
233
234    /// 保存插件数据到文件
235    ///
236    /// 对应 TypeScript 版本的 `saveData()` 方法
237    ///
238    /// # 参数
239    ///
240    /// * `key_name` - 插件 key
241    /// * `data` - 插件数据
242    pub fn save_plugin_data(&self, key_name: &str, data: &PluginData) -> InstallerResult<()> {
243        let file_path = self.get_plugin_path(key_name);
244
245        // 确保目录存在
246        if let Some(parent) = file_path.parent() {
247            fs::create_dir_all(parent)?;
248        }
249
250        // 构建保存的数据结构
251        let save_data = serde_json::json!({
252            "content": {
253                "type": "comm",
254                "data": data.content
255            },
256            "tsContent": {
257                "type": "ts",
258                "data": data.ts_content
259            }
260        });
261
262        // 写入文件
263        fs::write(&file_path, serde_json::to_string_pretty(&save_data)?)?;
264
265        Ok(())
266    }
267
268    /// 获取插件数据
269    ///
270    /// 对应 TypeScript 版本的 `getData()` 方法
271    ///
272    /// # 参数
273    ///
274    /// * `key_name` - 插件 key
275    ///
276    /// # 返回
277    ///
278    /// 返回插件数据(content 和 tsContent)
279    pub fn get_plugin_data(&self, key_name: &str) -> InstallerResult<Option<serde_json::Value>> {
280        let file_path = self.get_plugin_path(key_name);
281
282        if !file_path.exists() {
283            return Ok(None);
284        }
285
286        let content = fs::read_to_string(&file_path)?;
287        let data: serde_json::Value = serde_json::from_str(&content)?;
288
289        Ok(Some(data))
290    }
291
292    /// 删除插件数据
293    ///
294    /// 对应 TypeScript 版本的 `deleteData()` 方法
295    ///
296    /// * `key_name` - 插件 key
297    pub fn delete_plugin_data(&self, key_name: &str) -> InstallerResult<()> {
298        let file_path = self.get_plugin_path(key_name);
299
300        if file_path.exists() {
301            fs::remove_file(&file_path)?;
302        }
303
304        Ok(())
305    }
306
307    /// 获取插件文件路径
308    ///
309    /// 对应 TypeScript 版本的 `pluginPath()` 方法
310    fn get_plugin_path(&self, key_name: &str) -> PathBuf {
311        self.plugin_dir.join(key_name)
312    }
313}
314
315impl Default for PluginInstaller {
316    fn default() -> Self {
317        Self::new("plugins")
318    }
319}
320
321#[cfg(test)]
322mod tests {
323    use super::*;
324
325    #[test]
326    fn test_check_result() {
327        assert_eq!(CheckResult::Ok.message(), "检查通过");
328        assert_eq!(CheckResult::Exists.message(), "插件已存在,继续安装将覆盖");
329    }
330}