fix: recover macOS OAuth data from app update backups

This commit is contained in:
lzt404 committed 2026-09-28 12:47:33 +08:00
1 parent cf97e8ed24
commit 5bc3142249
8 files changed
+89 -7

No files matched your search

+27 -1
View File
@@ -1127,7 +1127,33 @@ pub(crate) fn migrate_auth_dir_from_macos_app_bundle(
));
}
copy_auth_directory(&source, &destination)?;
let migration_source = match fs::symlink_metadata(&source) {
Ok(_) => source.clone(),
Err(error) if error.kind() == io::ErrorKind::NotFound => source
.ancestors()
.find_map(|directory| {
if directory.file_name()? != std::ffi::OsStr::new("Contents") {
return None;
}
let app = directory.parent()?;
if !app.extension()?.to_string_lossy().eq_ignore_ascii_case("app") {
return None;
}
Some(
app.parent()?
.join(".EasyCLIProxyAPI.app.update-backup")
.join(source.strip_prefix(app).ok()?),
)
})
.unwrap_or_else(|| source.clone()),
Err(error) => {
return Err(format!(
"Failed to check old OAuth directory {}: {error}",
path_to_string(&source)
));
}
};
copy_auth_directory(&migration_source, &destination)?;
config.auth_dir = DEFAULT_AUTH_DIR.to_string();
Ok(true)
}
+3
View File
@@ -2380,6 +2380,9 @@ fn main() {
Ok(config) => config,
Err(error) => {
eprintln!("{error}");
if portable_update_ack.is_some() {
return;
}
let mut config = GuiConfigFile::default();
if let Err(secret_error) = ensure_strong_management_secret(&mut config) {
eprintln!("Failed to initialize WebUI security key: {secret_error}");
+9 -6
View File
@@ -627,10 +627,13 @@ fn portable_update_zip_accepts_the_complete_release_package() {
fs::remove_dir_all(root).unwrap();
}
#[cfg(windows)]
#[cfg(any(windows, target_os = "linux"))]
#[test]
fn portable_update_replacement_preserves_user_data_and_can_roll_back() {
let root = agent_test_home("portable-replace");
let platform = current_core_platform().unwrap();
let old_archive = core_release_asset_name("v7.2.100", &platform);
let new_archive = core_release_asset_name("v7.2.109", &platform);
let app_dir = root.join("app");
let work_dir = root.join("work");
let staging = work_dir.join("staging");
@@ -644,7 +647,7 @@ fn portable_update_replacement_preserves_user_data_and_can_roll_back() {
fs::write(
app_dir
.join("cpa-core")
.join("CLIProxyAPI_7.2.100_windows_amd64.zip"),
.join(&old_archive),
b"old core archive",
)
.unwrap();
@@ -670,7 +673,7 @@ fn portable_update_replacement_preserves_user_data_and_can_roll_back() {
fs::write(
staging
.join("cpa-core")
.join("CLIProxyAPI_7.2.109_windows_amd64.zip"),
.join(&new_archive),
b"new core archive",
)
.unwrap();
@@ -680,17 +683,17 @@ fn portable_update_replacement_preserves_user_data_and_can_roll_back() {
staged_exe: staging.join(PORTABLE_APP_BINARY),
current_manifest: app_dir.join(PORTABLE_APP_MANIFEST_FILE),
staged_manifest: staging.join(PORTABLE_APP_MANIFEST_FILE),
backup_exe: app_dir.join(".EasyCLIProxyAPI.exe.update-backup"),
backup_exe: app_dir.join(format!(".{PORTABLE_APP_BINARY}.update-backup")),
backup_manifest: app_dir.join(".portable-app.json.update-backup"),
current_core_version: app_dir.join(CORE_VERSION_FILE),
staged_core_version: staging.join(CORE_VERSION_FILE),
backup_core_version: app_dir.join(".core-version.txt.update-backup"),
staged_core_archive: staging
.join("cpa-core")
.join("CLIProxyAPI_7.2.109_windows_amd64.zip"),
.join(&new_archive),
target_core_archive: app_dir
.join("cpa-core")
.join("CLIProxyAPI_7.2.109_windows_amd64.zip"),
.join(&new_archive),
install_core_archive: true,
ack_path: work_dir.join("update-started.ack"),
work_dir,
+46
View File
@@ -756,6 +756,52 @@ fn packaged_macos_auth_directory_is_copied_before_config_is_repointed() {
fs::remove_dir_all(root).unwrap();
}
#[test]
fn packaged_macos_auth_migration_recovers_the_replaced_app_backup() {
let root = agent_test_home("macos-auth-update-backup");
let app = root.join("Applications/EasyCLIProxyAPI.app");
let relative = Path::new("Contents/MacOS/oauth");
let source = app.join(relative);
let backup = root.join("Applications/.EasyCLIProxyAPI.app.update-backup");
let old_auth = backup.join(relative);
let persistent = root.join("Library/Application Support/com.cpa.gui");
let install_dir = persistent.join("cpa-core");
let destination = persistent.join(OAUTH_DIR_NAME);
fs::create_dir_all(app.join("Contents/MacOS")).unwrap();
fs::create_dir_all(old_auth.join("logs/archive")).unwrap();
fs::write(old_auth.join("account.json"), b"existing-credential").unwrap();
fs::write(old_auth.join("logs/archive/core.log"), b"historical-log").unwrap();
let mut config = GuiConfigFile {
auth_dir: path_to_string(&source),
..GuiConfigFile::default()
};
assert!(
migrate_auth_dir_from_macos_app_bundle(&mut config, &install_dir, &destination).unwrap()
);
assert_eq!(config.auth_dir, DEFAULT_AUTH_DIR);
assert_eq!(fs::read(destination.join("account.json")).unwrap(), b"existing-credential");
assert_eq!(fs::read(destination.join("logs/archive/core.log")).unwrap(), b"historical-log");
assert_eq!(fs::read(old_auth.join("account.json")).unwrap(), b"existing-credential");
config.auth_dir = path_to_string(&source);
fs::write(destination.join("account.json"), b"different-credential").unwrap();
let error = migrate_auth_dir_from_macos_app_bundle(&mut config, &install_dir, &destination)
.unwrap_err();
assert!(error.contains("not overwritten"), "{error}");
assert_eq!(config.auth_dir, path_to_string(&source));
assert_eq!(fs::read(destination.join("account.json")).unwrap(), b"different-credential");
assert_eq!(fs::read(old_auth.join("account.json")).unwrap(), b"existing-credential");
fs::create_dir_all(&source).unwrap();
fs::write(source.join("account.json"), b"different-credential").unwrap();
assert!(
migrate_auth_dir_from_macos_app_bundle(&mut config, &install_dir, &destination).unwrap()
);
assert_eq!(fs::read(destination.join("account.json")).unwrap(), b"different-credential");
fs::remove_dir_all(root).unwrap();
}
#[test]
fn packaged_macos_auth_migration_does_not_overwrite_conflicting_credentials() {
let root = agent_test_home("packaged-macos-auth-conflict");