fix: preserve config across Velopack updates

This commit is contained in:
chen qiang
2026-08-26 09:08:47 +08:00
parent a1a0f58ae1
commit 6cc8e782b9
8 changed files with 137 additions and 21 deletions
+6 -5
View File
@@ -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 验证。
## 版本与发布
+8
View File
@@ -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
### 新增
+13 -10
View File
@@ -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. 交付说明必须区分:构建验证、打包验证、便携运行验证、自动更新验证和服务器部署验证。
+78 -3
View File
@@ -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)));
}
/// <summary>
/// Velopack 已替换 current、但尚未删除旧 current 备份时执行,抢救旧版本的 config.ini。
/// </summary>
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);
}
}
/// <summary>普通启动时把仍位于 current 的旧配置迁移到 Velopack 持久目录。</summary>
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
@@ -11,6 +11,33 @@ namespace henlo_migration.Common
/// <summary>程序运行根目录</summary>
public static string RootPath => AppDomain.CurrentDomain.BaseDirectory;
/// <summary>
/// 用户配置持久目录。Velopack 更新会整体替换 current 目录,因此安装态/便携态使用其父目录;
/// 普通 dotnet run 或 publish 运行时仍使用程序目录。
/// </summary>
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;
}
}
/// <summary>Oracle 连接配置文件路径</summary>
public static string ConfigPath => Path.Combine(PersistentRootPath, "config.ini");
/// <summary>源数据文件夹(存放 table.txt、bos.txt、prc.txt、data.txt 及各表的 .txt 文件)</summary>
public static string DataPath
{
+1 -1
View File
@@ -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();
+3 -1
View File
@@ -90,7 +90,9 @@ henlo_migration_wpf10/
- **源库**:读取数据的数据库
- **目标库**:写入数据的数据库
连接信息保存在运行目录的 `config.ini` 中。发布包不携带该文件,首次运行会创建空配置。
连接信息保存在用户配置持久目录的 `config.ini` 中。Velopack 安装态或便携态位于与 `Update.exe` 同级的根目录,普通 `dotnet run`/`publish` 运行时位于程序目录。发布包不携带该文件,首次运行会创建空配置。
从旧版本更新时,新程序会在 Velopack 删除旧 `current/` 备份前迁移原 `current/config.ini`;后续更新只替换 `current/`,不会覆盖根目录中的配置。
### 2. 索引文件
@@ -8,7 +8,7 @@
<Nullable>disable</Nullable>
<ApplicationIcon>Resources\favicon.ico</ApplicationIcon>
<Deterministic>true</Deterministic>
<Version>1.0.17</Version>
<Version>1.0.18</Version>
</PropertyGroup>
<ItemGroup>