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