feat: 目录结构迁移优化
- 将 MirrorRegistry MirrorTarget 迁移到 model.rs - TargetConfig 独立实现 FromLua,解耦反序列化与业务校验 - mirror—shim 新增 command.rs 并将 to_command 移动到 command.rs - 其他优化
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
use inquire::Select;
|
||||
use std::fs::{self, File};
|
||||
use std::io::{self, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
// 引入你的 mirror-core 类型
|
||||
use mirror_core::{MirrorRegistry, TargetConfig};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum UseError {
|
||||
ToolNotFound(String),
|
||||
NotVersioned(String),
|
||||
HomeDirNotFound(PathBuf),
|
||||
NoVersionsAvailable(PathBuf),
|
||||
UserCancelled,
|
||||
Io(io::Error),
|
||||
TargetValidationFailed(PathBuf),
|
||||
}
|
||||
|
||||
/// 执行版本切换主流程
|
||||
pub fn switch_version(
|
||||
registry: &MirrorRegistry,
|
||||
tool_name: &str,
|
||||
target_version: Option<&str>,
|
||||
) -> Result<(), UseError> {
|
||||
// 1. 获取配置并断言必须是多版本工具
|
||||
let spec = registry
|
||||
.targets
|
||||
.get(tool_name)
|
||||
.ok_or_else(|| UseError::ToolNotFound(tool_name.to_string()))?;
|
||||
|
||||
let (home, version_file) = match &spec.target {
|
||||
TargetConfig::Versioned {
|
||||
home, version_file, ..
|
||||
} => {
|
||||
let v_file = version_file.as_deref().unwrap_or(".version");
|
||||
(home, v_file)
|
||||
}
|
||||
TargetConfig::Static(_) => {
|
||||
return Err(UseError::NotVersioned(tool_name.to_string()));
|
||||
}
|
||||
};
|
||||
|
||||
if !home.exists() {
|
||||
return Err(UseError::HomeDirNotFound(home.clone()));
|
||||
}
|
||||
|
||||
// 2. 扫描候选版本目录
|
||||
let available_versions = scan_versions(home)?;
|
||||
if available_versions.is_empty() {
|
||||
return Err(UseError::NoVersionsAvailable(home.clone()));
|
||||
}
|
||||
|
||||
// 3. 确定目标版本(命令行传入 OR 唤起 Inquire 单选菜单)
|
||||
let selected_ver = match target_version {
|
||||
Some(ver) => {
|
||||
// 如果用户传了参,校验是否在候选集中(防呆)
|
||||
if !available_versions.iter().any(|v| v == ver) {
|
||||
eprintln!(
|
||||
"警告: 目录 {:?} 下未发现版本 '{}',仍将强制写入。",
|
||||
home, ver
|
||||
);
|
||||
}
|
||||
ver.to_string()
|
||||
}
|
||||
None => {
|
||||
// 尝试读取当前已激活的版本,作为 Inquire 的默认初始高亮项
|
||||
let current_version_path = home.join(version_file);
|
||||
let current_ver = fs::read_to_string(¤t_version_path)
|
||||
.map(|s| s.trim().to_string())
|
||||
.ok();
|
||||
|
||||
prompt_user_select(&available_versions, current_ver.as_deref())?
|
||||
}
|
||||
};
|
||||
|
||||
// 4. 原子化持久化写入版本文件
|
||||
let dest_version_path = home.join(version_file);
|
||||
atomic_write(&dest_version_path, &selected_ver).map_err(UseError::Io)?;
|
||||
|
||||
println!("✔ 已成功将 '{}' 切换至版本: {}", tool_name, selected_ver);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 扫描 home 目录下的非隐藏有效子文件夹作为版本列表
|
||||
fn scan_versions(home: &Path) -> Result<Vec<String>, UseError> {
|
||||
let mut versions = Vec::new();
|
||||
|
||||
let entries = fs::read_dir(home).map_err(UseError::Io)?;
|
||||
for entry in entries.filter_map(|e| e.ok()) {
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
if let Some(dir_name) = path.file_name().and_then(|n| n.to_str()) {
|
||||
// 排除以点开头的隐藏目录(如 .git, .cache)
|
||||
if !dir_name.starts_with('.') {
|
||||
versions.push(dir_name.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 自然排序版本列表
|
||||
versions.sort();
|
||||
Ok(versions)
|
||||
}
|
||||
|
||||
/// 唤起 Inquire 进行交互单选
|
||||
fn prompt_user_select(
|
||||
versions: &[String],
|
||||
default_version: Option<&str>,
|
||||
) -> Result<String, UseError> {
|
||||
let mut prompt = Select::new("请选择要激活的目标版本:", versions.to_vec());
|
||||
|
||||
// 如果能读取到当前版本,默认选中该项
|
||||
if let Some(def) = default_version {
|
||||
if let Some(idx) = versions.iter().position(|v| v == def) {
|
||||
prompt.starting_cursor = idx;
|
||||
}
|
||||
}
|
||||
|
||||
prompt.prompt().map_err(|_| UseError::UserCancelled)
|
||||
}
|
||||
|
||||
/// 使用临时文件同目录重命名实现“原子写入”
|
||||
///
|
||||
/// 杜绝中途掉电、杀死进程导致的空文件损坏
|
||||
fn atomic_write(dest_path: &Path, content: &str) -> io::Result<()> {
|
||||
let parent_dir = dest_path
|
||||
.parent()
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Parent dir not found"))?;
|
||||
|
||||
// 必须在目标文件的同级目录下创建临时文件,跨盘符/分区会导致原子重命名 (Rename) 失败
|
||||
let mut temp_file = NamedTempFile::new_in(parent_dir)?;
|
||||
|
||||
// 写入纯净内容,并强制落盘刷新 (Flush)
|
||||
temp_file.write_all(content.trim().as_bytes())?;
|
||||
temp_file.write_all(b"\n")?; // 末尾留标准换行符
|
||||
temp_file.as_file().sync_all()?;
|
||||
|
||||
// 核心操作:原子覆盖(在 Windows 上内部会调用 ReplaceFileW,在 Unix 则是 rename)
|
||||
temp_file.persist(dest_path).map_err(|e| e.error)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user