428 lines
15 KiB
C#
428 lines
15 KiB
C#
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
|
|
{
|
|
private const string LocalMd5File = "md5.json";
|
|
private const string DnsQueryDomain = "test.file.ipoi.cn";
|
|
private const string BaseDownloadUrl = "http://localhost:8000/";
|
|
private const int MaxConcurrentDownloads = 5;
|
|
|
|
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;
|
|
}
|
|
|
|
public async void Update_Load(object sender, EventArgs e)
|
|
{
|
|
await UpdateFile();
|
|
this.Close();
|
|
}
|
|
|
|
private async Task UpdateFile()
|
|
{
|
|
try
|
|
{
|
|
var localData = await ReadLocalMd5File();
|
|
if (!ValidateLocalData(localData.Version, localData.Md5, localData.Data)) return;
|
|
|
|
var onlineData = await GetOnlineMd5File();
|
|
if (!ValidateOnlineData(onlineData.Version, onlineData.Md5)) return;
|
|
|
|
if (!ShouldUpdate(localData.Version, onlineData.Version)) return;
|
|
|
|
var onlineFileData = await DownloadOnlineMd5File();
|
|
if (onlineFileData == null) return;
|
|
|
|
var differences = CompareDataDifferences(localData.Data, onlineFileData);
|
|
if (differences.Count > 0)
|
|
{
|
|
await DownloadUpdatedFiles(differences);
|
|
}
|
|
|
|
ReplaceLocalMd5File();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
UpdateStatus($"发生错误: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
private bool ValidateLocalData(string version, string md5, JObject data)
|
|
{
|
|
if (string.IsNullOrEmpty(version) || string.IsNullOrEmpty(md5) || data == null)
|
|
{
|
|
UpdateStatus("本地MD5文件无效");
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private bool ValidateOnlineData(string version, string md5)
|
|
{
|
|
if (string.IsNullOrEmpty(version) || string.IsNullOrEmpty(md5))
|
|
{
|
|
UpdateStatus("无法获取在线MD5信息");
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private async Task<(string Version, string Md5, JObject Data)> ReadLocalMd5File()
|
|
{
|
|
try
|
|
{
|
|
UpdateStatus("读取md5.json...");
|
|
string json = await Task.Run(() => File.ReadAllText(LocalMd5File));
|
|
var obj = JObject.Parse(json);
|
|
string version = obj["version"]?.ToString();
|
|
var data = (JObject)obj["data"];
|
|
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);
|
|
}
|
|
}
|
|
|
|
private async Task<(string Version, string Md5)> GetOnlineMd5File()
|
|
{
|
|
try
|
|
{
|
|
UpdateStatus("解析在线md5.json...");
|
|
string responseData = await QueryDnsAsync();
|
|
string firstUnescaped = JsonConvert.DeserializeObject<string>(responseData);
|
|
var dataJson = JObject.Parse(firstUnescaped);
|
|
|
|
string version = dataJson["version"]?.ToString();
|
|
string md5 = dataJson["md5"]?.ToString();
|
|
|
|
_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("校验信息...");
|
|
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;
|
|
}
|
|
}
|
|
|
|
private async Task<JObject> DownloadOnlineMd5File()
|
|
{
|
|
try
|
|
{
|
|
UpdateStatus("下载在线md5.json文件...");
|
|
using (var client = new WebClient())
|
|
{
|
|
await client.DownloadFileTaskAsync(
|
|
new Uri($"{BaseDownloadUrl}MD5/{_onlineMd5File}"),
|
|
_onlineMd5File);
|
|
}
|
|
|
|
UpdateStatus("读取在线md5.json文件...");
|
|
string json = await Task.Run(() => File.ReadAllText(_onlineMd5File));
|
|
var obj = JObject.Parse(json);
|
|
return (JObject)obj["data"];
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
UpdateStatus($"下载在线MD5文件失败: {ex.Message}");
|
|
return null;
|
|
}
|
|
}
|
|
|
|
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)
|
|
{
|
|
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;
|
|
}
|
|
|
|
private bool ShouldDownloadFile(JObject localData, string key, string expectedMd5, string fullPath)
|
|
{
|
|
bool fileMissing = !localData.ContainsKey(key);
|
|
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();
|
|
}
|
|
|
|
private async Task DownloadUpdatedFiles(Dictionary<string, string> differences)
|
|
{
|
|
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);
|
|
}
|
|
|
|
private async Task DownloadFileWithSemaphore(KeyValuePair<string, string> file, SemaphoreSlim semaphore)
|
|
{
|
|
await semaphore.WaitAsync();
|
|
try
|
|
{
|
|
await DownloadAndVerifyFile(file.Key, file.Value);
|
|
UpdateProgress();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
UpdateStatus($"下载失败: {file.Key} - {ex.Message}");
|
|
}
|
|
finally
|
|
{
|
|
semaphore.Release();
|
|
}
|
|
}
|
|
|
|
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}"),
|
|
tempPath);
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
private void VerifyFileMd5(string filePath, string expectedMd5, string relativePath)
|
|
{
|
|
string actualMd5 = CalculateFileMD5(filePath);
|
|
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()
|
|
{
|
|
this.Invoke((MethodInvoker)delegate
|
|
{
|
|
Update_Pro.Value++;
|
|
});
|
|
}
|
|
|
|
private void ReplaceLocalMd5File()
|
|
{
|
|
try
|
|
{
|
|
if (File.Exists(LocalMd5File))
|
|
{
|
|
File.Delete(LocalMd5File);
|
|
}
|
|
File.Move(_onlineMd5File, LocalMd5File);
|
|
UpdateStatus("已更新本地版本信息");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
UpdateStatus($"更新本地版本信息失败: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
private void UpdateStatus(string message)
|
|
{
|
|
this.Invoke((MethodInvoker)delegate
|
|
{
|
|
Status_Box.Text = message;
|
|
});
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|
|
|
|
private static async Task<string> QueryDnsAsync()
|
|
{
|
|
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>>();
|
|
|
|
foreach (var server in dohServers)
|
|
{
|
|
tasks.Add(QueryDnsServer(httpClient, server, cts.Token));
|
|
}
|
|
|
|
var completedTask = await Task.WhenAny(tasks);
|
|
cts.Cancel();
|
|
return await completedTask;
|
|
}
|
|
}
|
|
|
|
private static async Task<string> QueryDnsServer(HttpClient client, string server, CancellationToken token)
|
|
{
|
|
try
|
|
{
|
|
var url = $"{server}?name={DnsQueryDomain}&type=TXT";
|
|
var response = await client.GetStringAsync(url);
|
|
var jsonResponse = JObject.Parse(response);
|
|
|
|
foreach (var record in jsonResponse["Answer"])
|
|
{
|
|
string txtRecord = record["data"]?.ToString();
|
|
if (!string.IsNullOrEmpty(txtRecord))
|
|
{
|
|
return txtRecord;
|
|
}
|
|
}
|
|
return string.Empty;
|
|
}
|
|
catch
|
|
{
|
|
return string.Empty;
|
|
}
|
|
}
|
|
}
|
|
} |