14 Commits

Author SHA1 Message Date
218e1456f2 将临时目录移动到程序执行位置 2025-08-22 22:55:26 +08:00
734d3bb348 修正文件名称 2025-08-07 04:43:49 +08:00
b294c96687 移除临时文件备份 2025-07-20 00:07:09 +08:00
9084f35b8f 文件移动问题 2025-07-19 18:24:19 +08:00
6d12a89c9c 自身更新 2025-07-19 17:58:38 +08:00
848de6f3fa 修改了结束exe程序的位置 2025-07-18 17:22:23 +08:00
9b790fe9e0 将移动临时文件修改为复制临时文件 2025-07-18 17:10:55 +08:00
f950b05b62 移除清理临时文件的方法 2025-07-18 16:10:22 +08:00
a3bfee2755 7zdll目录修改 2025-07-03 12:46:26 +08:00
qinsi_travel
e3b32ed453 删除配置 2025-07-02 21:27:28 +08:00
qinsi_travel
0891d7534a 恢复密钥 2025-07-02 21:23:36 +08:00
ef9aa7bc5e 优化 2025-07-02 15:50:05 +08:00
9ba1b33ca3 移除添加以管理员身份运行的兼容性,以防windows报毒 2025-07-02 15:03:53 +08:00
96cfa4be48 更换更新进度显示样式 2025-06-30 23:54:52 +08:00
6 changed files with 633 additions and 456 deletions

View File

View File

@@ -137,16 +137,16 @@
<Reference Include="System.Configuration" />
</ItemGroup>
<ItemGroup>
<Compile Include="Form1.cs">
<Compile Include="Update.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="Form1.Designer.cs">
<DependentUpon>Form1.cs</DependentUpon>
<Compile Include="Update.Designer.cs">
<DependentUpon>Update.cs</DependentUpon>
</Compile>
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<EmbeddedResource Include="Form1.resx">
<DependentUpon>Form1.cs</DependentUpon>
<EmbeddedResource Include="Update.resx">
<DependentUpon>Update.cs</DependentUpon>
</EmbeddedResource>
<EmbeddedResource Include="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
@@ -184,22 +184,23 @@
</BootstrapperPackage>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="7z-x86.dll" />
<EmbeddedResource Include="7z-x64.dll" />
<Content Include="7z.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>这台计算机上缺少此项目引用的 NuGet 程序包。使用"NuGet 程序包还原"可下载这些程序包。有关更多信息,请参见 http://go.microsoft.com/fwlink/?LinkID=322105。缺少的文件是 {0}。</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('packages\Fody.6.9.2\build\Fody.targets')" Text="$([System.String]::Format('$(ErrorText)', 'packages\Fody.6.9.2\build\Fody.targets'))" />
<Error Condition="!Exists('packages\Costura.Fody.6.0.0\build\Costura.Fody.props')" Text="$([System.String]::Format('$(ErrorText)', 'packages\Costura.Fody.6.0.0\build\Costura.Fody.props'))" />
<Error Condition="!Exists('packages\Costura.Fody.6.0.0\build\Costura.Fody.targets')" Text="$([System.String]::Format('$(ErrorText)', 'packages\Costura.Fody.6.0.0\build\Costura.Fody.targets'))" />
<Error Condition="!Exists('packages\Fody.6.9.2\build\Fody.targets')" Text="$([System.String]::Format('$(ErrorText)', 'packages\Fody.6.9.2\build\Fody.targets'))" />
<Error Condition="!Exists('packages\SevenZipSharp.Interop.19.1.0\build\SevenZipSharp.Interop.targets')" Text="$([System.String]::Format('$(ErrorText)', 'packages\SevenZipSharp.Interop.19.1.0\build\SevenZipSharp.Interop.targets'))" />
<Error Condition="!Exists('packages\SevenZipExtractor.1.0.19\build\SevenZipExtractor.targets')" Text="$([System.String]::Format('$(ErrorText)', 'packages\SevenZipExtractor.1.0.19\build\SevenZipExtractor.targets'))" />
<!-- <Error Condition="!Exists('packages\SevenZipSharp.Interop.19.1.0\build\SevenZipSharp.Interop.targets')" Text="$([System.String]::Format('$(ErrorText)', 'packages\SevenZipSharp.Interop.19.1.0\build\SevenZipSharp.Interop.targets'))" /> -->
<!-- <Error Condition="!Exists('packages\SevenZipExtractor.1.0.19\build\SevenZipExtractor.targets')" Text="$([System.String]::Format('$(ErrorText)', 'packages\SevenZipExtractor.1.0.19\build\SevenZipExtractor.targets'))" /> -->
</Target>
<Import Project="packages\Costura.Fody.6.0.0\build\Costura.Fody.targets" Condition="Exists('packages\Costura.Fody.6.0.0\build\Costura.Fody.targets')" />
<Import Project="packages\Fody.6.9.2\build\Fody.targets" Condition="Exists('packages\Fody.6.9.2\build\Fody.targets')" />
<Import Project="packages\SevenZipSharp.Interop.19.1.0\build\SevenZipSharp.Interop.targets" Condition="Exists('packages\SevenZipSharp.Interop.19.1.0\build\SevenZipSharp.Interop.targets')" />
<Import Project="packages\SevenZipExtractor.1.0.19\build\SevenZipExtractor.targets" Condition="Exists('packages\SevenZipExtractor.1.0.19\build\SevenZipExtractor.targets')" />
<Import Project="packages\Costura.Fody.6.0.0\build\Costura.Fody.targets" Condition="Exists('packages\Costura.Fody.6.0.0\build\Costura.Fody.targets')" />
<!-- <Import Project="packages\SevenZipSharp.Interop.19.1.0\build\SevenZipSharp.Interop.targets" Condition="Exists('packages\SevenZipSharp.Interop.19.1.0\build\SevenZipSharp.Interop.targets')" /> -->
<!-- <Import Project="packages\SevenZipExtractor.1.0.19\build\SevenZipExtractor.targets" Condition="Exists('packages\SevenZipExtractor.1.0.19\build\SevenZipExtractor.targets')" /> -->
</Project>

130
README.md
View File

@@ -32,8 +32,8 @@
## 用户界面
- **实时进度显示**:通过进度条、已完成数量/总数、下载速度等信息,清晰地展示更新进度
- **简洁的状态反馈**:界面只显示当前正在处理的文件名等核心信息,避免被冗长的日志刷屏。
- **实时进度显示**:通过进度条、已完成数量/总数以及 **已下载总量(实时速度)** 的形式清晰地展示更新进度。已下载总量会包含已存在于临时目录的有效文件,速度则只计算运行时下载的部分
- **简洁的状态反馈**:界面会以 `下载:文件名...` 的格式显示当前任务,过长的文件名会被自动截断,避免信息刷屏。
- **友好的错误提示**:当发生严重错误时,会弹出简明扼要的错误信息窗口,而不是难以理解的完整堆栈跟踪。
- **自动定位与退出**:窗体启动时会自动停靠在屏幕右下角,更新完成后会自动关闭,对用户干扰极小。
@@ -53,45 +53,43 @@
## 🚀 主要功能
### 📥 多源下载支持
- **123盘云存储**: 支持主备域名自动切换,提供高可用性下载服务
- **阿里云OSS**: 备用下载源,确保文件下载的可靠性
- **智能DNS解析**: 使用多个DNS服务器提高域名解析成功率
- **123盘云存储**: (UID: `1826795402`, Path: `/1826795402/KeyAuth`) 支持主备域名自动切换,提供高可用性下载服务
- **阿里云OSS**: 备用下载源,确保文件下载的可靠性
- **智能DNS解析**: 使用多个DNS服务器提高域名解析成功率
### 🔄 智能更新机制
- **MD5完整性验证**: 下载前后进行MD5校验确保文件完整性
- **增量更新**: 仅下载已变更的文件,节省带宽和时间
- **断点续传**: 支持网络中断后继续下载,提高下载成功率
- **并发下载**: 可配置的多线程并发下载,显著提升下载速度
- **MD5完整性验证**: 下载前后进行MD5校验确保文件完整性
- **增量更新**: 仅下载已变更的文件,节省带宽和时间
- **断点续传支持**: 通过检查本地临时文件实现若文件已存在且MD5匹配则跳过下载。
- **并发下载**: 可配置的多线程并发下载,显著提升下载速度
### 🛡️ 文件处理与安全
- **文件占用处理**: 智能检测并处理被占用的文件
- **批处理脚本**: 为被占用文件创建延迟替换脚本
- **临时文件管理**: 自动清理临时文件,保持系统整洁
- **路径智能识别**: 基于MD5数据自动识别项目基准目录
- **文件占用处理**: 智能检测并处理被占用的文件,并创建延迟替换脚本。
- **临时文件管理**: 自动清理临时文件,保持系统整洁。
- **路径智能识别**: 基于MD5数据自动识别项目基准目录。
### 📦 自动解压功能
- **7z格式支持**: 内置7z解压引擎支持多种压缩格式
- **自动权限设置**: 解压后自动为exe文件设置管理员运行权限
- **覆盖解压**: 支持覆盖模式解压,确保文件更新
- **多架构兼容**: 自动选择32位/64位解压库适配不同运行环境
- **7z格式支持**: 内置7z解压引擎支持多种压缩格式
- **自动权限设置**: 解压后自动为exe文件设置管理员运行权限
- **多架构兼容**: 自动选择32位/64位解压库适配不同运行环境。
### 🔧 错误处理与重试
- **多次重试机制**: 下载失败自动重试,可配置重试次数
- **异常处理**: 完善的异常捕获和处理机制
- **状态实时显示**: 实时显示下载进度、状态和错误信息
- **多次重试机制**: 下载失败自动重试,可配置重试次数
- **异常处理**: 完善的异常捕获和处理机制
- **状态实时显示**: 实时显示下载进度、状态和错误信息
## 🏗️ 技术架构
### 核心组件
- **.NET Framework 4.7.2**: 基于稳定的.NET Framework构建
- **异步编程**: 全面采用async/await模式确保UI响应性
- **多线程下载**: 使用SemaphoreSlim控制并发数量
- **资源嵌入**: 将依赖库嵌入程序,实现单文件部署
- **.NET Framework**: 基于稳定的.NET Framework构建
- **异步编程**: 全面采用async/await模式确保UI响应性
- **多线程下载**: 使用SemaphoreSlim控制并发数量
- **资源嵌入**: 将依赖库嵌入程序,实现单文件部署
### 依赖库
- **Aliyun.OSS.SDK**: 阿里云对象存储服务支持
- **Newtonsoft.Json**: JSON数据处理
- **SevenZipExtractor**: 7z压缩文件解压支持
- **Aliyun.OSS.SDK**: 阿里云对象存储服务支持
- **Newtonsoft.Json**: JSON数据处理
- **SevenZipExtractor**: 7z压缩文件解压支持
## 📋 配置说明
@@ -115,79 +113,29 @@
"version": "1.0.0",
"data": {
"program.exe": "5d41402abc4b2a76b9719d911017c592",
"lib/library.dll": "098f6bcd4621d373cade4e832627b4f6",
"config/settings.ini": "5e40d4c123456789abcdef1234567890"
"lib/library.dll": "098f6bcd4621d373cade4e832627b4f6"
}
}
```
## 🔄 工作流程
1. **启动检查**: 程序启动时清理旧的临时文件
2. **下载MD5**: 从云存储下载最新的MD5文件
3. **文件比较**: 对比本地文件与在线MD5识别需要更新的文件
4. **并发下载**: 使用多线程下载需要更新的文件
5. **完整性验证**: 验证下载文件的MD5值
6. **文件替换**: 将新文件移动到目标位置
7. **7z解压**: 自动检测并解压tim.7z文件
8. **权限设置**: 为解压的exe文件设置管理员运行权限
9. **清理完成**: 清理临时文件,显示完成状态
1. **启动检查**: 程序启动时清理旧的更新文件 (`.new` 后缀)。
2. **下载MD5**: 从云存储下载最新的MD5文件
3. **文件比较**: 对比本地文件与在线MD5识别需要更新的文件
4. **并发下载**: 使用多线程下载需要更新的文件到临时目录。
5. **完整性验证**: 验证下载文件的MD5值
6. **文件替换**: 将新文件移动到目标位置,对被占用文件创建替换脚本。
7. **7z解压**: 自动检测并解压`tim.7z`文件(如果存在)。
8. **权限设置**: 为解压的exe文件设置管理员运行权限
9. **清理完成**: 清理临时目录,显示完成状态后退出。
## 🎯 使用场景
- **软件自动更新**: 为桌面应用程序提供自动更新功能
- **文件同步**: 在不同设备间同步文件和配置
- **批量部署**: 企业环境下的软件批量部署和更新
- **游戏更新**: 游戏客户端的增量更新和补丁分发
## 🛠️ 开发特性
### 网络优化
- **静态HttpClient**: 避免套接字耗尽问题
- **连接池复用**: 提高网络请求效率
- **超时控制**: 合理的超时设置,避免长时间等待
### 内存管理
- **流式处理**: 大文件下载使用流式处理,控制内存占用
- **及时释放**: 及时释放不再使用的资源
- **临时文件**: 合理使用临时文件,避免内存溢出
### 用户体验
- **进度显示**: 实时显示下载进度和文件信息
- **状态反馈**: 详细的状态信息和错误提示
- **窗口定位**: 智能定位到屏幕右下角,不影响用户操作
## 📈 性能特点
- **高效下载**: 多线程并发下载,充分利用网络带宽
- **智能重试**: 失败文件智能重试,提高成功率
- **资源节约**: 仅下载变更文件,节省网络流量
- **快速启动**: 优化的启动流程,快速进入工作状态
## 🔒 安全性
- **MD5验证**: 确保文件在传输过程中没有被篡改
- **用户权限**: 在当前用户权限下操作,避免权限滥用
- **临时目录**: 使用用户临时目录,避免权限问题
- **异常处理**: 完善的异常处理,避免程序崩溃
## 📝 版本历史
### 最新版本特性
- ✅ 添加7z自动解压功能
- ✅ 支持解压后程序自动设置管理员权限
- ✅ 优化多线程下载性能
- ✅ 增强错误处理和重试机制
- ✅ 改进用户界面和状态显示
- ✅ 过滤 .db / .db3 数据库文件,避免占用导致失败
- ✅ 更新列表或压缩包中出现 tim.dll 时,自动结束 tim.exe 后替换
- ✅ 所有文件匹配与进程检测均采用大小写不敏感逻辑,避免大小写差异造成的更新失败
- ✅ 精简 Status_Box 文本,并在失败时弹窗展示核心错误信息
## 🤝 技术支持
如需技术支持或报告问题,请联系开发团队。
- **软件自动更新**: 为桌面应用程序提供自动更新功能
- **文件同步**: 在不同设备间同步文件和配置
- **游戏更新**: 游戏客户端的增量更新和补丁分发。
---
**注意**: 本程序需要网络连接以下载更新文件。首次运行时可能需要较长时间来下载必要的文件。
**注意**: 本程序需要网络连接以下载更新文件。首次运行时可能需要较长时间来下载所有必要的文件。

View File

@@ -28,22 +28,12 @@
/// </summary>
private void InitializeComponent()
{
this.Update_Text = new System.Windows.Forms.Label();
this.Update_Pro = new System.Windows.Forms.ProgressBar();
this.Status_Box = new System.Windows.Forms.Label();
this.Count_Box = new System.Windows.Forms.Label();
this.Size_Box = new System.Windows.Forms.Label();
this.SuspendLayout();
//
// Update_Text
//
this.Update_Text.AutoSize = true;
this.Update_Text.Location = new System.Drawing.Point(12, 12);
this.Update_Text.Name = "Update_Text";
this.Update_Text.Size = new System.Drawing.Size(59, 12);
this.Update_Text.TabIndex = 0;
this.Update_Text.Text = "更新状态:";
//
// Update_Pro
//
this.Update_Pro.Location = new System.Drawing.Point(12, 36);
@@ -53,10 +43,9 @@
//
// Status_Box
//
this.Status_Box.AutoSize = false;
this.Status_Box.Location = new System.Drawing.Point(72, 12);
this.Status_Box.Location = new System.Drawing.Point(12, 12);
this.Status_Box.Name = "Status_Box";
this.Status_Box.Size = new System.Drawing.Size(94, 12);
this.Status_Box.Size = new System.Drawing.Size(148, 12);
this.Status_Box.TabIndex = 2;
this.Status_Box.Text = "...";
//
@@ -86,7 +75,6 @@
this.Controls.Add(this.Count_Box);
this.Controls.Add(this.Status_Box);
this.Controls.Add(this.Update_Pro);
this.Controls.Add(this.Update_Text);
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.None;
this.MaximizeBox = false;
this.MinimizeBox = false;
@@ -96,13 +84,10 @@
this.TopMost = true;
this.Load += new System.EventHandler(this.Update_Load);
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.Label Update_Text;
private System.Windows.Forms.ProgressBar Update_Pro;
private System.Windows.Forms.Label Status_Box;
private System.Windows.Forms.Label Count_Box;

View File

@@ -75,15 +75,40 @@ namespace CheckDownload
// 基准目录路径(用于文件更新的目标目录)
private string _baseDirectory;
// 7z.dll 库文件路径
private readonly string _sevenZipDllPath;
// 应用程序名称
private readonly string _appName;
// 当前进程ID
private readonly int _currentProcessId;
// === 新增: 用于显示整体下载大小与速度 ===
private long _totalDownloadedBytes = 0; // 已下载总字节数
private DateTime _downloadStartTime; // 下载开始时间
private DateTime _lastSpeedUpdateTime; // 上一次更新速度的时间
private readonly object _speedLock = new object(); // 锁,用于多线程更新 UI
private long _bytesSinceLastSpeedCalc = 0; // 距离上次速度计算新增的字节数
// 更新锁文件路径
private string _updateLockFilePath;
/// <summary>
/// 初始化窗体
/// </summary>
public Update()
{
InitializeComponent();
// 设置 7z.dll 的路径为程序运行目录
_sevenZipDllPath = Path.Combine(Application.StartupPath, "7z.dll");
_appName = Assembly.GetExecutingAssembly().GetName().Name;
_currentProcessId = Process.GetCurrentProcess().Id;
_baseDirectory = Application.StartupPath;
_updateLockFilePath = Path.Combine(_baseDirectory, "update.lock");
ConfigureProgressBar();
InitializeTempDirectory();
}
/// <summary>
@@ -127,21 +152,13 @@ namespace CheckDownload
/// <param name="message">状态消息</param>
private void UpdateStatus(string message)
{
// 只保留冒号后的简短信息(一般是文件名);若无冒号则原样显示
string display = message;
int idx = message.LastIndexOf(':');
if (idx >= 0 && idx < message.Length - 1)
{
display = message.Substring(idx + 1).Trim();
}
if (this.InvokeRequired)
{
this.Invoke((MethodInvoker)delegate { Status_Box.Text = display; });
this.Invoke((MethodInvoker)delegate { Status_Box.Text = message; });
}
else
{
Status_Box.Text = display;
Status_Box.Text = message;
}
}
@@ -189,6 +206,26 @@ namespace CheckDownload
}
finally
{
// 删除更新锁文件,让批处理脚本知道程序已退出
CleanupLockFile();
}
}
/// <summary>
/// 清理更新锁文件
/// </summary>
private void CleanupLockFile()
{
try
{
if (!string.IsNullOrEmpty(_updateLockFilePath) && File.Exists(_updateLockFilePath))
{
File.Delete(_updateLockFilePath);
}
}
catch
{
// 忽略清理锁文件时的错误
}
}
@@ -208,6 +245,7 @@ namespace CheckDownload
{
try
{
InitializeTempDirectory();
CleanupNewFiles();
UpdateStatus("下载在线MD5文件并读取...");
UpdateCount("");
@@ -247,7 +285,7 @@ namespace CheckDownload
UpdateProgressValue(100);
// 无需更新时清理临时文件夹
CleanupTempDirectory();
//CleanupTempDirectory();
// 显示更新完成并等待2秒
UpdateStatus("更新完成");
@@ -270,6 +308,13 @@ namespace CheckDownload
_totalCount = orderedFileList.Count;
_completedCount = 0;
_downloadedFiles.Clear();
// === 新增: 初始化速度统计 ===
_totalDownloadedBytes = 0;
_downloadStartTime = DateTime.UtcNow;
_lastSpeedUpdateTime = _downloadStartTime;
_bytesSinceLastSpeedCalc = 0;
var failedFiles = new ConcurrentDictionary<string, string>();
await PerformDownloads(orderedFileList, failedFiles);
@@ -290,7 +335,7 @@ namespace CheckDownload
if (_completedCount == 0 && orderedFileList.Count > 0)
{
throw new Exception("所有文件下载失败。");
throw new Exception("UpdateFile: 所有文件下载失败。");
}
await VerifyAndSaveAllFiles();
@@ -298,7 +343,7 @@ namespace CheckDownload
await DecompressTim7zAsync();
// 校验和保存成功后清理临时目录
CleanupTempDirectory();
//CleanupTempDirectory();
// 显示完成状态并退出
UpdateStatus("更新完成");
@@ -308,8 +353,7 @@ namespace CheckDownload
}
catch (Exception ex)
{
UpdateStatus("更新失败");
MessageBox.Show($"更新失败:\n{ex.ToString()}", "Update Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
HandleError("文件更新主流程 (UpdateFile)", ex);
await Task.Delay(3000);
this.Close();
}
@@ -364,8 +408,7 @@ namespace CheckDownload
}
catch (Exception ex) when (ex is OssException || ex is JsonException)
{
UpdateStatus($"读取在线MD5文件失败: {ex.Message}");
return (null, null, null);
throw new Exception($"ReadOnlineMd5File: 读取或解析在线MD5文件 '{filePath}' 失败。", ex);
}
}
@@ -447,37 +490,6 @@ namespace CheckDownload
return differences;
}
/// <summary>
/// 批量下载文件列表并进行MD5验证支持重试机制
/// </summary>
/// <param name="fileList">包含文件路径和期望MD5值的字典</param>
private async Task DownloadAndVerifyFiles(Dictionary<string, string> fileList)
{
_totalCount = fileList.Count;
_completedCount = 0;
_downloadedFiles.Clear();
var failedFiles = new ConcurrentDictionary<string, string>();
await PerformDownloads(fileList, failedFiles);
if (!failedFiles.IsEmpty)
{
UpdateStatus($"有 {failedFiles.Count} 个文件下载失败,开始重试...");
var stillFailing = await RetryFailedFilesAsync(new Dictionary<string, string>(failedFiles));
if (stillFailing.Any())
{
UpdateStatus($"重试后仍有 {stillFailing.Count} 个文件下载失败。");
}
}
if (_completedCount == 0 && fileList.Count > 0)
{
throw new Exception("所有文件下载失败。");
}
await VerifyAndSaveAllFiles();
}
/// <summary>
/// 使用信号量控制并发数量,执行多个文件的并发下载任务
/// </summary>
@@ -503,7 +515,12 @@ namespace CheckDownload
int progress = (int)((double)_completedCount / _totalCount * 95);
UpdateProgressValue(progress);
UpdateStatus($"{Path.GetFileName(file.Key)}");
string fileName = Path.GetFileName(file.Key);
string truncatedFileName = TruncateString(fileName, 10);
if (!string.IsNullOrWhiteSpace(truncatedFileName))
{
UpdateStatus($"下载:{truncatedFileName}");
}
UpdateCount($"{_completedCount}/{_totalCount}");
}
else
@@ -527,14 +544,9 @@ namespace CheckDownload
/// <returns>下载成功返回true否则返回false</returns>
private async Task<bool> AttemptDownloadAsync(string filePath, string expectedMd5)
{
// kill exe if running
if (Path.GetExtension(filePath).Equals(".exe", StringComparison.OrdinalIgnoreCase))
{
KillProcessIfRunning(filePath);
}
string tempFilePath = Path.Combine(_tempDirectory, filePath);
string fileName = Path.GetFileName(filePath);
string truncatedFileName = TruncateString(fileName, 10);
try
{
@@ -543,14 +555,20 @@ namespace CheckDownload
return true;
}
UpdateStatus($"{fileName}");
if (!string.IsNullOrWhiteSpace(truncatedFileName))
{
UpdateStatus($"下载:{truncatedFileName}");
}
UpdateCount($"{_completedCount + 1}/{_totalCount}");
if (await DownloadFileFromOneDrive(filePath, expectedMd5, tempFilePath))
{
return true;
}
UpdateStatus($"{fileName}");
if (!string.IsNullOrWhiteSpace(truncatedFileName))
{
UpdateStatus($"下载:{truncatedFileName}");
}
UpdateCount($"{_completedCount + 1}/{_totalCount}");
string ossKey = $"File/{expectedMd5}";
@@ -570,7 +588,10 @@ namespace CheckDownload
}
catch (Exception ex) when (ex is OssException || ex is WebException)
{
UpdateStatus($"{fileName}");
if (!string.IsNullOrWhiteSpace(truncatedFileName))
{
UpdateStatus($"下载:{truncatedFileName}");
}
UpdateCount($"{_completedCount + 1}/{_totalCount}");
UpdateSize("");
string ossKey = $"File/{expectedMd5}";
@@ -578,7 +599,10 @@ namespace CheckDownload
}
catch (Exception ex)
{
UpdateStatus($"下载异常: {fileName} - {ex.Message}");
if (!string.IsNullOrWhiteSpace(truncatedFileName))
{
UpdateStatus($"下载异常: {truncatedFileName} - {ex.Message}");
}
return false;
}
}
@@ -596,28 +620,48 @@ namespace CheckDownload
{
if (!File.Exists(tempFilePath))
{
//UpdateStatus($"[调试] 临时文件不存在: {fileName}");
return false;
}
UpdateStatus($"检查已存在的临时文件: {fileName}");
if (!string.IsNullOrWhiteSpace(fileName))
{
UpdateStatus($"检查已存在的临时文件: {fileName}");
}
string actualMd5 = await Task.Run(() => CalculateMD5FromFile(tempFilePath));
if (actualMd5.Equals(expectedMd5, StringComparison.OrdinalIgnoreCase))
{
UpdateStatus($"临时文件完整,跳过下载: {fileName}");
if (!string.IsNullOrWhiteSpace(fileName))
{
UpdateStatus($"临时文件完整,跳过下载: {fileName}");
}
// === 新增: 将已存在的有效临时文件大小计入总下载量 ===
try
{
var fileInfo = new FileInfo(tempFilePath);
Interlocked.Add(ref _totalDownloadedBytes, fileInfo.Length);
//UpdateStatus($"[调试] 临时文件验证通过,大小: {fileInfo.Length} bytes - {fileName}");
}
catch
{
// 忽略获取文件大小的错误
}
return true;
}
else
{
UpdateStatus($"临时文件不完整,重新下载: {fileName}");
File.Delete(tempFilePath);
return false;
}
}
catch (Exception ex)
{
UpdateStatus($"检查临时文件时出错,将重新下载: {fileName} - {ex.Message}");
if (!string.IsNullOrWhiteSpace(fileName))
{
UpdateStatus($"临时文件异常,删除文件: {fileName} - {ex.Message}");
}
try
{
if (File.Exists(tempFilePath))
@@ -654,7 +698,6 @@ namespace CheckDownload
UpdateSize("");
var request = new HttpRequestMessage(HttpMethod.Get, authUrl) { Version = HttpVersion.Version11 };
request.Headers.Add("User-Agent", "CheckDownload/1.0");
using (var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead))
{
@@ -697,7 +740,6 @@ namespace CheckDownload
UpdateSize("");
var request = new HttpRequestMessage(HttpMethod.Get, authUrl) { Version = HttpVersion.Version11 };
request.Headers.Add("User-Agent", "CheckDownload/1.0");
using (var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead))
{
@@ -743,7 +785,6 @@ namespace CheckDownload
string authUrl = GenerateAuthUrl($"http://{OneDriveMainDomain}{OneDrivePath}", fileName);
var request = new HttpRequestMessage(HttpMethod.Get, authUrl) { Version = HttpVersion.Version11 };
request.Headers.Add("User-Agent", "CheckDownload/1.0");
using (var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead))
{
@@ -772,7 +813,6 @@ namespace CheckDownload
string authUrl = GenerateAuthUrl($"http://{OneDriveBackupDomain}{OneDrivePath}", fileName);
var request = new HttpRequestMessage(HttpMethod.Get, authUrl) { Version = HttpVersion.Version11 };
request.Headers.Add("User-Agent", "CheckDownload/1.0");
using (var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead))
{
@@ -843,7 +883,6 @@ namespace CheckDownload
{
var requestUri = $"http://{dnsServer}/resolve?name={domain}&type=1&short=1";
var request = new HttpRequestMessage(HttpMethod.Get, requestUri) { Version = HttpVersion.Version11 };
request.Headers.Add("User-Agent", "CheckDownload/1.0");
request.Headers.Host = dnsServer;
var response = await _httpClient.SendAsync(request);
@@ -876,6 +915,10 @@ namespace CheckDownload
UpdateStatus("正在校验文件...");
UpdateCount("");
UpdateSize("");
// 新增:在开始校验前创建临时文件备份
//await CreateTempFileBackup();
var failedFiles = new ConcurrentBag<string>();
var filesForScripting = new ConcurrentBag<(string original, string newFile)>();
@@ -892,9 +935,18 @@ namespace CheckDownload
string expectedMd5 = item.Value;
string tempFilePath = Path.Combine(_tempDirectory, relativePath);
// 添加调试信息:检查临时文件是否存在
if (!File.Exists(tempFilePath))
{
//UpdateStatus($"[调试] 临时文件在校验前消失: {relativePath}");
failedFiles.Add(relativePath);
return;
}
string actualMd5 = CalculateMD5FromFile(tempFilePath);
if (actualMd5 != expectedMd5.ToLower())
{
//UpdateStatus($"[调试] 文件MD5校验失败: {relativePath}");
failedFiles.Add(relativePath);
return;
}
@@ -906,13 +958,24 @@ namespace CheckDownload
Directory.CreateDirectory(localDir);
}
if (!await TryMoveFileAsync(tempFilePath, localPath))
//UpdateStatus($"[调试] 开始复制文件: {relativePath}");
if (!await TryCopyFileAsync(tempFilePath, localPath)) // 改为使用复制方法
{
//UpdateStatus($"[调试] 直接复制失败,创建.new文件: {relativePath}");
string backupPath = localPath + ".new";
File.Move(tempFilePath, backupPath);
File.Copy(tempFilePath, backupPath, true); // 复制而非移动
filesForScripting.Add((localPath, backupPath));
}
else
{
//UpdateStatus($"[调试] 文件复制成功: {relativePath}");
}
// 复制后再次检查临时文件是否还存在
if (!File.Exists(tempFilePath))
{
//UpdateStatus($"[调试] 警告:文件复制后临时文件消失: {relativePath}");
}
}
finally
{
@@ -937,7 +1000,9 @@ namespace CheckDownload
if (failedFiles.Count > 0)
{
throw new Exception($"{failedFiles.Count}个文件校验失败");
string failedFileList = string.Join(", ", failedFiles.Take(3));
if (failedFiles.Count > 3) failedFileList += "...";
throw new Exception($"VerifyAndSaveAllFiles: {failedFiles.Count}个文件校验失败: {failedFileList}");
}
else
{
@@ -998,67 +1063,154 @@ namespace CheckDownload
{
string batchFilePath = Path.Combine(_baseDirectory, "update_files.bat");
string processId = Process.GetCurrentProcess().Id.ToString();
string processName = Process.GetCurrentProcess().ProcessName;
// 创建锁文件,程序退出前会删除它
File.WriteAllText(_updateLockFilePath, processId);
var batchContent = new StringBuilder();
batchContent.AppendLine("@echo off");
batchContent.AppendLine("chcp 65001 > nul");
batchContent.AppendLine(":check_process");
batchContent.AppendLine($"tasklist /FI \"PID eq {processId}\" 2>NUL | find /I \"{processId}\" >NUL");
batchContent.AppendLine("if %ERRORLEVEL% == 0 (");
batchContent.AppendLine(" timeout /t 1 /nobreak > NUL");
batchContent.AppendLine(" goto check_process");
batchContent.AppendLine("title Update Process");
// 简化的等待逻辑,避免复杂的计数器
batchContent.AppendLine("echo 等待程序退出...");
batchContent.AppendLine(":wait_loop");
// 优先检查锁文件
batchContent.AppendLine($"if not exist \"{_updateLockFilePath}\" goto process_check");
// 等待0.5秒
batchContent.AppendLine("ping 127.0.0.1 -n 1 -w 500 >nul");
batchContent.AppendLine("goto wait_loop");
batchContent.AppendLine(":process_check");
// 再次确认进程已退出
batchContent.AppendLine($"tasklist /FI \"PID eq {processId}\" 2>nul | find /I \"{processId}\" >nul");
batchContent.AppendLine("if %ERRORLEVEL% equ 0 (");
batchContent.AppendLine(" ping 127.0.0.1 -n 1 -w 500 >nul");
batchContent.AppendLine(" goto process_check");
batchContent.AppendLine(")");
batchContent.AppendLine("timeout /t 2 /nobreak > NUL");
// 额外等待确保完全退出
batchContent.AppendLine("ping 127.0.0.1 -n 1 -w 1000 >nul");
batchContent.AppendLine("echo 开始更新文件...");
// 简化的文件处理逻辑,避免复杂的标签
int fileIndex = 0;
foreach (var file in files)
{
batchContent.AppendLine($"del \"{file.original}\" /f /q");
batchContent.AppendLine($"move \"{file.newFile}\" \"{file.original}\"");
string fileName = Path.GetFileName(file.original);
batchContent.AppendLine($"echo 处理文件: {fileName}");
// 检查新文件是否存在
batchContent.AppendLine($"if not exist \"{file.newFile}\" (");
batchContent.AppendLine($" echo 错误: 新文件不存在 - {fileName}");
batchContent.AppendLine($" goto file_{fileIndex}_end");
batchContent.AppendLine($")");
// 删除原文件(如果存在)
batchContent.AppendLine($"if exist \"{file.original}\" (");
batchContent.AppendLine($" echo 删除原文件: {fileName}");
batchContent.AppendLine($" del \"{file.original}\" /f /q");
batchContent.AppendLine($")");
// 复制新文件
batchContent.AppendLine($"echo 复制新文件: {fileName}");
batchContent.AppendLine($"copy /Y \"{file.newFile}\" \"{file.original}\"");
// 验证复制是否成功
batchContent.AppendLine($"if exist \"{file.original}\" (");
batchContent.AppendLine($" echo 成功更新: {fileName}");
batchContent.AppendLine($" del \"{file.newFile}\" /f /q");
batchContent.AppendLine($") else (");
batchContent.AppendLine($" echo 失败: 无法创建 {fileName}");
batchContent.AppendLine($")");
batchContent.AppendLine($":file_{fileIndex}_end");
fileIndex++;
}
// 清理工作
batchContent.AppendLine("echo 清理临时文件...");
// 删除锁文件
batchContent.AppendLine($"if exist \"{_updateLockFilePath}\" del \"{_updateLockFilePath}\" /f /q");
// 简化的清理逻辑
batchContent.AppendLine($"cd /d \"{_baseDirectory}\"");
batchContent.AppendLine("for %%f in (*.new) do del \"%%f\" /f /q");
batchContent.AppendLine("for %%f in (*.bak) do del \"%%f\" /f /q");
batchContent.AppendLine("echo 更新完成");
batchContent.AppendLine("ping 127.0.0.1 -n 1 -w 2000 >nul");
// 删除批处理脚本自身
batchContent.AppendLine("del \"%~f0\" /f /q");
batchContent.AppendLine("exit");
File.WriteAllText(batchFilePath, batchContent.ToString(), new UTF8Encoding(false));
// 使用Win7兼容的启动方式
var startInfo = new ProcessStartInfo
{
FileName = "cmd.exe",
Arguments = $"/c \"{batchFilePath}\"",
CreateNoWindow = true,
UseShellExecute = false,
WindowStyle = ProcessWindowStyle.Hidden
FileName = batchFilePath,
CreateNoWindow = false, // Win7下设为false更稳定
UseShellExecute = true, // Win7下必须使用Shell执行
WindowStyle = ProcessWindowStyle.Minimized,
WorkingDirectory = _baseDirectory
};
Process.Start(startInfo);
try
{
Process.Start(startInfo);
}
catch (Exception)
{
// 如果上面的方式失败尝试直接用cmd执行
var fallbackStartInfo = new ProcessStartInfo
{
FileName = "cmd.exe",
Arguments = $"/c \"{batchFilePath}\"",
CreateNoWindow = true,
UseShellExecute = true,
WindowStyle = ProcessWindowStyle.Hidden,
WorkingDirectory = _baseDirectory
};
Process.Start(fallbackStartInfo);
}
UpdateStatus("已创建更新脚本,程序退出后将自动完成更新");
}
catch (Exception ex)
{
UpdateStatus($"创建替换脚本时出错: {ex.Message}");
HandleError("创建文件替换脚本 (CreateReplaceScriptForAll)", ex);
}
}
/// <summary>
/// 初始化程序使用的临时目录,用于存储下载过程中的临时文件
/// </summary>
private void InitializeTempDirectory()
{
try
{
_tempDirectory = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"Temp",
"CheckDownload"
);
if (!Directory.Exists(_tempDirectory))
{
Directory.CreateDirectory(_tempDirectory);
}
}
catch (Exception ex)
{
UpdateStatus($"初始化临时目录失败: {ex.Message}");
}
private void InitializeTempDirectory()
{
try
{
string exeDir = AppDomain.CurrentDomain.BaseDirectory;
_tempDirectory = Path.Combine(exeDir, "Temp");
if (File.Exists(_tempDirectory))
{
throw new IOException($"无法创建目录,因为已存在同名文件: {_tempDirectory}");
}
if (!Directory.Exists(_tempDirectory))
{
Directory.CreateDirectory(_tempDirectory);
}
}
catch (Exception ex)
{
throw new Exception("初始化临时目录 (InitializeTempDirectory)", ex);
}
}
/// <summary>
@@ -1076,20 +1228,6 @@ namespace CheckDownload
}
}
/// <summary>
/// 计算字节数组的MD5哈希值返回32位小写十六进制字符串
/// </summary>
/// <param name="data">要计算哈希值的字节数组</param>
/// <returns>32位小写十六进制MD5哈希值</returns>
private string CalculateMD5(byte[] data)
{
using (var md5 = MD5.Create())
{
byte[] hashBytes = md5.ComputeHash(data);
return BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant();
}
}
/// <summary>
/// 从阿里云OSS下载文件包含SDK直连和IP直连两种备用方案
/// </summary>
@@ -1314,7 +1452,7 @@ namespace CheckDownload
string tempPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"Temp",
"CheckDownload"
_appName
);
if (Directory.Exists(tempPath))
@@ -1496,10 +1634,21 @@ namespace CheckDownload
{
await localStream.WriteAsync(buffer, 0, bytesRead);
totalRead += bytesRead;
if (totalBytes.HasValue)
// === 新增: 统计整体下载量 ===
Interlocked.Add(ref _totalDownloadedBytes, bytesRead);
Interlocked.Add(ref _bytesSinceLastSpeedCalc, bytesRead);
// === 修改: 显示整体下载进度和速度(每 500ms 更新一次,避免频繁刷新) ===
if (DateTime.UtcNow - _lastSpeedUpdateTime > TimeSpan.FromMilliseconds(500))
{
string progressText = $"{FormatBytes(totalRead)}/{FormatBytes(totalBytes.Value)}";
UpdateSize(progressText);
lock (_speedLock)
{
if (DateTime.UtcNow - _lastSpeedUpdateTime > TimeSpan.FromMilliseconds(500))
{
UpdateOverallSize();
}
}
}
}
}
@@ -1521,7 +1670,7 @@ namespace CheckDownload
dblSByte = bytes / 1024.0;
}
}
return $"{dblSByte:0.##}{suffixes[i]}";
return $"{dblSByte:0.0}{suffixes[i]}";
}
/// <summary>
@@ -1540,22 +1689,17 @@ namespace CheckDownload
await Task.Run(() => {
try
{
string sevenZipDllPath = Extract7zDll();
if (string.IsNullOrEmpty(sevenZipDllPath))
{
throw new Exception("无法提取7z.dll解压中止。");
}
string extractionPath = Path.GetDirectoryName(sevenZipFile);
string tempExtractionDir = Path.Combine(_tempDirectory, Path.GetFileNameWithoutExtension(sevenZipFile) + "_temp");
if (Directory.Exists(tempExtractionDir))
{
Directory.Delete(tempExtractionDir, true);
}
Directory.CreateDirectory(tempExtractionDir);
using (var archiveFile = new ArchiveFile(sevenZipFile, sevenZipDllPath))
// 使用 SevenZipExtractor 解压文件到临时目录
using (var archiveFile = new ArchiveFile(sevenZipFile, _sevenZipDllPath))
{
archiveFile.Extract(tempExtractionDir, true);
}
@@ -1579,17 +1723,10 @@ namespace CheckDownload
}
Directory.Delete(tempExtractionDir, true);
UpdateStatus("为解压的程序设置管理员权限...");
var exeFiles = Directory.GetFiles(extractionPath, "*.exe", SearchOption.AllDirectories);
foreach (var exeFile in exeFiles)
{
SetRunAsAdminCompatibility(exeFile);
}
}
catch (Exception ex)
{
throw new Exception($"解压失败: {ex.Message}");
throw new Exception("DecompressTim7zAsync: 解压失败。", ex);
}
});
@@ -1599,8 +1736,7 @@ namespace CheckDownload
}
catch (Exception ex)
{
UpdateStatus($"处理 tim.7z 时出错: {ex.Message}");
await Task.Delay(3000);
throw new Exception("DecompressTim7zAsync: 处理 tim.7z 时出错。", ex);
}
}
@@ -1608,69 +1744,6 @@ namespace CheckDownload
/// 为指定程序路径在注册表中设置"以管理员身份运行"的兼容性标志。
/// </summary>
/// <param name="exePath">要设置的.exe文件的完整路径。</param>
private void SetRunAsAdminCompatibility(string exePath)
{
const string keyPath = @"Software\Microsoft\Windows NT\CurrentVersion\AppCompatFlags\Layers";
try
{
using (RegistryKey key = Registry.CurrentUser.CreateSubKey(keyPath))
{
if (key != null)
{
key.SetValue(exePath, "~ RUNASADMIN");
}
else
{
UpdateStatus($"无法打开或创建注册表项: {keyPath}");
}
}
}
catch (Exception ex)
{
UpdateStatus($"设置管理员权限失败: {exePath} - {ex.Message}");
}
}
/// <summary>
/// 从嵌入的资源中提取与当前进程体系结构匹配的7z.dll到临时目录。
/// </summary>
/// <returns>提取的7z.dll的路径如果失败则返回null。</returns>
private string Extract7zDll()
{
try
{
string dllName = Environment.Is64BitProcess ? "7z-x64.dll" : "7z-x86.dll";
string resourceName = $"CheckDownload.{dllName}";
string dllPath = Path.Combine(_tempDirectory, "7z.dll");
if (File.Exists(dllPath))
{
// 可以选择在这里添加对现有DLL版本的校验但为简化我们先直接返回
return dllPath;
}
using (var resourceStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName))
{
if (resourceStream == null)
{
UpdateStatus($"在嵌入资源中未找到 {dllName}。");
return null;
}
using (var fileStream = new FileStream(dllPath, FileMode.Create, FileAccess.Write))
{
resourceStream.CopyTo(fileStream);
}
}
return dllPath;
}
catch (Exception ex)
{
UpdateStatus($"提取7z.dll失败: {ex.Message}");
return null;
}
}
// add helper to check db extension
private bool IsDatabaseFile(string relativePath)
{
@@ -1678,17 +1751,34 @@ namespace CheckDownload
return ext == ".db" || ext == ".db3";
}
private void KillProcessIfRunning(string exeRelativePath)
private void KillProcessIfRunning(string exePath)
{
try
{
string exeName = Path.GetFileNameWithoutExtension(exeRelativePath);
string exeName = Path.GetFileNameWithoutExtension(exePath);
string targetExeDir;
// 判断是否为完整路径
if (Path.IsPathRooted(exePath))
{
targetExeDir = Path.GetDirectoryName(exePath);
}
else
{
targetExeDir = Path.GetDirectoryName(Path.Combine(_baseDirectory, exePath));
}
foreach (var proc in Process.GetProcessesByName(exeName))
{
if (proc.Id == _currentProcessId) continue;
try
{
proc.Kill();
proc.WaitForExit(5000);
string runningProcessDir = Path.GetDirectoryName(proc.MainModule.FileName);
if (string.Equals(runningProcessDir, targetExeDir, StringComparison.OrdinalIgnoreCase))
{
proc.Kill();
proc.WaitForExit(5000);
}
}
catch { }
}
@@ -1700,20 +1790,173 @@ namespace CheckDownload
{
try
{
foreach (var proc in Process.GetProcesses())
foreach (var proc in Process.GetProcessesByName(baseName))
{
if (string.Equals(proc.ProcessName, baseName, StringComparison.OrdinalIgnoreCase))
if (proc.Id == _currentProcessId) continue;
try
{
try
if (proc.MainModule.FileName.StartsWith(_baseDirectory, StringComparison.OrdinalIgnoreCase))
{
proc.Kill();
proc.WaitForExit(5000);
}
catch { }
}
catch { }
}
}
catch { }
}
/// <summary>
/// Truncates a string to a maximum length and appends an ellipsis.
/// </summary>
/// <param name="value">The string to truncate.</param>
/// <param name="maxLength">The maximum length of the string.</param>
/// <returns>The truncated string.</returns>
private string TruncateString(string value, int maxLength)
{
if (string.IsNullOrEmpty(value)) return value;
return value.Length <= maxLength ? value : value.Substring(0, maxLength) + "...";
}
// === 新增: 更新整体下载大小与速度 ===
private void UpdateOverallSize()
{
DateTime now = DateTime.UtcNow;
double intervalSeconds = (now - _lastSpeedUpdateTime).TotalSeconds;
if (intervalSeconds <= 0) intervalSeconds = 0.1; // 防止除零
// 读取并清零自上次计算以来的字节数
long intervalBytes = Interlocked.Exchange(ref _bytesSinceLastSpeedCalc, 0);
double bytesPerSec = intervalBytes / intervalSeconds;
long downloaded = Interlocked.Read(ref _totalDownloadedBytes);
string speedText = $"{FormatBytes((long)bytesPerSec)}/s";
string sizeText = $"{FormatBytes(downloaded)}({speedText})";
_lastSpeedUpdateTime = now;
UpdateSize(sizeText);
}
/// <summary>
/// 统一处理并显示错误信息
/// </summary>
/// <param name="location">要显示给用户的基本错误信息</param>
/// <param name="ex">可选的异常对象,其消息将被附加</param>
private void HandleError(string location, Exception ex = null)
{
UpdateStatus("更新失败");
// 递归查找最深层的 InnerException 以获取最根本的错误信息
var innermostException = ex;
while (innermostException?.InnerException != null)
{
innermostException = innermostException.InnerException;
}
string errorMessage = innermostException?.Message ?? ex?.Message ?? "发生未知错误。";
MessageBox.Show(errorMessage, "更新错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
/// <summary>
/// 尝试将文件从源位置复制到目标位置
/// </summary>
private async Task<bool> TryCopyFileAsync(string sourcePath, string targetPath)
{
try
{
// 如果目标文件是.exe文件先检查并kill掉可能正在运行的进程
if (Path.GetExtension(targetPath).Equals(".exe", StringComparison.OrdinalIgnoreCase))
{
KillProcessIfRunning(targetPath);
}
// 确保目标目录存在
string targetDir = Path.GetDirectoryName(targetPath);
if (!Directory.Exists(targetDir))
{
Directory.CreateDirectory(targetDir);
}
// 执行复制操作
File.Copy(sourcePath, targetPath, true);
return true;
}
catch (Exception ex)
{
// 文件可能被占用,稍后重试
try
{
await Task.Delay(1000);
File.Copy(sourcePath, targetPath, true);
return true;
}
catch
{
UpdateStatus($"文件复制失败: {Path.GetFileName(targetPath)}");
return false;
}
}
}
/// <summary>
/// 创建临时文件的备份,防止在程序退出后被删除
/// </summary>
private async Task CreateTempFileBackup()
{
try
{
string tempPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"Temp",
_appName
);
if (Directory.Exists(tempPath))
{
string backupDir = Path.Combine(_baseDirectory, "TempBackup");
if (!Directory.Exists(backupDir))
{
Directory.CreateDirectory(backupDir);
}
string timestamp = DateTime.Now.ToString("yyyyMMddHHmmss");
string backupPath = Path.Combine(backupDir, $"Temp_{timestamp}");
// 简单复制整个临时目录
await Task.Run(() => CopyDirectory(tempPath, backupPath));
//UpdateStatus($"[调试] 临时文件已备份到: {backupPath}");
}
}
catch (Exception ex)
{
UpdateStatus($"创建临时文件备份失败: {ex.Message}");
}
}
/// <summary>
/// 递归复制目录
/// </summary>
private void CopyDirectory(string sourceDir, string targetDir)
{
Directory.CreateDirectory(targetDir);
foreach (var file in Directory.GetFiles(sourceDir))
{
string fileName = Path.GetFileName(file);
string targetFile = Path.Combine(targetDir, fileName);
File.Copy(file, targetFile, true);
}
foreach (var subDir in Directory.GetDirectories(sourceDir))
{
string dirName = Path.GetFileName(subDir);
string targetSubDir = Path.Combine(targetDir, dirName);
CopyDirectory(subDir, targetSubDir);
}
}
}
}

View File

@@ -1,120 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>