using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Security.Cryptography; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; using Newtonsoft.Json; 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"; /// 基础下载URL private const string BaseDownloadUrl = "http://localhost:60006/"; /// 最大并发下载数 private const int MaxConcurrentDownloads = 5; /// 在线MD5文件名,从DNS查询获取 private string _onlineMd5File = ""; // 类级别变量,用于存储下载的文件数据 private Dictionary _downloadedFiles = new Dictionary(); /// 已完成下载的文件数 private int _completedFiles = 0; /// 需要下载的总文件数 private int _totalFilesToDownload = 0; /// /// 构造函数,初始化更新窗体 /// public Update() { InitializeComponent(); ConfigureProgressBar(); } /// /// 配置进度条初始设置 /// private void ConfigureProgressBar() { Update_Pro.Minimum = 0; // 设置最小值 Update_Pro.Maximum = 100; // 设置最大值 Update_Pro.Value = 0; // 设置初始值 Update_Pro.Step = 1; // 设置步进值 } /// /// 更新进度条值 /// /// 进度百分比(0-100) private void UpdateProgressValue(int percentage) { // 确保百分比在有效范围内 if (percentage < 0) percentage = 0; if (percentage > 100) percentage = 100; // 在UI线程上更新进度条值 if (this.InvokeRequired) { this.Invoke((MethodInvoker)delegate { // 确保进度条值不超出范围 if (percentage >= Update_Pro.Minimum && percentage <= Update_Pro.Maximum) { Update_Pro.Value = percentage; } else { // 如果超出范围,则设置为最大或最小值 Update_Pro.Value = Math.Min(Math.Max(percentage, Update_Pro.Minimum), Update_Pro.Maximum); } // 添加Application.DoEvents()调用,确保消息队列被处理 Application.DoEvents(); }); } else { if (percentage >= Update_Pro.Minimum && percentage <= Update_Pro.Maximum) { Update_Pro.Value = percentage; } else { Update_Pro.Value = Math.Min(Math.Max(percentage, Update_Pro.Minimum), Update_Pro.Maximum); } Application.DoEvents(); } } /// /// 窗体加载事件处理方法,启动更新流程 /// /// 事件源 /// 事件参数 public async void Update_Load(object sender, EventArgs e) { // 设置窗体位置到屏幕右下角 PositionFormToBottomRight(); await UpdateFile(); // 执行更新流程 // 注意:窗体关闭逻辑已移至UpdateFile方法中,确保显示完成消息后再关闭 } /// /// 设置窗体位置到屏幕右下角 /// private void PositionFormToBottomRight() { // 获取当前屏幕的工作区域(排除任务栏等系统元素) Rectangle workingArea = Screen.GetWorkingArea(this); // 计算窗体在右下角的位置 int x = workingArea.Right - this.Width; int y = workingArea.Bottom - this.Height; // 设置窗体位置 this.Location = new Point(x, y); } /// /// 主要更新流程,检查并下载更新文件 /// /// 更新任务 private async Task UpdateFile() { try { // 定义更新流程的总步骤数和当前步骤 const int totalSteps = 6; // 总共6个主要步骤 int currentStep = 0; // 更新进度条初始状态 UpdateProgressValue(0); // 步骤1: 读取本地MD5文件信息 UpdateStatus("读取本地MD5文件..."); var localData = await ReadLocalMd5File(); if (!ValidateLocalData(localData.Version, localData.Md5, localData.Data)) return; UpdateProgressValue(++currentStep * 100 / totalSteps); // 更新进度为16% // 步骤2: 获取在线MD5文件信息 UpdateStatus("获取在线MD5文件信息..."); var onlineData = await GetOnlineMd5File(); if (!ValidateOnlineData(onlineData.Version, onlineData.Md5)) return; UpdateProgressValue(++currentStep * 100 / totalSteps); // 更新进度为33% // 步骤3: 比较版本,判断是否需要更新 UpdateStatus("比较版本信息..."); if (!ShouldUpdate(localData.Version, onlineData.Version)) { UpdateStatus("当前已是最新版本,无需更新"); UpdateProgressValue(100); // 完成进度 return; } UpdateProgressValue(++currentStep * 100 / totalSteps); // 更新进度为50% // 步骤4: 下载在线MD5文件 UpdateStatus("下载在线MD5文件..."); var onlineFileData = await DownloadOnlineMd5File(); if (onlineFileData == null) return; UpdateProgressValue(++currentStep * 100 / totalSteps); // 更新进度为66% // 步骤5: 比较文件差异 UpdateStatus("比较文件差异..."); var differences = CompareDataDifferences(localData.Data, onlineFileData); UpdateProgressValue(++currentStep * 100 / totalSteps); // 更新进度为83% // 步骤6: 下载和更新文件 if (differences.Count > 0) { // 下载需要更新的文件 await DownloadUpdatedFiles(differences); } else { UpdateStatus("无文件需要更新"); } // 替换本地MD5文件 ReplaceLocalMd5File(); UpdateProgressValue(100); // 完成进度 // 显示更新完成消息 UpdateStatus("更新完成"); // 等待1秒后关闭窗体 await Task.Delay(1000); this.Close(); } catch (Exception ex) { UpdateStatus($"发生错误: {ex.Message}"); } } /// /// 验证本地数据的有效性 /// /// 版本号 /// MD5校验值 /// 文件数据对象 /// 数据是否有效 private bool ValidateLocalData(string version, string md5, JObject data) { if (string.IsNullOrEmpty(version) || string.IsNullOrEmpty(md5) || data == null) { UpdateStatus("本地MD5文件无效"); return false; } return true; } /// /// 验证在线数据的有效性 /// /// 版本号 /// MD5校验值 /// 数据是否有效 private bool ValidateOnlineData(string version, string md5) { if (string.IsNullOrEmpty(version) || string.IsNullOrEmpty(md5)) { UpdateStatus("无法获取在线MD5信息"); return false; } 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); } catch (Exception ex) when (ex is FileNotFoundException || ex is JsonException) { UpdateStatus($"读取本地MD5文件失败: {ex.Message}"); return (null, null, null); } } /// /// 获取在线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); } catch (Exception ex) { UpdateStatus($"获取在线MD5信息失败: {ex.Message}"); return (null, null); } } /// /// 判断是否需要更新 /// /// 本地版本号 /// 在线版本号 /// 是否需要更新 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) { UpdateStatus($"版本比较失败: {ex.Message}"); return false; } } /// /// 下载在线MD5文件并解析内容 /// /// 文件数据对象 private async Task DownloadOnlineMd5File() { try { UpdateStatus("下载在线md5.json文件..."); // 使用WebClient下载文件 using (var client = new WebClient()) { await client.DownloadFileTaskAsync( 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) { UpdateStatus($"下载在线MD5文件失败: {ex.Message}"); return null; } } /// /// 比较本地数据和在线数据的差异 /// /// 本地数据对象 /// 在线数据对象 /// 当前路径,用于递归调用 /// 需要更新的文件路径和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; } } else if (onlineValue.Type == JTokenType.Object) { // 如果是对象类型,表示是目录,递归比较 JObject localSubData = GetSubData(localData, key); var subDifferences = CompareDataDifferences(localSubData, (JObject)onlineValue, fullPath); foreach (var diff in subDifferences) { differences[diff.Key] = diff.Value; } } } 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) { // 重置下载状态 _completedFiles = 0; _downloadedFiles.Clear(); // 设置总文件数 _totalFilesToDownload = differences.Count; // 更新状态信息 UpdateStatus($"开始下载更新文件,共{differences.Count}个文件..."); // 创建信号量控制并发下载数量 var semaphore = new SemaphoreSlim(MaxConcurrentDownloads); var downloadTasks = new List(); // 为每个需要更新的文件创建下载任务 foreach (var file in differences) { downloadTasks.Add(DownloadFileWithSemaphore(file, semaphore)); } // 等待所有下载任务完成 await Task.WhenAll(downloadTasks); // 所有文件下载完成后统一校验和保存 UpdateStatus("所有文件下载完成,开始校验和保存..."); VerifyAndSaveAllFiles(); } /// /// 使用信号量控制并发下载文件 /// /// 文件路径和MD5值的键值对 /// 控制并发的信号量 /// 下载任务 private async Task DownloadFileWithSemaphore(KeyValuePair file, SemaphoreSlim semaphore) { await semaphore.WaitAsync(); try { await DownloadToMemory(file.Key, file.Value); } finally { semaphore.Release(); } } /// /// 下载文件到内存并暂存 /// private async Task DownloadToMemory(string relativePath, string expectedMd5) { try { // 更新状态显示当前下载进度 int current = Interlocked.Increment(ref _completedFiles); // 下载文件到内存 using (var client = new HttpClient()) { var fileUrl = $"{BaseDownloadUrl}File/{expectedMd5}"; var response = await client.GetAsync(fileUrl); response.EnsureSuccessStatusCode(); byte[] fileData = await response.Content.ReadAsByteArrayAsync(); // 存储到内存字典 lock (_downloadedFiles) { _downloadedFiles[relativePath] = (fileData, expectedMd5); } } // 更新进度 UpdateProgress(); } catch (Exception ex) { UpdateStatus($"下载失败: {relativePath} - {ex.Message}"); throw; } } /// /// 校验并保存所有已下载的文件 /// private void VerifyAndSaveAllFiles() { UpdateStatus("正在校验文件..."); // 创建失败文件列表 var failedFiles = new List(); int processedCount = 0; int totalFiles = _downloadedFiles.Count; foreach (var item in _downloadedFiles) { string relativePath = item.Key; byte[] data = item.Value.Data; string expectedMd5 = item.Value.ExpectedMd5; try { // 更新状态信息 processedCount++; UpdateStatus($"正在校验和保存文件 ({processedCount}/{totalFiles}): {relativePath}"); // 计算内存中数据的MD5 string actualMd5; using (var md5 = MD5.Create()) { byte[] hashBytes = md5.ComputeHash(data); actualMd5 = BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant(); } // 校验MD5 if (actualMd5 != expectedMd5.ToLower()) { throw new Exception($"MD5校验失败 (期望: {expectedMd5}, 实际: {actualMd5})"); } // 获取本地路径并创建目录 string localPath = GetLocalPath(relativePath); EnsureDirectoryExists(localPath); // 保存文件 File.WriteAllBytes(localPath, data); // 更新进度条 - 从95%到98% // 确保即使在totalFiles为0的情况下也不会出现除零错误,并且百分比不会超过98 int percentage = 95; if (totalFiles > 0) { // 计算进度时确保不会超过98% percentage = 95 + Math.Min(3, (int)(processedCount * 3.0 / totalFiles)); } UpdateProgressValue(percentage); } catch (Exception ex) { UpdateStatus($"文件校验失败: {relativePath} - {ex.Message}"); failedFiles.Add(relativePath); } } // 清理失败的文件记录 foreach (var failedFile in failedFiles) { _downloadedFiles.Remove(failedFile); } if (failedFiles.Count > 0) { throw new Exception($"{failedFiles.Count}个文件校验失败"); } else { UpdateStatus("所有文件校验和保存成功"); } } /// /// 获取文件的本地路径 /// /// 文件的相对路径 /// 本地文件路径 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); throw new Exception($"MD5校验失败: {relativePath}"); } } /// /// 替换现有文件 /// /// 临时文件路径 /// 目标文件路径 private void ReplaceExistingFile(string tempPath, string localPath) { // 如果目标文件已存在,则先删除 if (File.Exists(localPath)) { File.Delete(localPath); } // 将临时文件移动到目标位置 File.Move(tempPath, localPath); } /// /// 更新文件下载进度 /// private void UpdateProgress() { // 计算下载进度百分比,避免除零错误 int percentage = 0; if (_totalFilesToDownload > 0) { percentage = (int)(_completedFiles * 100.0 / _totalFilesToDownload); } // 更新状态信息 UpdateStatus($"正在下载文件 ({_completedFiles}/{_totalFilesToDownload})..."); // 在UI线程上更新进度条值 this.BeginInvoke((MethodInvoker)delegate { // 在下载阶段,进度从83%到95%之间变化 // 确保不会超过95% int overallPercentage = 83 + Math.Min(12, (int)(percentage * 12.0 / 100)); // 使用UpdateProgressValue方法确保值在有效范围内 UpdateProgressValue(overallPercentage); }); } /// /// 替换本地MD5文件为在线下载的新版本 /// private void ReplaceLocalMd5File() { try { // 如果本地MD5文件已存在,则先删除 if (File.Exists(LocalMd5File)) { File.Delete(LocalMd5File); } // 将下载的在线MD5文件移动到本地MD5文件位置 File.Move(_onlineMd5File, LocalMd5File); UpdateStatus("更新完成"); } catch (Exception ex) { UpdateStatus($"更新本地版本信息失败: {ex.Message}"); } } /// /// 更新状态信息显示 /// /// 要显示的状态信息 private void UpdateStatus(string message) { // 使用Invoke而不是BeginInvoke,确保UI更新完成 if (this.InvokeRequired) { this.Invoke((MethodInvoker)delegate { Status_Box.Text = message; // 立即刷新控件,确保状态实时显示 Status_Box.Update(); // 添加Application.DoEvents()调用,确保消息队列被处理 Application.DoEvents(); }); } else { Status_Box.Text = message; Status_Box.Update(); Application.DoEvents(); } } /// /// 计算字符串的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", "https://dns.cloudflare.com/dns-query", "https://1.1.1.1/dns-query", "https://1.0.0.1/dns-query", "https://dns.google/resolve", "https://sm2.doh.pub/dns-query", "https://doh.pub/dns-query", "https://dns.alidns.com/resolve", "https://223.5.5.5/resolve", "https://223.6.6.6/resolve", "https://doh.360.cn/resolve" }; 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; } } return string.Empty; } catch { // 查询失败时返回空字符串 return string.Empty; } } } }