feat(fs): atomic writes for JSON and TOML saves\n\n- Introduce atomic_write utility and use it in write_json_file\n- Add write_text_file for TOML/strings and use in Codex paths\n- Reduce risk of partial writes and ensure directory creation
This commit is contained in:
@@ -79,8 +79,7 @@ pub fn save_codex_provider_config(
|
||||
toml::from_str::<toml::Table>(config_str)
|
||||
.map_err(|e| format!("config.toml 格式错误: {}", e))?;
|
||||
}
|
||||
fs::write(&config_path, config_str)
|
||||
.map_err(|e| format!("写入供应商 config.toml 失败: {}", e))?;
|
||||
crate::config::write_text_file(&config_path, config_str)?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,7 +125,8 @@ pub fn restore_codex_provider_config(provider_id: &str, provider_name: &str) ->
|
||||
log::info!("已恢复 Codex config.toml");
|
||||
} else {
|
||||
// 写入空文件
|
||||
fs::write(&config_path, "").map_err(|e| format!("创建空的 config.toml 失败: {}", e))?;
|
||||
crate::config::write_text_file(&config_path, "")
|
||||
.map_err(|e| format!("创建空的 config.toml 失败: {}", e))?;
|
||||
log::info!("供应商 config.toml 缺失,已创建空文件");
|
||||
}
|
||||
|
||||
|
||||
@@ -320,16 +320,16 @@ pub async fn switch_provider(
|
||||
toml::from_str::<toml::Table>(cfg_str)
|
||||
.map_err(|e| format!("config.toml 格式错误: {}", e))?;
|
||||
}
|
||||
std::fs::write(&config_path, cfg_str)
|
||||
crate::config::write_text_file(&config_path, cfg_str)
|
||||
.map_err(|e| format!("写入 config.toml 失败: {}", e))?;
|
||||
} else {
|
||||
// 非字符串时,写空
|
||||
std::fs::write(&config_path, "")
|
||||
crate::config::write_text_file(&config_path, "")
|
||||
.map_err(|e| format!("写入空的 config.toml 失败: {}", e))?;
|
||||
}
|
||||
} else {
|
||||
// 缺失则写空
|
||||
std::fs::write(&config_path, "")
|
||||
crate::config::write_text_file(&config_path, "")
|
||||
.map_err(|e| format!("写入空的 config.toml 失败: {}", e))?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// 获取 Claude Code 配置目录路径
|
||||
@@ -79,7 +80,54 @@ pub fn write_json_file<T: Serialize>(path: &Path, data: &T) -> Result<(), String
|
||||
let json =
|
||||
serde_json::to_string_pretty(data).map_err(|e| format!("序列化 JSON 失败: {}", e))?;
|
||||
|
||||
fs::write(path, json).map_err(|e| format!("写入文件失败: {}", e))
|
||||
atomic_write(path, json.as_bytes())
|
||||
}
|
||||
|
||||
/// 原子写入文本文件(用于 TOML/纯文本)
|
||||
pub fn write_text_file(path: &Path, data: &str) -> Result<(), String> {
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent).map_err(|e| format!("创建目录失败: {}", e))?;
|
||||
}
|
||||
atomic_write(path, data.as_bytes())
|
||||
}
|
||||
|
||||
/// 原子写入:写入临时文件后 rename 替换,避免半写状态
|
||||
pub fn atomic_write(path: &Path, data: &[u8]) -> Result<(), String> {
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent).map_err(|e| format!("创建目录失败: {}", e))?;
|
||||
}
|
||||
|
||||
let parent = path.parent().ok_or_else(|| "无效的路径".to_string())?;
|
||||
let mut tmp = parent.to_path_buf();
|
||||
let file_name = path
|
||||
.file_name()
|
||||
.ok_or_else(|| "无效的文件名".to_string())?
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
let ts = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_nanos();
|
||||
tmp.push(format!("{}.tmp.{}", file_name, ts));
|
||||
|
||||
{
|
||||
let mut f = fs::File::create(&tmp).map_err(|e| format!("创建临时文件失败: {}", e))?;
|
||||
f.write_all(data)
|
||||
.map_err(|e| format!("写入临时文件失败: {}", e))?;
|
||||
f.flush().map_err(|e| format!("刷新临时文件失败: {}", e))?;
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
if let Ok(meta) = fs::metadata(path) {
|
||||
let perm = meta.permissions().mode();
|
||||
let _ = fs::set_permissions(&tmp, fs::Permissions::from_mode(perm));
|
||||
}
|
||||
}
|
||||
|
||||
fs::rename(&tmp, path).map_err(|e| format!("原子替换失败: {}", e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 复制文件
|
||||
@@ -132,10 +180,7 @@ pub fn import_current_config_as_default() -> Result<Value, String> {
|
||||
// 读取当前配置
|
||||
let settings_config: Value = read_json_file(&settings_path)?;
|
||||
|
||||
// 保存为 default 供应商
|
||||
let default_provider_path = get_provider_config_path("default", Some("default"));
|
||||
write_json_file(&default_provider_path, &settings_config)?;
|
||||
|
||||
log::info!("已导入当前配置为默认供应商");
|
||||
// 不再写入供应商副本文件,这里仅返回读取到的配置
|
||||
log::info!("已读取当前配置用于默认供应商导入");
|
||||
Ok(settings_config)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user