From 5ec395740fdba057e6554ab56ced16fa28bb984d Mon Sep 17 00:00:00 2001
From: Dong <1278815766@qq.com>
Date: Tue, 6 May 2025 16:36:24 +0800
Subject: [PATCH] =?UTF-8?q?refactor(Update):=20=E9=87=8D=E6=9E=84=E8=87=AA?=
=?UTF-8?q?=E5=8A=A8=E6=9B=B4=E6=96=B0=E6=A8=A1=E5=9D=97=EF=BC=8C=E4=BC=98?=
=?UTF-8?q?=E5=8C=96=E4=BB=A3=E7=A0=81=E7=BB=93=E6=9E=84=E5=92=8C=E6=B3=A8?=
=?UTF-8?q?=E9=87=8A?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
重构了自动更新模块的代码结构,增加了详细的XML注释,提升了代码的可读性和可维护性。同时优化了部分逻辑,如并发下载控制和文件校验流程,以确保更新过程的稳定性和效率。
---
Form1.cs | 239 ++++++++++++++++++++++++++++++++++++++++++++++++++++---
1 file changed, 230 insertions(+), 9 deletions(-)
diff --git a/Form1.cs b/Form1.cs
index f9912c1..9b8e80b 100644
--- a/Form1.cs
+++ b/Form1.cs
@@ -17,57 +17,88 @@ using Newtonsoft.Json.Linq;
namespace CheckDownload
{
+ ///
+ /// 自动更新窗体类,负责检查和下载软件更新
+ ///
public partial class Update : Form
{
+ /// 本地MD5文件名
private const string LocalMd5File = "md5.json";
+ /// DNS查询域名,用于获取更新信息
private const string DnsQueryDomain = "test.file.ipoi.cn";
- private const string BaseDownloadUrl = "http://localhost:8000/";
+ /// 基础下载URL
+ private const string BaseDownloadUrl = "http://localhost:60006/";
+ /// 最大并发下载数
private const int MaxConcurrentDownloads = 5;
+ /// 在线MD5文件名,从DNS查询获取
private string _onlineMd5File = "";
+ /// 已完成下载的文件数
private int _completedFiles = 0;
+ ///
+ /// 构造函数,初始化更新窗体
+ ///
public Update()
{
InitializeComponent();
ConfigureProgressBar();
}
+ ///
+ /// 配置进度条初始设置
+ ///
private void ConfigureProgressBar()
{
- Update_Pro.Minimum = 0;
- Update_Pro.Maximum = 100;
- Update_Pro.Value = 0;
- Update_Pro.Step = 1;
+ Update_Pro.Minimum = 0; // 设置最小值
+ Update_Pro.Maximum = 100; // 设置最大值
+ Update_Pro.Value = 0; // 设置初始值
+ Update_Pro.Step = 1; // 设置步进值
}
+ ///
+ /// 窗体加载事件处理方法,启动更新流程
+ ///
+ /// 事件源
+ /// 事件参数
public async void Update_Load(object sender, EventArgs e)
{
- await UpdateFile();
- this.Close();
+ await UpdateFile(); // 执行更新流程
+ this.Close(); // 更新完成后关闭窗体
}
+ ///
+ /// 主要更新流程,检查并下载更新文件
+ ///
+ /// 更新任务
private async Task UpdateFile()
{
try
{
+ // 读取本地MD5文件信息
var localData = await ReadLocalMd5File();
if (!ValidateLocalData(localData.Version, localData.Md5, localData.Data)) return;
+ // 获取在线MD5文件信息
var onlineData = await GetOnlineMd5File();
if (!ValidateOnlineData(onlineData.Version, onlineData.Md5)) return;
+ // 比较版本,判断是否需要更新
if (!ShouldUpdate(localData.Version, onlineData.Version)) return;
+ // 下载在线MD5文件
var onlineFileData = await DownloadOnlineMd5File();
if (onlineFileData == null) return;
+ // 比较文件差异
var differences = CompareDataDifferences(localData.Data, onlineFileData);
if (differences.Count > 0)
{
+ // 下载需要更新的文件
await DownloadUpdatedFiles(differences);
}
+ // 替换本地MD5文件
ReplaceLocalMd5File();
}
catch (Exception ex)
@@ -76,6 +107,13 @@ namespace CheckDownload
}
}
+ ///
+ /// 验证本地数据的有效性
+ ///
+ /// 版本号
+ /// MD5校验值
+ /// 文件数据对象
+ /// 数据是否有效
private bool ValidateLocalData(string version, string md5, JObject data)
{
if (string.IsNullOrEmpty(version) || string.IsNullOrEmpty(md5) || data == null)
@@ -86,6 +124,12 @@ namespace CheckDownload
return true;
}
+ ///
+ /// 验证在线数据的有效性
+ ///
+ /// 版本号
+ /// MD5校验值
+ /// 数据是否有效
private bool ValidateOnlineData(string version, string md5)
{
if (string.IsNullOrEmpty(version) || string.IsNullOrEmpty(md5))
@@ -96,15 +140,24 @@ namespace CheckDownload
return true;
}
+ ///
+ /// 读取本地MD5文件并解析内容
+ ///
+ /// 包含版本号、MD5值和数据对象的元组
private async Task<(string Version, string Md5, JObject Data)> ReadLocalMd5File()
{
try
{
UpdateStatus("读取md5.json...");
+ // 异步读取文件内容
string json = await Task.Run(() => File.ReadAllText(LocalMd5File));
+ // 解析JSON对象
var obj = JObject.Parse(json);
+ // 获取版本号
string version = obj["version"]?.ToString();
+ // 获取数据对象
var data = (JObject)obj["data"];
+ // 计算JSON内容的MD5值
string jsonMd5 = CalculateMD5(json);
return (version, jsonMd5, data);
}
@@ -115,18 +168,27 @@ namespace CheckDownload
}
}
+ ///
+ /// 获取在线MD5文件信息
+ ///
+ /// 包含版本号和MD5值的元组
private async Task<(string Version, string Md5)> GetOnlineMd5File()
{
try
{
UpdateStatus("解析在线md5.json...");
+ // 通过DNS查询获取响应数据
string responseData = await QueryDnsAsync();
+ // 反序列化JSON字符串
string firstUnescaped = JsonConvert.DeserializeObject(responseData);
+ // 解析JSON对象
var dataJson = JObject.Parse(firstUnescaped);
+ // 获取版本号和MD5值
string version = dataJson["version"]?.ToString();
string md5 = dataJson["md5"]?.ToString();
+ // 设置在线MD5文件名
_onlineMd5File = $"{md5}.json";
return (version, md5);
@@ -138,13 +200,21 @@ namespace CheckDownload
}
}
+ ///
+ /// 判断是否需要更新
+ ///
+ /// 本地版本号
+ /// 在线版本号
+ /// 是否需要更新
private bool ShouldUpdate(string localVersion, string onlineVersion)
{
try
{
UpdateStatus("校验信息...");
+ // 解析版本号字符串为Version对象
var localVer = new Version(localVersion);
var onlineVer = new Version(onlineVersion);
+ // 比较版本号,如果本地版本小于在线版本,则需要更新
return localVer.CompareTo(onlineVer) < 0;
}
catch (Exception ex) when (ex is FormatException || ex is ArgumentNullException)
@@ -154,21 +224,29 @@ namespace CheckDownload
}
}
+ ///
+ /// 下载在线MD5文件并解析内容
+ ///
+ /// 文件数据对象
private async Task DownloadOnlineMd5File()
{
try
{
UpdateStatus("下载在线md5.json文件...");
+ // 使用WebClient下载文件
using (var client = new WebClient())
{
await client.DownloadFileTaskAsync(
- new Uri($"{BaseDownloadUrl}MD5/{_onlineMd5File}"),
+ new Uri($"{BaseDownloadUrl}{_onlineMd5File}"),
_onlineMd5File);
}
UpdateStatus("读取在线md5.json文件...");
+ // 异步读取文件内容
string json = await Task.Run(() => File.ReadAllText(_onlineMd5File));
+ // 解析JSON对象
var obj = JObject.Parse(json);
+ // 返回数据部分
return (JObject)obj["data"];
}
catch (Exception ex)
@@ -178,19 +256,31 @@ namespace CheckDownload
}
}
+ ///
+ /// 比较本地数据和在线数据的差异
+ ///
+ /// 本地数据对象
+ /// 在线数据对象
+ /// 当前路径,用于递归调用
+ /// 需要更新的文件路径和MD5值的字典
private Dictionary CompareDataDifferences(JObject localData, JObject onlineData, string currentPath = "")
{
var differences = new Dictionary();
foreach (var onlineProperty in onlineData.Properties())
{
+ // 获取属性名称
string key = onlineProperty.Name;
+ // 获取属性值
JToken onlineValue = onlineProperty.Value;
+ // 构建完整路径
string fullPath = string.IsNullOrEmpty(currentPath) ? key : $"{currentPath}/{key}";
if (onlineValue.Type == JTokenType.String)
{
+ // 如果是字符串类型,表示是文件的MD5值
string expectedMd5 = onlineValue.ToString();
+ // 判断是否需要下载该文件
if (ShouldDownloadFile(localData, key, expectedMd5, fullPath))
{
differences[fullPath] = expectedMd5;
@@ -198,6 +288,7 @@ namespace CheckDownload
}
else if (onlineValue.Type == JTokenType.Object)
{
+ // 如果是对象类型,表示是目录,递归比较
JObject localSubData = GetSubData(localData, key);
var subDifferences = CompareDataDifferences(localSubData, (JObject)onlineValue, fullPath);
foreach (var diff in subDifferences)
@@ -210,46 +301,83 @@ namespace CheckDownload
return differences;
}
+ ///
+ /// 判断是否需要下载文件
+ ///
+ /// 本地数据对象
+ /// 文件键名
+ /// 期望的MD5值
+ /// 文件完整路径
+ /// 是否需要下载
private bool ShouldDownloadFile(JObject localData, string key, string expectedMd5, string fullPath)
{
+ // 本地数据中不存在该键
bool fileMissing = !localData.ContainsKey(key);
+ // 本地数据中存在该键,但MD5值不匹配
bool md5Mismatch = localData.ContainsKey(key) &&
(localData[key].Type != JTokenType.String ||
localData[key].ToString() != expectedMd5);
+ // 物理文件不存在
bool physicalFileMissing = !File.Exists(Path.Combine(".", fullPath.Replace('/', '\\')));
+ // 满足任一条件则需要下载
return fileMissing || md5Mismatch || physicalFileMissing;
}
+ ///
+ /// 获取本地数据中的子数据对象
+ ///
+ /// 本地数据对象
+ /// 子数据键名
+ /// 子数据对象,如果不存在则返回空对象
private JObject GetSubData(JObject localData, string key)
{
+ // 如果本地数据中存在该键且类型为对象,则返回该对象,否则返回空对象
return localData.ContainsKey(key) && localData[key].Type == JTokenType.Object
? (JObject)localData[key]
: new JObject();
}
+ ///
+ /// 下载需要更新的文件
+ ///
+ /// 需要更新的文件路径和MD5值的字典
+ /// 下载任务
private async Task DownloadUpdatedFiles(Dictionary differences)
{
+ // 设置进度条最大值为需要更新的文件数量
Update_Pro.Maximum = differences.Count;
Update_Pro.Value = 0;
+ // 创建信号量控制并发下载数量
var semaphore = new SemaphoreSlim(MaxConcurrentDownloads);
var downloadTasks = new List();
+ // 为每个需要更新的文件创建下载任务
foreach (var file in differences)
{
downloadTasks.Add(DownloadFileWithSemaphore(file, semaphore));
}
+ // 等待所有下载任务完成
await Task.WhenAll(downloadTasks);
}
+ ///
+ /// 使用信号量控制并发下载文件
+ ///
+ /// 文件路径和MD5值的键值对
+ /// 控制并发的信号量
+ /// 下载任务
private async Task DownloadFileWithSemaphore(KeyValuePair file, SemaphoreSlim semaphore)
{
+ // 等待信号量,控制并发数量
await semaphore.WaitAsync();
try
{
+ // 下载并验证文件
await DownloadAndVerifyFile(file.Key, file.Value);
+ // 更新进度
UpdateProgress();
}
catch (Exception ex)
@@ -258,47 +386,81 @@ namespace CheckDownload
}
finally
{
+ // 释放信号量,允许其他下载任务执行
semaphore.Release();
}
}
+ ///
+ /// 下载并验证文件
+ ///
+ /// 文件的相对路径
+ /// 期望的MD5值
+ /// 下载任务
private async Task DownloadAndVerifyFile(string relativePath, string expectedMd5)
{
+ // 更新状态显示当前下载进度
UpdateStatus($"正在下载 ({Update_Pro.Value + 1}/{Update_Pro.Maximum}): {relativePath}");
+ // 获取本地路径和临时文件路径
string localPath = GetLocalPath(relativePath);
string tempPath = $"{localPath}.tmp";
+ // 确保目录存在
EnsureDirectoryExists(localPath);
+ // 下载文件到临时路径
using (var client = new WebClient())
{
await client.DownloadFileTaskAsync(
- new Uri($"{BaseDownloadUrl}{relativePath}"),
+ new Uri($"{BaseDownloadUrl}File/{relativePath}"),
tempPath);
}
+ // 验证文件MD5
VerifyFileMd5(tempPath, expectedMd5, relativePath);
+ // 替换现有文件
ReplaceExistingFile(tempPath, localPath);
}
+ ///
+ /// 获取文件的本地路径
+ ///
+ /// 文件的相对路径
+ /// 本地文件路径
private string GetLocalPath(string relativePath)
{
+ // 将相对路径转换为本地路径,替换斜杠为反斜杠
return Path.Combine(".", relativePath.Replace('/', '\\'));
}
+ ///
+ /// 确保文件所在目录存在
+ ///
+ /// 文件路径
private void EnsureDirectoryExists(string filePath)
{
+ // 获取文件所在目录
string directory = Path.GetDirectoryName(filePath);
+ // 如果目录不存在,则创建目录
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
}
+ ///
+ /// 验证文件的MD5值
+ ///
+ /// 文件路径
+ /// 期望的MD5值
+ /// 文件的相对路径,用于错误提示
+ /// 当MD5校验失败时抛出异常
private void VerifyFileMd5(string filePath, string expectedMd5, string relativePath)
{
+ // 计算文件的实际MD5值
string actualMd5 = CalculateFileMD5(filePath);
+ // 如果MD5值不匹配,删除文件并抛出异常
if (actualMd5 != expectedMd5)
{
File.Delete(filePath);
@@ -306,31 +468,47 @@ namespace CheckDownload
}
}
+ ///
+ /// 替换现有文件
+ ///
+ /// 临时文件路径
+ /// 目标文件路径
private void ReplaceExistingFile(string tempPath, string localPath)
{
+ // 如果目标文件已存在,则先删除
if (File.Exists(localPath))
{
File.Delete(localPath);
}
+ // 将临时文件移动到目标位置
File.Move(tempPath, localPath);
}
+ ///
+ /// 更新进度条
+ ///
private void UpdateProgress()
{
+ // 在UI线程上更新进度条值
this.Invoke((MethodInvoker)delegate
{
Update_Pro.Value++;
});
}
+ ///
+ /// 替换本地MD5文件为在线下载的新版本
+ ///
private void ReplaceLocalMd5File()
{
try
{
+ // 如果本地MD5文件已存在,则先删除
if (File.Exists(LocalMd5File))
{
File.Delete(LocalMd5File);
}
+ // 将下载的在线MD5文件移动到本地MD5文件位置
File.Move(_onlineMd5File, LocalMd5File);
UpdateStatus("已更新本地版本信息");
}
@@ -340,36 +518,61 @@ namespace CheckDownload
}
}
+ ///
+ /// 更新状态信息显示
+ ///
+ /// 要显示的状态信息
private void UpdateStatus(string message)
{
+ // 在UI线程上更新状态文本框
this.Invoke((MethodInvoker)delegate
{
Status_Box.Text = message;
});
}
+ ///
+ /// 计算字符串的MD5值
+ ///
+ /// 输入字符串
+ /// MD5哈希值(小写十六进制字符串)
private string CalculateMD5(string input)
{
using (var md5 = MD5.Create())
{
+ // 将字符串转换为字节数组
byte[] inputBytes = Encoding.UTF8.GetBytes(input);
+ // 计算哈希值
byte[] hashBytes = md5.ComputeHash(inputBytes);
+ // 将字节数组转换为十六进制字符串并返回
return BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant();
}
}
+ ///
+ /// 计算文件的MD5值
+ ///
+ /// 文件路径
+ /// MD5哈希值(小写十六进制字符串)
private string CalculateFileMD5(string filePath)
{
using (var md5 = MD5.Create())
using (var stream = File.OpenRead(filePath))
{
+ // 计算文件流的哈希值
byte[] hashBytes = md5.ComputeHash(stream);
+ // 将字节数组转换为十六进制字符串并返回
return BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant();
}
}
+ ///
+ /// 通过多个DNS over HTTPS服务器查询TXT记录
+ ///
+ /// 查询到的TXT记录内容
private static async Task QueryDnsAsync()
{
+ // DNS over HTTPS服务器列表
var dohServers = new List
{
"https://cloudflare-dns.com/dns-query",
@@ -387,31 +590,48 @@ namespace CheckDownload
using (var httpClient = new HttpClient())
{
+ // 创建取消令牌源,用于取消其他任务
var cts = new CancellationTokenSource();
var tasks = new List>();
+ // 并行查询所有DNS服务器
foreach (var server in dohServers)
{
tasks.Add(QueryDnsServer(httpClient, server, cts.Token));
}
+ // 等待任意一个任务完成
var completedTask = await Task.WhenAny(tasks);
+ // 取消其他正在进行的任务
cts.Cancel();
+ // 返回第一个完成的任务结果
return await completedTask;
}
}
+ ///
+ /// 查询指定DNS over HTTPS服务器的TXT记录
+ ///
+ /// HTTP客户端
+ /// DNS服务器URL
+ /// 取消令牌
+ /// 查询到的TXT记录内容,失败则返回空字符串
private static async Task QueryDnsServer(HttpClient client, string server, CancellationToken token)
{
try
{
+ // 构建DNS查询URL
var url = $"{server}?name={DnsQueryDomain}&type=TXT";
+ // 发送HTTP请求获取响应
var response = await client.GetStringAsync(url);
+ // 解析JSON响应
var jsonResponse = JObject.Parse(response);
+ // 遍历Answer部分查找TXT记录
foreach (var record in jsonResponse["Answer"])
{
string txtRecord = record["data"]?.ToString();
+ // 如果找到有效的TXT记录,则返回
if (!string.IsNullOrEmpty(txtRecord))
{
return txtRecord;
@@ -421,6 +641,7 @@ namespace CheckDownload
}
catch
{
+ // 查询失败时返回空字符串
return string.Empty;
}
}