MD5Tools/Form1.cs
Dong 2a339142ad refactor(下载逻辑): 重构文件下载和校验流程
将文件下载和校验逻辑拆分为独立的方法,引入内存缓存机制,避免频繁的磁盘操作。所有文件下载完成后统一校验和保存,提高代码可维护性和执行效率。
2025-05-07 12:21:30 +08:00

711 lines
27 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
{
/// <summary>
/// 自动更新窗体类,负责检查和下载软件更新
/// </summary>
public partial class Update : Form
{
/// <summary>本地MD5文件名</summary>
private const string LocalMd5File = "md5.json";
/// <summary>DNS查询域名用于获取更新信息</summary>
private const string DnsQueryDomain = "test.file.ipoi.cn";
/// <summary>基础下载URL</summary>
private const string BaseDownloadUrl = "http://localhost:60006/";
/// <summary>最大并发下载数</summary>
private const int MaxConcurrentDownloads = 5;
/// <summary>在线MD5文件名从DNS查询获取</summary>
private string _onlineMd5File = "";
// 类级别变量,用于存储下载的文件数据
private Dictionary<string, (byte[] Data, string ExpectedMd5)> _downloadedFiles = new Dictionary<string, (byte[], string)>();
/// <summary>已完成下载的文件数</summary>
private int _completedFiles = 0;
/// <summary>
/// 构造函数,初始化更新窗体
/// </summary>
public Update()
{
InitializeComponent();
ConfigureProgressBar();
}
/// <summary>
/// 配置进度条初始设置
/// </summary>
private void ConfigureProgressBar()
{
Update_Pro.Minimum = 0; // 设置最小值
Update_Pro.Maximum = 100; // 设置最大值
Update_Pro.Value = 0; // 设置初始值
Update_Pro.Step = 1; // 设置步进值
}
/// <summary>
/// 窗体加载事件处理方法,启动更新流程
/// </summary>
/// <param name="sender">事件源</param>
/// <param name="e">事件参数</param>
public async void Update_Load(object sender, EventArgs e)
{
await UpdateFile(); // 执行更新流程
this.Close(); // 更新完成后关闭窗体
}
/// <summary>
/// 主要更新流程,检查并下载更新文件
/// </summary>
/// <returns>更新任务</returns>
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)
{
UpdateStatus($"发生错误: {ex.Message}");
}
}
/// <summary>
/// 验证本地数据的有效性
/// </summary>
/// <param name="version">版本号</param>
/// <param name="md5">MD5校验值</param>
/// <param name="data">文件数据对象</param>
/// <returns>数据是否有效</returns>
private bool ValidateLocalData(string version, string md5, JObject data)
{
if (string.IsNullOrEmpty(version) || string.IsNullOrEmpty(md5) || data == null)
{
UpdateStatus("本地MD5文件无效");
return false;
}
return true;
}
/// <summary>
/// 验证在线数据的有效性
/// </summary>
/// <param name="version">版本号</param>
/// <param name="md5">MD5校验值</param>
/// <returns>数据是否有效</returns>
private bool ValidateOnlineData(string version, string md5)
{
if (string.IsNullOrEmpty(version) || string.IsNullOrEmpty(md5))
{
UpdateStatus("无法获取在线MD5信息");
return false;
}
return true;
}
/// <summary>
/// 读取本地MD5文件并解析内容
/// </summary>
/// <returns>包含版本号、MD5值和数据对象的元组</returns>
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);
}
}
/// <summary>
/// 获取在线MD5文件信息
/// </summary>
/// <returns>包含版本号和MD5值的元组</returns>
private async Task<(string Version, string Md5)> GetOnlineMd5File()
{
try
{
UpdateStatus("解析在线md5.json...");
// 通过DNS查询获取响应数据
string responseData = await QueryDnsAsync();
// 反序列化JSON字符串
string firstUnescaped = JsonConvert.DeserializeObject<string>(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);
}
}
/// <summary>
/// 判断是否需要更新
/// </summary>
/// <param name="localVersion">本地版本号</param>
/// <param name="onlineVersion">在线版本号</param>
/// <returns>是否需要更新</returns>
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;
}
}
/// <summary>
/// 下载在线MD5文件并解析内容
/// </summary>
/// <returns>文件数据对象</returns>
private async Task<JObject> 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;
}
}
/// <summary>
/// 比较本地数据和在线数据的差异
/// </summary>
/// <param name="localData">本地数据对象</param>
/// <param name="onlineData">在线数据对象</param>
/// <param name="currentPath">当前路径,用于递归调用</param>
/// <returns>需要更新的文件路径和MD5值的字典</returns>
private Dictionary<string, string> CompareDataDifferences(JObject localData, JObject onlineData, string currentPath = "")
{
var differences = new Dictionary<string, string>();
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;
}
/// <summary>
/// 判断是否需要下载文件
/// </summary>
/// <param name="localData">本地数据对象</param>
/// <param name="key">文件键名</param>
/// <param name="expectedMd5">期望的MD5值</param>
/// <param name="fullPath">文件完整路径</param>
/// <returns>是否需要下载</returns>
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;
}
/// <summary>
/// 获取本地数据中的子数据对象
/// </summary>
/// <param name="localData">本地数据对象</param>
/// <param name="key">子数据键名</param>
/// <returns>子数据对象,如果不存在则返回空对象</returns>
private JObject GetSubData(JObject localData, string key)
{
// 如果本地数据中存在该键且类型为对象,则返回该对象,否则返回空对象
return localData.ContainsKey(key) && localData[key].Type == JTokenType.Object
? (JObject)localData[key]
: new JObject();
}
/// <summary>
/// 下载需要更新的文件
/// </summary>
/// <param name="differences">需要更新的文件路径和MD5值的字典</param>
/// <returns>下载任务</returns>
private async Task DownloadUpdatedFiles(Dictionary<string, string> differences)
{
// 重置下载状态
_completedFiles = 0;
_downloadedFiles.Clear();
Update_Pro.Maximum = differences.Count;
Update_Pro.Value = 0;
// 创建信号量控制并发下载数量
var semaphore = new SemaphoreSlim(MaxConcurrentDownloads);
var downloadTasks = new List<Task>();
// 为每个需要更新的文件创建下载任务
foreach (var file in differences)
{
downloadTasks.Add(DownloadFileWithSemaphore(file, semaphore));
}
// 等待所有下载任务完成
await Task.WhenAll(downloadTasks);
// 所有文件下载完成后统一校验和保存
VerifyAndSaveAllFiles();
}
/// <summary>
/// 使用信号量控制并发下载文件
/// </summary>
/// <param name="file">文件路径和MD5值的键值对</param>
/// <param name="semaphore">控制并发的信号量</param>
/// <returns>下载任务</returns>
private async Task DownloadFileWithSemaphore(KeyValuePair<string, string> file, SemaphoreSlim semaphore)
{
await semaphore.WaitAsync();
try
{
await DownloadToMemory(file.Key, file.Value);
}
finally
{
semaphore.Release();
}
}
/// <summary>
/// 下载文件到内存并暂存
/// </summary>
private async Task DownloadToMemory(string relativePath, string expectedMd5)
{
try
{
// 更新状态显示当前下载进度
int current = Interlocked.Increment(ref _completedFiles);
UpdateStatus($"正在下载 ({Update_Pro.Value + 1}/{Update_Pro.Maximum}): {expectedMd5}");
// 下载文件到内存
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;
}
}
/// <summary>
/// 校验并保存所有已下载的文件
/// </summary>
private void VerifyAndSaveAllFiles()
{
UpdateStatus("正在校验文件...");
// 创建失败文件列表
var failedFiles = new List<string>();
foreach (var item in _downloadedFiles)
{
string relativePath = item.Key;
byte[] data = item.Value.Data;
string expectedMd5 = item.Value.ExpectedMd5;
try
{
// 计算内存中数据的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);
}
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}个文件校验失败");
}
}
/// <summary>
/// 获取文件的本地路径
/// </summary>
/// <param name="relativePath">文件的相对路径</param>
/// <returns>本地文件路径</returns>
private string GetLocalPath(string relativePath)
{
// 将相对路径转换为本地路径,替换斜杠为反斜杠
return Path.Combine(".", relativePath.Replace('/', '\\'));
}
/// <summary>
/// 确保文件所在目录存在
/// </summary>
/// <param name="filePath">文件路径</param>
private void EnsureDirectoryExists(string filePath)
{
// 获取文件所在目录
string directory = Path.GetDirectoryName(filePath);
// 如果目录不存在,则创建目录
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
}
/// <summary>
/// 验证文件的MD5值
/// </summary>
/// <param name="filePath">文件路径</param>
/// <param name="expectedMd5">期望的MD5值</param>
/// <param name="relativePath">文件的相对路径,用于错误提示</param>
/// <exception cref="Exception">当MD5校验失败时抛出异常</exception>
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}");
}
}
/// <summary>
/// 替换现有文件
/// </summary>
/// <param name="tempPath">临时文件路径</param>
/// <param name="localPath">目标文件路径</param>
private void ReplaceExistingFile(string tempPath, string localPath)
{
// 如果目标文件已存在,则先删除
if (File.Exists(localPath))
{
File.Delete(localPath);
}
// 将临时文件移动到目标位置
File.Move(tempPath, localPath);
}
/// <summary>
/// 更新进度条
/// </summary>
private void UpdateProgress()
{
// 在UI线程上更新进度条值
this.Invoke((MethodInvoker)delegate
{
Update_Pro.Value++;
});
}
/// <summary>
/// 替换本地MD5文件为在线下载的新版本
/// </summary>
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}");
}
}
/// <summary>
/// 更新状态信息显示
/// </summary>
/// <param name="message">要显示的状态信息</param>
private void UpdateStatus(string message)
{
// 在UI线程上更新状态文本框
this.Invoke((MethodInvoker)delegate
{
Status_Box.Text = message;
});
}
/// <summary>
/// 计算字符串的MD5值
/// </summary>
/// <param name="input">输入字符串</param>
/// <returns>MD5哈希值小写十六进制字符串</returns>
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();
}
}
/// <summary>
/// 计算文件的MD5值
/// </summary>
/// <param name="filePath">文件路径</param>
/// <returns>MD5哈希值小写十六进制字符串</returns>
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();
}
}
/// <summary>
/// 通过多个DNS over HTTPS服务器查询TXT记录
/// </summary>
/// <returns>查询到的TXT记录内容</returns>
private static async Task<string> QueryDnsAsync()
{
// DNS over HTTPS服务器列表
var dohServers = new List<string>
{
"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<Task<string>>();
// 并行查询所有DNS服务器
foreach (var server in dohServers)
{
tasks.Add(QueryDnsServer(httpClient, server, cts.Token));
}
// 等待任意一个任务完成
var completedTask = await Task.WhenAny(tasks);
// 取消其他正在进行的任务
cts.Cancel();
// 返回第一个完成的任务结果
return await completedTask;
}
}
/// <summary>
/// 查询指定DNS over HTTPS服务器的TXT记录
/// </summary>
/// <param name="client">HTTP客户端</param>
/// <param name="server">DNS服务器URL</param>
/// <param name="token">取消令牌</param>
/// <returns>查询到的TXT记录内容失败则返回空字符串</returns>
private static async Task<string> 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;
}
}
}
}