1use crate::exception::CoolCommException;
8use crate::plugin::PluginInfo;
9use serde::{Deserialize, Serialize};
10use std::fs;
11use std::path::{Path, PathBuf};
12use thiserror::Error;
13
14#[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#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct PluginData {
36 pub plugin_json: PluginInfo,
38 pub readme: String,
40 pub logo: String,
42 pub content: String,
44 pub ts_content: String,
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum CheckResult {
53 Incomplete = 0,
55 Exists = 1,
57 HookExists = 2,
59 Ok = 3,
61}
62
63impl CheckResult {
64 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
77pub struct PluginInstaller {
81 plugin_dir: PathBuf,
83}
84
85impl PluginInstaller {
86 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 #[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 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 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 let readme = get_file_content(&plugin_json.readme, "utf-8")
149 .map_err(|_| InstallerError::IncompleteInfo("readme 文件不存在".to_string()))?;
150
151 let logo = get_file_content(&plugin_json.logo, "base64")
153 .map_err(|_| InstallerError::IncompleteInfo("logo 文件不存在".to_string()))?;
154
155 let content = get_file_content("src/index.js", "utf-8")
157 .map_err(|_| InstallerError::IncompleteInfo("src/index.js 文件不存在".to_string()))?;
158
159 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 #[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 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 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 if key == "plugin" {
215 return Err(InstallerError::Common(CoolCommException::new(
216 "插件key不能为plugin,请更换其他key",
217 )));
218 }
219
220 if let Some((is_hook, is_enabled)) = existing_plugins.get(key) {
222 if !is_hook {
223 return Ok((CheckResult::Exists, None));
225 } else if *is_enabled {
226 return Ok((CheckResult::HookExists, None));
228 }
229 }
230
231 Ok((CheckResult::Ok, None))
232 }
233
234 pub fn save_plugin_data(&self, key_name: &str, data: &PluginData) -> InstallerResult<()> {
243 let file_path = self.get_plugin_path(key_name);
244
245 if let Some(parent) = file_path.parent() {
247 fs::create_dir_all(parent)?;
248 }
249
250 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 fs::write(&file_path, serde_json::to_string_pretty(&save_data)?)?;
264
265 Ok(())
266 }
267
268 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 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 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}