use crate::exception::CoolCommException;
use crate::plugin::PluginInfo;
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::{Path, PathBuf};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum InstallerError {
#[error("插件信息不完整: {0}")]
IncompleteInfo(String),
#[error("IO 错误: {0}")]
Io(#[from] std::io::Error),
#[error("ZIP 错误: {0}")]
Zip(String),
#[error("序列化错误: {0}")]
Serialization(#[from] serde_json::Error),
#[error("通用错误: {0}")]
Common(#[from] CoolCommException),
}
pub type InstallerResult<T> = Result<T, InstallerError>;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginData {
pub plugin_json: PluginInfo,
pub readme: String,
pub logo: String,
pub content: String,
pub ts_content: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CheckResult {
Incomplete = 0,
Exists = 1,
HookExists = 2,
Ok = 3,
}
impl CheckResult {
pub fn message(&self) -> &'static str {
match self {
CheckResult::Incomplete => "插件信息不完整",
CheckResult::Exists => "插件已存在,继续安装将覆盖",
CheckResult::HookExists => {
"已存在同名Hook插件,你可以继续安装,但是多个相同的Hook插件只能同时开启一个"
}
CheckResult::Ok => "检查通过",
}
}
}
pub struct PluginInstaller {
plugin_dir: PathBuf,
}
impl PluginInstaller {
pub fn new(plugin_dir: impl AsRef<Path>) -> Self {
Self {
plugin_dir: plugin_dir.as_ref().to_path_buf(),
}
}
#[cfg(feature = "zip")]
pub fn extract_plugin_data(&self, zip_path: impl AsRef<Path>) -> InstallerResult<PluginData> {
use std::fs::File;
use std::io::Read;
use zip::ZipArchive;
let file = File::open(zip_path.as_ref())?;
let mut archive = ZipArchive::new(file)
.map_err(|e| InstallerError::Zip(format!("无法打开 ZIP 文件: {}", e)))?;
let mut get_file_content = |entry_name: &str, encoding: &str| -> InstallerResult<String> {
let mut file = archive.by_name(entry_name).map_err(|_| {
InstallerError::IncompleteInfo(format!("文件 {} 不存在", entry_name))
})?;
let mut buffer = Vec::new();
file.read_to_end(&mut buffer)
.map_err(|e| InstallerError::Io(e))?;
match encoding {
"base64" => {
use base64::Engine;
Ok(base64::engine::general_purpose::STANDARD.encode(&buffer))
}
"utf-8" | _ => String::from_utf8(buffer)
.map_err(|e| InstallerError::IncompleteInfo(format!("UTF-8 解码失败: {}", e))),
}
};
let plugin_json_str = get_file_content("plugin.json", "utf-8")?;
let plugin_json: PluginInfo = serde_json::from_str(&plugin_json_str)
.map_err(|e| InstallerError::IncompleteInfo(format!("plugin.json 解析失败: {}", e)))?;
let readme = get_file_content(&plugin_json.readme, "utf-8")
.map_err(|_| InstallerError::IncompleteInfo("readme 文件不存在".to_string()))?;
let logo = get_file_content(&plugin_json.logo, "base64")
.map_err(|_| InstallerError::IncompleteInfo("logo 文件不存在".to_string()))?;
let content = get_file_content("src/index.js", "utf-8")
.map_err(|_| InstallerError::IncompleteInfo("src/index.js 文件不存在".to_string()))?;
let ts_content = get_file_content("source/index.ts", "utf-8").map_err(|_| {
InstallerError::IncompleteInfo("source/index.ts 文件不存在".to_string())
})?;
Ok(PluginData {
plugin_json,
readme,
logo,
content,
ts_content,
})
}
#[cfg(not(feature = "zip"))]
pub fn extract_plugin_data(&self, _zip_path: impl AsRef<Path>) -> InstallerResult<PluginData> {
Err(InstallerError::Zip(
"ZIP 功能未启用,请在 Cargo.toml 中添加 zip 依赖并启用 zip feature".to_string(),
))
}
pub fn check_plugin(
&self,
zip_path: impl AsRef<Path>,
existing_plugins: &std::collections::HashMap<String, (bool, bool)>,
) -> InstallerResult<(CheckResult, Option<String>)> {
let data = match self.extract_plugin_data(&zip_path) {
Ok(d) => d,
Err(e) => {
return Ok((
CheckResult::Incomplete,
Some(format!("插件信息不完整: {}", e)),
));
}
};
let key = &data.plugin_json.key;
if key == "plugin" {
return Err(InstallerError::Common(CoolCommException::new(
"插件key不能为plugin,请更换其他key",
)));
}
if let Some((is_hook, is_enabled)) = existing_plugins.get(key) {
if !is_hook {
return Ok((CheckResult::Exists, None));
} else if *is_enabled {
return Ok((CheckResult::HookExists, None));
}
}
Ok((CheckResult::Ok, None))
}
pub fn save_plugin_data(&self, key_name: &str, data: &PluginData) -> InstallerResult<()> {
let file_path = self.get_plugin_path(key_name);
if let Some(parent) = file_path.parent() {
fs::create_dir_all(parent)?;
}
let save_data = serde_json::json!({
"content": {
"type": "comm",
"data": data.content
},
"tsContent": {
"type": "ts",
"data": data.ts_content
}
});
fs::write(&file_path, serde_json::to_string_pretty(&save_data)?)?;
Ok(())
}
pub fn get_plugin_data(&self, key_name: &str) -> InstallerResult<Option<serde_json::Value>> {
let file_path = self.get_plugin_path(key_name);
if !file_path.exists() {
return Ok(None);
}
let content = fs::read_to_string(&file_path)?;
let data: serde_json::Value = serde_json::from_str(&content)?;
Ok(Some(data))
}
pub fn delete_plugin_data(&self, key_name: &str) -> InstallerResult<()> {
let file_path = self.get_plugin_path(key_name);
if file_path.exists() {
fs::remove_file(&file_path)?;
}
Ok(())
}
fn get_plugin_path(&self, key_name: &str) -> PathBuf {
self.plugin_dir.join(key_name)
}
}
impl Default for PluginInstaller {
fn default() -> Self {
Self::new("plugins")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_check_result() {
assert_eq!(CheckResult::Ok.message(), "检查通过");
assert_eq!(CheckResult::Exists.message(), "插件已存在,继续安装将覆盖");
}
}