fix(core): preserve installed plugins during core updates
Overlay extracted core files onto the existing installation instead of replacing the entire cpa-core directory, preserving plugins and user data.
This commit is contained in:
1 parent
708001ca37
commit
11b1ea23a5
2 files changed
+136
-23
No files matched your search
@@ -180,7 +180,6 @@ pub(crate) async fn install_core_version_inner(
|
||||
let install_dir = core_install_dir()?;
|
||||
let base_dir = core_base_dir()?;
|
||||
let staging_dir = base_dir.join("cpa-core.staging");
|
||||
let backup_dir = base_dir.join("cpa-core.backup");
|
||||
let download_dir = base_dir.join("cpa-core.download");
|
||||
|
||||
if current_core_status(None, None)?.running {
|
||||
@@ -231,7 +230,7 @@ pub(crate) async fn install_core_version_inner(
|
||||
},
|
||||
)?;
|
||||
|
||||
replace_install_dir(&install_dir, &staging_dir, &backup_dir)?;
|
||||
overlay_install_dir(&install_dir, &staging_dir)?;
|
||||
let _ = fs::remove_dir_all(&download_dir);
|
||||
|
||||
Ok(CoreInstallResult {
|
||||
@@ -252,7 +251,6 @@ pub(crate) fn install_bundled_core_inner(
|
||||
let install_dir = core_install_dir()?;
|
||||
let base_dir = core_base_dir()?;
|
||||
let staging_dir = base_dir.join("cpa-core.staging");
|
||||
let backup_dir = base_dir.join("cpa-core.backup");
|
||||
|
||||
if current_core_status(None, None)?.running {
|
||||
return Err("CPA 内核正在运行,请先停止后再使用内置内核".to_string());
|
||||
@@ -294,7 +292,7 @@ pub(crate) fn install_bundled_core_inner(
|
||||
installed_at_unix: unix_now(),
|
||||
},
|
||||
)?;
|
||||
replace_install_dir(&install_dir, &staging_dir, &backup_dir)?;
|
||||
overlay_install_dir(&install_dir, &staging_dir)?;
|
||||
|
||||
Ok(CoreInstallResult {
|
||||
version: info.version.clone(),
|
||||
@@ -1654,30 +1652,70 @@ pub(crate) fn reset_dir(path: &Path) -> Result<(), String> {
|
||||
fs::create_dir_all(path).map_err(|err| format!("创建目录失败 {}: {err}", path_to_string(path)))
|
||||
}
|
||||
|
||||
pub(crate) fn replace_install_dir(
|
||||
install_dir: &Path,
|
||||
staging_dir: &Path,
|
||||
backup_dir: &Path,
|
||||
) -> Result<(), String> {
|
||||
if backup_dir.exists() {
|
||||
fs::remove_dir_all(backup_dir).map_err(|err| format!("清理备份目录失败: {err}"))?;
|
||||
pub(crate) fn overlay_install_dir(install_dir: &Path, staging_dir: &Path) -> Result<(), String> {
|
||||
if !staging_dir.is_dir() {
|
||||
return Err(format!(
|
||||
"内核暂存目录不存在: {}",
|
||||
path_to_string(staging_dir)
|
||||
));
|
||||
}
|
||||
|
||||
if install_dir.exists() {
|
||||
fs::rename(install_dir, backup_dir)
|
||||
.map_err(|err| format!("备份旧内核目录失败,请确认 CPA 内核未运行: {err}"))?;
|
||||
if !install_dir.exists() {
|
||||
return fs::rename(staging_dir, install_dir)
|
||||
.map_err(|err| format!("安装新内核目录失败: {err}"));
|
||||
}
|
||||
if !install_dir.is_dir() {
|
||||
return Err(format!(
|
||||
"内核安装路径不是目录: {}",
|
||||
path_to_string(install_dir)
|
||||
));
|
||||
}
|
||||
|
||||
if let Err(err) = fs::rename(staging_dir, install_dir) {
|
||||
if backup_dir.exists() {
|
||||
let _ = fs::rename(backup_dir, install_dir);
|
||||
overlay_directory(staging_dir, install_dir)?;
|
||||
fs::remove_dir_all(staging_dir).map_err(|err| format!("清理内核暂存目录失败: {err}"))
|
||||
}
|
||||
|
||||
fn overlay_directory(source_dir: &Path, target_dir: &Path) -> Result<(), String> {
|
||||
for entry in fs::read_dir(source_dir)
|
||||
.map_err(|err| format!("读取内核暂存目录失败 {}: {err}", path_to_string(source_dir)))?
|
||||
{
|
||||
let entry = entry.map_err(|err| format!("读取内核暂存条目失败: {err}"))?;
|
||||
let source_path = entry.path();
|
||||
let target_path = target_dir.join(entry.file_name());
|
||||
let file_type = entry
|
||||
.file_type()
|
||||
.map_err(|err| format!("读取内核暂存条目类型失败: {err}"))?;
|
||||
|
||||
if file_type.is_dir() {
|
||||
if target_path.exists() && !target_path.is_dir() {
|
||||
return Err(format!(
|
||||
"无法用内核目录覆盖同名文件: {}",
|
||||
path_to_string(&target_path)
|
||||
));
|
||||
}
|
||||
fs::create_dir_all(&target_path).map_err(|err| {
|
||||
format!(
|
||||
"创建内核安装子目录失败 {}: {err}",
|
||||
path_to_string(&target_path)
|
||||
)
|
||||
})?;
|
||||
overlay_directory(&source_path, &target_path)?;
|
||||
} else if file_type.is_file() {
|
||||
if target_path.exists() && !target_path.is_file() {
|
||||
return Err(format!(
|
||||
"无法用内核文件覆盖同名目录: {}",
|
||||
path_to_string(&target_path)
|
||||
));
|
||||
}
|
||||
fs::copy(&source_path, &target_path).map_err(|err| {
|
||||
format!("覆盖内核文件失败 {}: {err}", path_to_string(&target_path))
|
||||
})?;
|
||||
} else {
|
||||
return Err(format!(
|
||||
"内核暂存目录包含不支持的条目: {}",
|
||||
path_to_string(&source_path)
|
||||
));
|
||||
}
|
||||
|
||||
return Err(format!("切换新内核目录失败: {err}"));
|
||||
}
|
||||
|
||||
if backup_dir.exists() {
|
||||
fs::remove_dir_all(backup_dir).map_err(|err| format!("删除旧内核备份目录失败: {err}"))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -40,6 +40,81 @@ fn replacing_a_core_preserves_only_regular_bundled_assets() {
|
||||
fs::remove_dir_all(root).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overlaying_a_core_updates_packaged_files_and_preserves_plugins() {
|
||||
let root = agent_test_home("core-overlay-preserves-plugins");
|
||||
let install_dir = root.join("cpa-core");
|
||||
let staging_dir = root.join("cpa-core.staging");
|
||||
fs::create_dir_all(install_dir.join("plugins/custom-router")).unwrap();
|
||||
fs::create_dir_all(staging_dir.join("plugins/bundled-router")).unwrap();
|
||||
fs::create_dir_all(staging_dir.join("runtime")).unwrap();
|
||||
fs::write(install_dir.join(core_binary_name()), b"old core").unwrap();
|
||||
fs::write(install_dir.join("README.md"), b"old readme").unwrap();
|
||||
fs::write(install_dir.join("user-data.json"), b"user data").unwrap();
|
||||
fs::write(
|
||||
install_dir.join("plugins/custom-router/plugin.js"),
|
||||
b"custom plugin",
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(staging_dir.join(core_binary_name()), b"new core").unwrap();
|
||||
fs::write(staging_dir.join("README.md"), b"new readme").unwrap();
|
||||
fs::write(
|
||||
staging_dir.join("plugins/bundled-router/plugin.js"),
|
||||
b"bundled plugin",
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(staging_dir.join("runtime/default.json"), b"new runtime").unwrap();
|
||||
|
||||
overlay_install_dir(&install_dir, &staging_dir).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
fs::read(install_dir.join(core_binary_name())).unwrap(),
|
||||
b"new core"
|
||||
);
|
||||
assert_eq!(
|
||||
fs::read(install_dir.join("README.md")).unwrap(),
|
||||
b"new readme"
|
||||
);
|
||||
assert_eq!(
|
||||
fs::read(install_dir.join("runtime/default.json")).unwrap(),
|
||||
b"new runtime"
|
||||
);
|
||||
assert_eq!(
|
||||
fs::read(install_dir.join("plugins/custom-router/plugin.js")).unwrap(),
|
||||
b"custom plugin"
|
||||
);
|
||||
assert_eq!(
|
||||
fs::read(install_dir.join("plugins/bundled-router/plugin.js")).unwrap(),
|
||||
b"bundled plugin"
|
||||
);
|
||||
assert_eq!(
|
||||
fs::read(install_dir.join("user-data.json")).unwrap(),
|
||||
b"user data"
|
||||
);
|
||||
assert!(!staging_dir.exists());
|
||||
fs::remove_dir_all(root).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn installing_a_core_into_a_missing_directory_moves_the_complete_staging_tree() {
|
||||
let root = agent_test_home("core-overlay-first-install");
|
||||
let install_dir = root.join("cpa-core");
|
||||
let staging_dir = root.join("cpa-core.staging");
|
||||
fs::create_dir_all(&staging_dir).unwrap();
|
||||
fs::write(staging_dir.join(core_binary_name()), b"new core").unwrap();
|
||||
fs::write(staging_dir.join(CORE_EXAMPLE_CONFIG_FILE), b"port: 8317\n").unwrap();
|
||||
|
||||
overlay_install_dir(&install_dir, &staging_dir).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
fs::read(install_dir.join(core_binary_name())).unwrap(),
|
||||
b"new core"
|
||||
);
|
||||
assert!(install_dir.join(CORE_EXAMPLE_CONFIG_FILE).is_file());
|
||||
assert!(!staging_dir.exists());
|
||||
fs::remove_dir_all(root).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replacing_a_core_migrates_old_fields_into_the_new_template() {
|
||||
let root = agent_test_home("core-config-migrate");
|
||||
|
||||
Reference in new issue
Block a user