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; } }