From 6cc8e782b9322585e173c8a8bc3b8c9757408323 Mon Sep 17 00:00:00 2001 From: chen qiang <343005560@qq.com> Date: Wed, 26 Aug 2026 09:08:47 +0800 Subject: [PATCH] fix: preserve config across Velopack updates --- AGENTS.md | 11 +-- CHANGELOG.md | 8 ++ RELEASE.md | 23 +++--- henlo_migration_wpf10/App.xaml.cs | 81 ++++++++++++++++++- henlo_migration_wpf10/Common/CommonHelper.cs | 27 +++++++ henlo_migration_wpf10/Common/SysConfig.cs | 2 +- henlo_migration_wpf10/README.md | 4 +- .../henlo_migration_wpf10.csproj | 2 +- 8 files changed, 137 insertions(+), 21 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index c948554..1f23aa2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -40,7 +40,7 @@ ### 启动与界面 - `App.xaml`:应用资源、HandyControl 主题和 `MainViewModel` 资源注册;启动页是 `Views/HomeWindow.xaml`。 -- `App.xaml.cs`:Velopack 初始化、后台/手动更新、更新前后 `config.ini`、`data/` 与 `pre/` 备份恢复、全局异常日志。 +- `App.xaml.cs`:Velopack 初始化、旧版 `current/config.ini` 更新迁移、后台/手动更新、更新前后 `config.ini`、`data/` 与 `pre/` 备份恢复、全局异常日志。 - `Views/HomeWindow.xaml`:单窗口主界面,包含五个迁移 Tab、连接状态、SQL 编辑区和批量操作。 - `Views/HomeWindow.xaml.cs`:只处理窗口关闭和手动检查更新等窗口事件。 - `Views/ConnectionDialog.*`:源库/目标库连接配置与测试。 @@ -65,8 +65,8 @@ - `GetTableData`:生成表数据 INSERT SQL; - `ExecuteCreate`:按 `\n/` 分隔 SQL,在目标库逐条执行并检查 `USER_ERRORS`。 - `Common/SqlSugarHelper.cs`:维护 `orig`(源库)和 `dest`(目标库)两个 SqlSugar 连接。 -- `Common/SysConfig.cs`:读取和写入运行目录下的 `config.ini`。 -- `Common/CommonHelper.cs`:运行目录、`data/`、`data/from/`、`pre/` 路径及文本读写。 +- `Common/SysConfig.cs`:读取和写入用户持久目录下的 `config.ini`。 +- `Common/CommonHelper.cs`:运行目录、Velopack 持久目录、`config.ini`、`data/`、`data/from/`、`pre/` 路径及文本读写。 ## 真实数据流 @@ -94,7 +94,8 @@ ## 配置和运行时文件 -- `config.ini` 保存源库和目标库的 IP/服务名、用户名、密码。当前实现是明文读写,首次运行缺失时创建空配置;发布包不得携带本机配置。 +- `config.ini` 保存源库和目标库的 IP/服务名、用户名、密码。当前实现是明文读写,首次运行缺失时创建空配置;发布包不得携带本机配置。Velopack 安装态/便携态必须写在与 `Update.exe` 同级的根目录,不能写回每次更新都会整体替换的 `current/`;普通未打包运行时仍写程序目录。 +- 从旧版本首次更新时,`OnAfterUpdateFastCallback` 会在 Velopack 删除旧 `current/` 临时备份之前,将其中的 `config.ini` 迁移到根目录。不要移除该兼容逻辑,除非已确认所有在用旧版本完成迁移。 - 不要在日志、回答、补丁或测试输出中展示真实连接串和密码。 - 修改或发布时不要用样例配置覆盖用户现有 `config.ini`。 - `data/` 和 `pre/` 是用户可编辑、更新时需要保留的运行数据;不要无故清空、批量改名或删除。 @@ -157,7 +158,7 @@ dotnet build .\henlo_migration_wpf10\henlo_migration_wpf10.csproj -c Debug --no- dotnet run --project .\henlo_migration_wpf10\henlo_migration_wpf10.csproj ``` -启动会读取或创建运行目录下的配置,并可能尝试初始化更新检查;不适合无界面的 CI 验证。 +启动会读取或创建用户持久目录下的配置,并可能尝试初始化更新检查;不适合无界面的 CI 验证。 ## 版本与发布 diff --git a/CHANGELOG.md b/CHANGELOG.md index 62039d8..0c86ee6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # 更新记录 +## 1.0.18 - 2026-08-26 + +### 修复 + +- 将 `config.ini` 从 Velopack 每次更新都会整体替换的 `current/` 目录迁移到与 `Update.exe` 同级的持久目录。 +- 增加 Velopack 更新后快速回调:从更新器尚未清理的旧 `current/` 备份中恢复 `config.ini`,兼容 1.0.16 等旧版本首次升级。 +- 更新前临时备份和更新后恢复统一读写持久目录中的 `config.ini`,后续升级不再依赖 `current/` 内的配置文件。 + ## 1.0.17 - 2026-08-26 ### 新增 diff --git a/RELEASE.md b/RELEASE.md index bff4b90..753e379 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -75,7 +75,7 @@ cd .\henlo_migration_wpf10 - `.csproj` 只复制 `pre/*.txt|*.fnc|*.prc` 等程序辅助脚本,不复制本机 `config.ini` 或 `data/from/*.txt`;`pack.bat` 还会在打包前再次删除 `publish/config.ini`,并创建空的 `data/from/` 目录。 - `pre/hmig_get_table_bos_31.prc`、`pre/hmig_get_table_bos_field_31.prc` 等运行时辅助过程必须出现在发布输出中。 - 新安装的索引列表为空;程序添加第一条对应配置时会在 `data/from/` 创建 `table.txt`、`bos.txt`、`bos_field.txt`、`prc.txt` 或 `data.txt`。 -- 新安装首次运行时会创建值为空的 `config.ini`,由用户在界面配置;不得把真实或样例账号密码放入发布产物。 +- 新安装首次运行时会在 Velopack 根目录(与 `Update.exe` 同级)创建值为空的 `config.ini`;普通未打包运行时仍在程序目录创建。不得把真实或样例账号密码放入发布产物。 ## Velopack 产物 @@ -115,16 +115,18 @@ http://zjhenlo.henlo.net:8878/henlo_migration/auto_update/ `App.xaml.cs` 当前行为: -1. UI 启动前调用 `VelopackApp.Build().Run()`。 -2. 从系统临时目录恢复上次更新前备份的 `config.ini`、`data/`、`pre/`。 -3. 创建指向更新服务器的 `UpdateManager`;非 Velopack 环境创建失败时禁用自动更新。 -4. 后台静默检查并下载更新;手动“检查更新”使用相同更新源。 -5. 用户同意立即重启时,先备份 `config.ini`、`data/`、`pre/`,再调用 `ApplyUpdatesAndRestart()`。 -6. 新版本首次启动时恢复备份并删除临时备份目录。 +1. UI 启动前注册 `OnAfterUpdateFastCallback` 并调用 `VelopackApp.Build().Run()`。 +2. 从旧版更新时,更新器替换 `current/` 后会先调用新程序的快速回调;此时旧 `current/` 仍保存在 `packages/VelopackTemp/tmp_*`,程序从中把旧 `config.ini` 迁移到 Velopack 根目录。 +3. 从系统临时目录恢复上次更新前备份的 `config.ini`、`data/`、`pre/`。 +4. 创建指向更新服务器的 `UpdateManager`;非 Velopack 环境创建失败时禁用自动更新。 +5. 后台静默检查并下载更新;手动“检查更新”使用相同更新源。 +6. 用户同意立即重启时,先备份持久目录中的 `config.ini` 以及 `current/` 下的 `data/`、`pre/`,再调用 `ApplyUpdatesAndRestart()`。 +7. 新版本首次启动时恢复备份并删除临时备份目录。 当前限制和未验证项: -- 更新备份范围包含 `config.ini`、`data/` 和 `pre/`;恢复失败时会记录到 `error.log`,发版验收仍需验证用户配置未丢失。 +- `config.ini` 必须位于 Velopack 根目录,不能重新放回会被整体替换的 `current/`。旧版本首次升级依赖 `OnAfterUpdateFastCallback` 在更新器清理旧目录前完成迁移;迁移异常记录到根目录的 `config_migration.log`。 +- 更新备份范围包含持久目录中的 `config.ini` 以及 `current/` 中的 `data/`、`pre/`;恢复失败时会记录到 `error.log`,发版验收仍需验证用户配置未丢失。 - 用户下载完成后选择“不立即重启”时,代码没有明确实现下次启动自动应用已下载更新的流程。 - 直接运行普通 `publish/` 中的 EXE 时通常不属于 Velopack 安装态,程序会禁用更新或把检查异常写入 `error.log`。 - `--noInst` 生成的是便携模式;便携包的检查、下载、应用和重启更新链路尚未完成实际端到端回归,不能声称自动更新可靠可用。 @@ -138,5 +140,6 @@ http://zjhenlo.henlo.net:8878/henlo_migration/auto_update/ 5. 检查便携 ZIP、full/delta 包、`RELEASES` 和两个 JSON 的版本、大小及时间。 6. 检查发布包中的 `pre/` 和必要运行文件,并确认产物中不存在 `config.ini` 及开发机 `data/from/*.txt`。 7. 在未安装态测试普通 EXE/便携包启动;若任务涉及自动更新,再在对应 Velopack 运行形态下单独验证更新。 -8. 只有获得上传授权后才能部署服务器;上传后验证 URL 和包完整性。 -9. 交付说明必须区分:构建验证、打包验证、便携运行验证、自动更新验证和服务器部署验证。 +8. 使用旧版便携目录写入不含真实凭据的标记配置,应用新版 full 包,确认标记配置已从旧 `current/config.ini` 迁移为根目录 `config.ini`,且新版再次启动不会覆盖。 +9. 只有获得上传授权后才能部署服务器;上传后验证 URL 和包完整性。 +10. 交付说明必须区分:构建验证、打包验证、便携运行验证、自动更新验证和服务器部署验证。 diff --git a/henlo_migration_wpf10/App.xaml.cs b/henlo_migration_wpf10/App.xaml.cs index 041b59e..91d2daf 100644 --- a/henlo_migration_wpf10/App.xaml.cs +++ b/henlo_migration_wpf10/App.xaml.cs @@ -1,6 +1,7 @@ using System; using System.Diagnostics; using System.IO; +using System.Linq; using System.Threading.Tasks; using System.Windows; using Velopack; @@ -23,7 +24,12 @@ namespace henlo_migration try { // Velopack 初始化:必须在所有 UI 之前调用 - VelopackApp.Build().Run(); + VelopackApp.Build() + .OnAfterUpdateFastCallback(_ => PreserveLegacyConfigAfterUpdate()) + .Run(); + + // 兼容手工覆盖 current 目录、旧便携包等非标准升级方式。 + MigrateLegacyConfig(); // 更新后恢复用户数据(如果存在备份) RestoreUserData(); @@ -164,7 +170,7 @@ namespace henlo_migration var rootDir = AppDomain.CurrentDomain.BaseDirectory; var dataDir = Path.Combine(rootDir, "data"); var preDir = Path.Combine(rootDir, "pre"); - var configPath = Path.Combine(rootDir, "config.ini"); + var configPath = Common.CommonHelper.ConfigPath; if (!Directory.Exists(dataDir) && !Directory.Exists(preDir) && !File.Exists(configPath)) return; @@ -206,7 +212,7 @@ namespace henlo_migration var backupConfig = Path.Combine(BackupDir, "config.ini"); if (File.Exists(backupConfig)) - File.Copy(backupConfig, Path.Combine(rootDir, "config.ini"), true); + File.Copy(backupConfig, Common.CommonHelper.ConfigPath, true); Directory.Delete(BackupDir, true); } @@ -225,6 +231,75 @@ namespace henlo_migration CopyDirectory(dir, Path.Combine(targetDir, Path.GetFileName(dir))); } + /// + /// Velopack 已替换 current、但尚未删除旧 current 备份时执行,抢救旧版本的 config.ini。 + /// + private static void PreserveLegacyConfigAfterUpdate() + { + try + { + var locator = VelopackLocator.Current; + if (string.IsNullOrWhiteSpace(locator.RootAppDir) || + string.IsNullOrWhiteSpace(locator.AppTempDir)) + return; + + var targetConfig = Path.Combine(locator.RootAppDir, "config.ini"); + if (File.Exists(targetConfig)) return; + + var legacyConfig = Directory.EnumerateDirectories(locator.AppTempDir, "tmp_*") + .Select(path => Path.Combine(path, "config.ini")) + .Where(File.Exists) + .OrderByDescending(File.GetLastWriteTimeUtc) + .FirstOrDefault(); + + if (legacyConfig == null) return; + + Directory.CreateDirectory(locator.RootAppDir); + File.Copy(legacyConfig, targetConfig, false); + } + catch (Exception ex) + { + LogMigrationException(ex); + } + } + + /// 普通启动时把仍位于 current 的旧配置迁移到 Velopack 持久目录。 + private static void MigrateLegacyConfig() + { + try + { + var targetConfig = Common.CommonHelper.ConfigPath; + var legacyConfig = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "config.ini"); + + if (!Path.GetFullPath(targetConfig).Equals( + Path.GetFullPath(legacyConfig), StringComparison.OrdinalIgnoreCase) && + !File.Exists(targetConfig) && + File.Exists(legacyConfig)) + { + File.Copy(legacyConfig, targetConfig, false); + } + } + catch (Exception ex) + { + LogMigrationException(ex); + } + } + + private static void LogMigrationException(Exception ex) + { + try + { + var logRoot = VelopackLocator.IsCurrentSet && + !string.IsNullOrWhiteSpace(VelopackLocator.Current.RootAppDir) + ? VelopackLocator.Current.RootAppDir + : AppDomain.CurrentDomain.BaseDirectory; + File.AppendAllText( + Path.Combine(logRoot, "config_migration.log"), + $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {ex.Message}{Environment.NewLine}"); + } + catch { } + } + private void LogException(string type, Exception ex) { try diff --git a/henlo_migration_wpf10/Common/CommonHelper.cs b/henlo_migration_wpf10/Common/CommonHelper.cs index d60a0a1..a451531 100644 --- a/henlo_migration_wpf10/Common/CommonHelper.cs +++ b/henlo_migration_wpf10/Common/CommonHelper.cs @@ -11,6 +11,33 @@ namespace henlo_migration.Common /// 程序运行根目录 public static string RootPath => AppDomain.CurrentDomain.BaseDirectory; + /// + /// 用户配置持久目录。Velopack 更新会整体替换 current 目录,因此安装态/便携态使用其父目录; + /// 普通 dotnet run 或 publish 运行时仍使用程序目录。 + /// + public static string PersistentRootPath + { + get + { + var rootPath = Path.GetFullPath(RootPath) + .TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + var rootDir = new DirectoryInfo(rootPath); + var parentDir = rootDir.Parent; + + if (parentDir != null && + rootDir.Name.Equals("current", StringComparison.OrdinalIgnoreCase) && + File.Exists(Path.Combine(parentDir.FullName, "Update.exe"))) + { + return parentDir.FullName; + } + + return rootDir.FullName; + } + } + + /// Oracle 连接配置文件路径 + public static string ConfigPath => Path.Combine(PersistentRootPath, "config.ini"); + /// 源数据文件夹(存放 table.txt、bos.txt、prc.txt、data.txt 及各表的 .txt 文件) public static string DataPath { diff --git a/henlo_migration_wpf10/Common/SysConfig.cs b/henlo_migration_wpf10/Common/SysConfig.cs index b4b08fb..76aa076 100644 --- a/henlo_migration_wpf10/Common/SysConfig.cs +++ b/henlo_migration_wpf10/Common/SysConfig.cs @@ -8,7 +8,7 @@ namespace henlo_migration.Common { private static IniData _init; - private static string ConfigPath => System.IO.Path.Combine(CommonHelper.RootPath, "config.ini"); + private static string ConfigPath => CommonHelper.ConfigPath; static SysConfig() { var parser = new FileIniDataParser(); diff --git a/henlo_migration_wpf10/README.md b/henlo_migration_wpf10/README.md index 2380207..2ed60a6 100644 --- a/henlo_migration_wpf10/README.md +++ b/henlo_migration_wpf10/README.md @@ -90,7 +90,9 @@ henlo_migration_wpf10/ - **源库**:读取数据的数据库 - **目标库**:写入数据的数据库 -连接信息保存在运行目录的 `config.ini` 中。发布包不携带该文件,首次运行会创建空配置。 +连接信息保存在用户配置持久目录的 `config.ini` 中。Velopack 安装态或便携态位于与 `Update.exe` 同级的根目录,普通 `dotnet run`/`publish` 运行时位于程序目录。发布包不携带该文件,首次运行会创建空配置。 + +从旧版本更新时,新程序会在 Velopack 删除旧 `current/` 备份前迁移原 `current/config.ini`;后续更新只替换 `current/`,不会覆盖根目录中的配置。 ### 2. 索引文件 diff --git a/henlo_migration_wpf10/henlo_migration_wpf10.csproj b/henlo_migration_wpf10/henlo_migration_wpf10.csproj index 452d572..9084191 100644 --- a/henlo_migration_wpf10/henlo_migration_wpf10.csproj +++ b/henlo_migration_wpf10/henlo_migration_wpf10.csproj @@ -8,7 +8,7 @@ disable Resources\favicon.ico true - 1.0.17 + 1.0.18