diff --git a/docs/release-notes/v0.3.9/en.md b/docs/release-notes/v0.3.9/en.md index 69175c4..cdfe23a 100644 --- a/docs/release-notes/v0.3.9/en.md +++ b/docs/release-notes/v0.3.9/en.md @@ -10,6 +10,7 @@ ## Fixed +- Fixed macOS updates losing access to OAuth credentials stored inside the old app bundle. Credentials and their logs are now recovered from the update backup into persistent storage. If configuration loading fails after an automatic update, startup stops so the updater can roll back instead of accepting default settings. - Fixed core updates overwriting existing settings with v8 template defaults. Upgrades from v7 and updates to partially migrated v8 configurations now preserve existing settings, including the OAuth credential directory, port, and access keys. - Fixed upstream API credentials being overwritten with client access keys during startup or access-key changes when a v8 configuration has no version marker. - Core startup logs now append to existing output, preserving startup history when the core starts or restarts. diff --git a/docs/release-notes/v0.3.9/ja.md b/docs/release-notes/v0.3.9/ja.md index 32c631d..4e36f80 100644 --- a/docs/release-notes/v0.3.9/ja.md +++ b/docs/release-notes/v0.3.9/ja.md @@ -10,6 +10,7 @@ ## 修正 +- macOS のアプリ更新後に、旧アプリバンドル内の OAuth 認証情報の保存先が見つからなくなる問題を修正しました。更新時のバックアップから認証情報とそのログを永続保存先に復元します。自動更新後に設定の読み込みに失敗した場合は、既定の設定で更新成功とせず、起動を中止して旧バージョンにロールバックします。 - コアのアップグレード時に、新しい設定テンプレートの既定値が既存の設定を上書きする問題を修正しました。v7 から v8 へのアップグレードや、移行が一部のみ完了した v8 設定の更新でも、OAuth 認証情報の保存先、ポート、アクセスキーなどの既存設定を保持します。 - バージョン情報のない v8 設定で、起動時やアクセスキーの変更時に、上流 API の認証情報がクライアント用アクセスキーで上書きされる問題を修正しました。 - コアの起動ログを追記方式に変更し、起動や再起動時にも過去の起動出力を保持するようにしました。 diff --git a/docs/release-notes/v0.3.9/zh-CN.md b/docs/release-notes/v0.3.9/zh-CN.md index ef07676..58a9814 100644 --- a/docs/release-notes/v0.3.9/zh-CN.md +++ b/docs/release-notes/v0.3.9/zh-CN.md @@ -10,6 +10,7 @@ ## 修复 +- 修复 macOS 替换应用包后可能无法找到旧包内 OAuth 凭据目录的问题;从更新备份恢复凭据及其日志到持久化目录。自动更新后的配置加载失败时触发回滚,避免按默认设置确认升级成功。 - 修复内核升级时新版模板默认值可能覆盖原有设置的问题。从 v7 升级到 v8,以及更新部分已迁移的 v8 配置时,保留现有配置,避免 OAuth 凭据目录、端口和访问密钥等设置被重置。 - 修复未标记版本的 v8 配置在启动或修改访问密钥时,可能误将上游 API 凭据覆盖为访问密钥的问题。 - 内核启动日志改为追加写入,启动或重启内核时保留历史启动输出。 diff --git a/docs/release-notes/v0.3.9/zh-TW.md b/docs/release-notes/v0.3.9/zh-TW.md index 1817c40..c3c054e 100644 --- a/docs/release-notes/v0.3.9/zh-TW.md +++ b/docs/release-notes/v0.3.9/zh-TW.md @@ -10,6 +10,7 @@ ## 修復 +- 修復 macOS 替換應用程式套件後,可能找不到舊套件內 OAuth 憑據目錄的問題;從更新備份將憑據及其日誌復原至持久化目錄。自動更新後若設定載入失敗,會觸發回復,避免以預設設定確認升級成功。 - 修復核心升級時新版範本預設值可能覆蓋原有設定的問題。從 v7 升級至 v8,以及更新部分已遷移的 v8 設定時,保留現有設定,避免 OAuth 憑據目錄、連接埠及存取金鑰等項目被重設。 - 修復未標記版本的 v8 設定在啟動或修改存取金鑰時,可能誤將上游 API 憑據覆蓋為存取金鑰的問題。 - 核心啟動日誌改為附加寫入,啟動或重新啟動核心時保留歷史啟動輸出。 diff --git a/src-tauri/src/core_config/settings.rs b/src-tauri/src/core_config/settings.rs index 5bfad1b..a62a337 100644 --- a/src-tauri/src/core_config/settings.rs +++ b/src-tauri/src/core_config/settings.rs @@ -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) } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 328aa1c..53a6efb 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -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}"); diff --git a/src-tauri/src/tests/app_update.rs b/src-tauri/src/tests/app_update.rs index d2a5211..9add79c 100644 --- a/src-tauri/src/tests/app_update.rs +++ b/src-tauri/src/tests/app_update.rs @@ -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, diff --git a/src-tauri/src/tests/core_config.rs b/src-tauri/src/tests/core_config.rs index 1e5006a..9973339 100644 --- a/src-tauri/src/tests/core_config.rs +++ b/src-tauri/src/tests/core_config.rs @@ -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");