Files
CheckDownload/Form1.cs
2025-06-26 22:48:33 +08:00

1367 lines
54 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.Diagnostics.CodeAnalysis;
using System.Drawing;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using Aliyun.OSS;
using Aliyun.OSS.Common;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Configuration;
namespace CheckDownload
{
public partial class Update : Form
{
// MD5文件名称
private const string Md5File = "md5.json";
// 阿里云OSS访问地址
private const string OssEndpoint = "oss-cn-hongkong.aliyuncs.com";
// 阿里云OSS存储空间名称
private const string OssBucketName = "suwin-oss";
// 阿里云OSS访问密钥ID
private const string OssAccessKeyId = "LTAI5tCwRcL5LUgyHB2j7w82";
// 阿里云OSS访问密钥Secret
private const string OssAccessKeySecret = "7ClQns3wz6psmIp9T2OfuEn3tpzrCK";
// 123盘鉴权密钥
private const string OneDriveAuthKey = "ZhwG3LxOtGJwM3ym";
// 123盘UID
private const string OneDriveUid = "1850250683";
// 123盘路径不包含域名- 修改此处即可同时生效于主备域名
private const string OneDrivePath = "/1850250683/SuWin";
// 123盘主域名
private const string OneDriveMainDomain = "vip.123pan.cn";
// 123盘备用域名
private const string OneDriveBackupDomain = "vip.123yx.com";
// 123盘鉴权有效时间
private const int OneDriveAuthTimeout = 600;
// 网络优化: 静态HttpClient实例避免套接字耗尽
private static readonly HttpClient _httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(20) };
// OSS客户端仅初始化一次避免频繁创建导致内存占用过高
private static readonly OssClient _ossClient = new OssClient(OssEndpoint, OssAccessKeyId, OssAccessKeySecret);
// 网络优化: 备用DNS服务列表提高解析成功率
private static readonly List<string> _dnsServers = new List<string> { "223.5.5.5", "119.29.29.29" };
// 最大并发下载数量
private static readonly int MaxConcurrentDownloads = int.TryParse(ConfigurationManager.AppSettings["MaxConcurrentDownloads"], out var mcd) ? mcd : 4;
// 最大下载重试次数
private static readonly int MaxDownloadRetries = int.TryParse(ConfigurationManager.AppSettings["MaxDownloadRetries"], out var mdr) ? mdr : 2;
// 用于存储下载的文件数据
private Dictionary<string, string> _downloadedFiles = new Dictionary<string, string>();
// 已完成的下载数量
private int _completedCount = 0;
// 总下载数量
private int _totalCount = 0;
// 临时文件夹路径
private string _tempDirectory;
// 基准目录路径(用于文件更新的目标目录)
private string _baseDirectory;
/// <summary>
/// 初始化窗体
/// </summary>
public Update()
{
InitializeComponent();
ConfigureProgressBar();
InitializeTempDirectory();
}
/// <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="percentage">进度百分比</param>
private void UpdateProgressValue(int percentage)
{
if (percentage < 0) percentage = 0;
if (percentage > 100) percentage = 100;
if (this.InvokeRequired)
{
this.Invoke((MethodInvoker)delegate
{
Update_Pro.Value = Math.Min(Math.Max(percentage, Update_Pro.Minimum), Update_Pro.Maximum);
Application.DoEvents();
});
}
else
{
Update_Pro.Value = Math.Min(Math.Max(percentage, Update_Pro.Minimum), Update_Pro.Maximum);
Application.DoEvents();
}
}
/// <summary>
/// 更新状态显示文本(线程安全)
/// </summary>
/// <param name="message">状态消息</param>
private void UpdateStatus(string message)
{
if (this.InvokeRequired)
{
this.Invoke((MethodInvoker)delegate { Status_Box.Text = message; });
}
else
{
Status_Box.Text = message;
}
}
/// <summary>
/// 窗体加载事件处理,启动更新流程
/// </summary>
public async void Update_Load(object sender, EventArgs e)
{
PositionFormToBottomRight();
try
{
await UpdateFile();
}
finally
{
}
}
/// <summary>
/// 将窗体定位到屏幕右下角
/// </summary>
private void PositionFormToBottomRight()
{
Rectangle workingArea = Screen.GetWorkingArea(this);
this.Location = new Point(workingArea.Right - this.Width, workingArea.Bottom - this.Height);
}
/// <summary>
/// 主要的文件更新流程
/// </summary>
private async Task UpdateFile()
{
try
{
CleanupNewFiles();
UpdateStatus("下载在线MD5文件并读取...");
string tempFilePath = Path.Combine(_tempDirectory, Md5File);
// 使用新的带123盘的下载方法
if (!await DownloadMd5FileWithFallback(Md5File, tempFilePath))
{
UpdateStatus("下载在线MD5文件失败");
await Task.Delay(3000);
this.Close();
return;
}
var onlineData = ReadOnlineMd5File(tempFilePath);
if (!ValidateOnlineData(onlineData.Version, onlineData.Md5, onlineData.Data))
{
UpdateStatus("在线MD5文件无效");
await Task.Delay(3000);
this.Close();
return;
}
// 基于md5.json内容智能检测基准目录
InitializeBaseDirectoryFromMd5Data(onlineData.Data);
UpdateStatus("比较本地和在线MD5文件...");
var compareResult = CompareMd5Data(onlineData.Data);
if (compareResult.Count == 0)
{
UpdateStatus("所有文件都是最新的,无需更新");
UpdateProgressValue(100);
// 无需更新时清理临时文件夹
CleanupTempDirectory();
// 显示更新完成并等待2秒
UpdateStatus("更新完成");
await Task.Delay(2000);
this.Close();
return;
}
UpdateStatus("下载并验证文件...");
// 根据路径长度排序,优先下载小文件/浅层文件,可加其它排序规则
var orderedFileList = compareResult.OrderBy(k => k.Key.Length)
.ToDictionary(k => k.Key, v => v.Value);
_totalCount = orderedFileList.Count;
_completedCount = 0;
_downloadedFiles.Clear();
var failedFiles = new ConcurrentDictionary<string, string>();
await PerformDownloads(orderedFileList, failedFiles);
if (!failedFiles.IsEmpty)
{
UpdateStatus($"有 {failedFiles.Count} 个文件下载失败,开始重试...");
var stillFailing = await RetryFailedFilesAsync(new Dictionary<string, string>(failedFiles));
if (stillFailing.Any())
{
UpdateStatus($"重试后仍有 {stillFailing.Count} 个文件下载失败。");
}
}
if (_completedCount == 0 && orderedFileList.Count > 0)
{
throw new Exception("所有文件下载失败。");
}
await VerifyAndSaveAllFiles();
// 校验和保存成功后清理临时目录
CleanupTempDirectory();
// 显示完成状态并退出
UpdateStatus("更新完成");
await Task.Delay(3000);
this.Close();
return;
}
catch (Exception ex)
{
UpdateStatus($"更新失败: {ex.Message}");
await Task.Delay(3000);
this.Close();
}
}
/// <summary>
/// 清理旧的更新文件,删除所有.new后缀的临时文件
/// </summary>
private void CleanupNewFiles()
{
try
{
var newFiles = Directory.GetFiles(_baseDirectory, "*.new", SearchOption.AllDirectories);
if (newFiles.Length > 0)
{
UpdateStatus("正在清理旧的更新文件...");
foreach (var file in newFiles)
{
try
{
File.Delete(file);
}
catch
{
}
}
}
}
catch
{
}
}
/// <summary>
/// 读取在线MD5文件并解析JSON内容提取版本信息和文件清单数据
/// </summary>
/// <param name="filePath">MD5文件的本地路径</param>
/// <returns>包含版本号、MD5值和文件数据的元组</returns>
private (string Version, string Md5, JObject Data) ReadOnlineMd5File(string filePath)
{
try
{
using (var reader = new StreamReader(filePath))
{
string json = reader.ReadToEnd();
var parsed = JObject.Parse(json);
string version = parsed["version"]?.ToString();
var data = (JObject)parsed["data"];
string jsonMd5 = CalculateMD5(json);
return (version, jsonMd5, data);
}
}
catch (Exception ex) when (ex is OssException || ex is JsonException)
{
UpdateStatus($"读取在线MD5文件失败: {ex.Message}");
return (null, null, null);
}
}
/// <summary>
/// 验证在线MD5数据的完整性和有效性
/// </summary>
/// <param name="version">版本号字符串</param>
/// <param name="md5">文件的MD5哈希值</param>
/// <param name="data">包含文件信息的JSON对象</param>
/// <returns>如果所有必需字段都有效则返回true否则返回false</returns>
private bool ValidateOnlineData(string version, string md5, JObject data)
{
if (string.IsNullOrEmpty(version) || string.IsNullOrEmpty(md5) || data == null)
{
UpdateStatus("在线MD5文件无效");
return false;
}
return true;
}
/// <summary>
/// 递归比较本地文件和在线MD5数据识别需要更新的文件
/// </summary>
/// <param name="onlineData">在线文件清单的JSON数据</param>
/// <param name="currentPath">当前递归路径,用于构建完整的文件路径</param>
/// <returns>包含需要更新的文件路径和对应MD5值的字典</returns>
private Dictionary<string, string> CompareMd5Data(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 relativePath = string.IsNullOrEmpty(currentPath) ? key : Path.Combine(currentPath, key);
string localFullPath = Path.Combine(_baseDirectory, relativePath);
if (onlineValue.Type == JTokenType.String)
{
string expectedMd5 = onlineValue.ToString();
if (!File.Exists(localFullPath))
{
if (!differences.ContainsKey(relativePath))
{
differences[relativePath] = expectedMd5;
}
}
else
{
string localMd5 = CalculateMD5FromFile(localFullPath);
if (!localMd5.Equals(expectedMd5, StringComparison.OrdinalIgnoreCase))
{
if (!differences.ContainsKey(relativePath))
{
differences[relativePath] = expectedMd5;
}
}
}
}
else if (onlineValue.Type == JTokenType.Object)
{
var subDifferences = CompareMd5Data((JObject)onlineValue, relativePath);
foreach (var diff in subDifferences)
{
if (!differences.ContainsKey(diff.Key))
{
differences[diff.Key] = diff.Value;
}
}
}
}
return differences;
}
/// <summary>
/// 批量下载文件列表并进行MD5验证支持重试机制
/// </summary>
/// <param name="fileList">包含文件路径和期望MD5值的字典</param>
private async Task DownloadAndVerifyFiles(Dictionary<string, string> fileList)
{
_totalCount = fileList.Count;
_completedCount = 0;
_downloadedFiles.Clear();
var failedFiles = new ConcurrentDictionary<string, string>();
await PerformDownloads(fileList, failedFiles);
if (!failedFiles.IsEmpty)
{
UpdateStatus($"有 {failedFiles.Count} 个文件下载失败,开始重试...");
var stillFailing = await RetryFailedFilesAsync(new Dictionary<string, string>(failedFiles));
if (stillFailing.Any())
{
UpdateStatus($"重试后仍有 {stillFailing.Count} 个文件下载失败。");
}
}
if (_completedCount == 0 && fileList.Count > 0)
{
throw new Exception("所有文件下载失败。");
}
await VerifyAndSaveAllFiles();
}
/// <summary>
/// 使用信号量控制并发数量,执行多个文件的并发下载任务
/// </summary>
/// <param name="filesToDownload">包含文件路径和MD5值的下载任务字典</param>
/// <param name="failedDownloads">用于收集下载失败文件的线程安全集合</param>
private async Task PerformDownloads(IDictionary<string, string> filesToDownload, ConcurrentDictionary<string, string> failedDownloads)
{
var semaphore = new SemaphoreSlim(MaxConcurrentDownloads);
var downloadTasks = filesToDownload.Select(async (file) =>
{
await semaphore.WaitAsync();
try
{
bool success = await AttemptDownloadAsync(file.Key, file.Value);
if (success)
{
lock (_downloadedFiles)
{
_downloadedFiles[file.Key] = file.Value;
}
Interlocked.Increment(ref _completedCount);
int progress = (int)((double)_completedCount / _totalCount * 95);
UpdateProgressValue(progress);
UpdateStatus($"{Path.GetFileName(file.Key)} {_completedCount}/{_totalCount}");
}
else
{
failedDownloads.TryAdd(file.Key, file.Value);
}
}
finally
{
semaphore.Release();
}
});
await Task.WhenAll(downloadTasks);
}
/// <summary>
/// 尝试从多个数据源下载单个文件优先使用123盘OSS作为备用
/// </summary>
/// <param name="filePath">文件的相对路径</param>
/// <param name="expectedMd5">文件的期望MD5值用于验证完整性</param>
/// <returns>下载成功返回true否则返回false</returns>
private async Task<bool> AttemptDownloadAsync(string filePath, string expectedMd5)
{
string tempFilePath = Path.Combine(_tempDirectory, filePath);
string fileName = Path.GetFileName(filePath);
try
{
if (await CheckExistingTempFile(tempFilePath, expectedMd5, fileName))
{
return true;
}
UpdateStatus($"{fileName} {_completedCount + 1}/{_totalCount}");
if (await DownloadFileFromOneDrive(filePath, expectedMd5, tempFilePath))
{
return true;
}
UpdateStatus($"{fileName} {_completedCount + 1}/{_totalCount}");
string ossKey = $"File/{expectedMd5}";
var obj = _ossClient.GetObject(OssBucketName, ossKey);
string tempDir = Path.GetDirectoryName(tempFilePath);
if (!Directory.Exists(tempDir))
{
Directory.CreateDirectory(tempDir);
}
using (var fileStream = File.Create(tempFilePath))
{
await obj.Content.CopyToAsync(fileStream);
}
return true;
}
catch (Exception ex) when (ex is OssException || ex is WebException)
{
UpdateStatus($"{fileName} {_completedCount + 1}/{_totalCount}");
string ossKey = $"File/{expectedMd5}";
return await DownloadFileWithFallback(ossKey, tempFilePath);
}
catch (Exception ex)
{
UpdateStatus($"下载异常: {fileName} - {ex.Message}");
return false;
}
}
/// <summary>
/// 检查临时目录中是否已存在完整的文件通过MD5验证文件完整性
/// </summary>
/// <param name="tempFilePath">临时文件的完整路径</param>
/// <param name="expectedMd5">文件的期望MD5哈希值</param>
/// <param name="fileName">文件名,用于状态显示</param>
/// <returns>如果文件存在且MD5匹配返回true否则返回false</returns>
private async Task<bool> CheckExistingTempFile(string tempFilePath, string expectedMd5, string fileName)
{
try
{
if (!File.Exists(tempFilePath))
{
return false;
}
UpdateStatus($"检查已存在的临时文件: {fileName}");
string actualMd5 = await Task.Run(() => CalculateMD5FromFile(tempFilePath));
if (actualMd5.Equals(expectedMd5, StringComparison.OrdinalIgnoreCase))
{
UpdateStatus($"临时文件完整,跳过下载: {fileName}");
return true;
}
else
{
UpdateStatus($"临时文件不完整,重新下载: {fileName}");
File.Delete(tempFilePath);
return false;
}
}
catch (Exception ex)
{
UpdateStatus($"检查临时文件时出错,将重新下载: {fileName} - {ex.Message}");
try
{
if (File.Exists(tempFilePath))
{
File.Delete(tempFilePath);
}
}
catch
{
}
return false;
}
}
/// <summary>
/// 从123盘下载文件先尝试主域名失败后自动切换到备用域名
/// </summary>
/// <param name="baseUrl">123盘的基础URL地址</param>
/// <param name="fileName">要下载的文件名</param>
/// <param name="localPath">文件保存的本地路径</param>
/// <returns>下载成功返回true两个域名都失败则返回false</returns>
private async Task<bool> DownloadFromOneDrive(string baseUrl, string fileName, string localPath)
{
try
{
UpdateStatus($"正在从123盘下载: {fileName}");
string authUrl = GenerateAuthUrl($"http://{OneDriveMainDomain}{OneDrivePath}", fileName);
UpdateStatus($"使用主域名下载文件...");
var request = new HttpRequestMessage(HttpMethod.Get, authUrl) { Version = HttpVersion.Version11 };
request.Headers.Add("User-Agent", "CheckDownload/1.0");
using (var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead))
{
response.EnsureSuccessStatusCode();
string localDir = Path.GetDirectoryName(localPath);
if (!Directory.Exists(localDir))
{
Directory.CreateDirectory(localDir);
}
using (var remote = await response.Content.ReadAsStreamAsync())
using (var localFile = File.Create(localPath))
{
await remote.CopyToAsync(localFile);
}
UpdateStatus($"123盘主域名下载成功: {fileName}");
return true;
}
}
catch (Exception ex)
{
UpdateStatus($"123盘主域名下载失败尝试备用域名: {fileName}");
}
try
{
UpdateStatus($"正在从123盘备用域名下载: {fileName}");
string authUrl = GenerateAuthUrl($"http://{OneDriveBackupDomain}{OneDrivePath}", fileName);
UpdateStatus($"使用备用域名下载文件...");
var request = new HttpRequestMessage(HttpMethod.Get, authUrl) { Version = HttpVersion.Version11 };
request.Headers.Add("User-Agent", "CheckDownload/1.0");
using (var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead))
{
response.EnsureSuccessStatusCode();
string localDir = Path.GetDirectoryName(localPath);
if (!Directory.Exists(localDir))
{
Directory.CreateDirectory(localDir);
}
using (var remote = await response.Content.ReadAsStreamAsync())
using (var localFile = File.Create(localPath))
{
await remote.CopyToAsync(localFile);
}
UpdateStatus($"123盘备用域名下载成功: {fileName}");
return true;
}
}
catch (Exception ex)
{
UpdateStatus($"123盘备用域名下载失败: {fileName} - {ex.Message}");
return false;
}
}
/// <summary>
/// 根据文件MD5从123盘下载文件使用主备域名双重保障
/// </summary>
/// <param name="filePath">文件的相对路径(仅用于日志显示)</param>
/// <param name="expectedMd5">文件的MD5值用于构建123盘存储路径</param>
/// <param name="localPath">文件保存的本地完整路径</param>
/// <returns>下载成功返回true两个域名都失败则返回false</returns>
private async Task<bool> DownloadFileFromOneDrive(string filePath, string expectedMd5, string localPath)
{
string fileName = $"File/{expectedMd5}";
try
{
string authUrl = GenerateAuthUrl($"http://{OneDriveMainDomain}{OneDrivePath}", fileName);
var request = new HttpRequestMessage(HttpMethod.Get, authUrl) { Version = HttpVersion.Version11 };
request.Headers.Add("User-Agent", "CheckDownload/1.0");
using (var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead))
{
response.EnsureSuccessStatusCode();
string localDir = Path.GetDirectoryName(localPath);
if (!Directory.Exists(localDir))
{
Directory.CreateDirectory(localDir);
}
using (var remote = await response.Content.ReadAsStreamAsync())
using (var localFile = File.Create(localPath))
{
await remote.CopyToAsync(localFile);
}
return true;
}
}
catch (Exception ex)
{
}
try
{
string authUrl = GenerateAuthUrl($"http://{OneDriveBackupDomain}{OneDrivePath}", fileName);
var request = new HttpRequestMessage(HttpMethod.Get, authUrl) { Version = HttpVersion.Version11 };
request.Headers.Add("User-Agent", "CheckDownload/1.0");
using (var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead))
{
response.EnsureSuccessStatusCode();
string localDir = Path.GetDirectoryName(localPath);
if (!Directory.Exists(localDir))
{
Directory.CreateDirectory(localDir);
}
using (var remote = await response.Content.ReadAsStreamAsync())
using (var localFile = File.Create(localPath))
{
await remote.CopyToAsync(localFile);
}
return true;
}
}
catch (Exception ex)
{
return false;
}
}
/// <summary>
/// 对下载失败的文件进行重试,最多重试指定次数
/// </summary>
/// <param name="failedFiles">包含失败文件路径和MD5值的字典</param>
/// <returns>重试后仍然失败的文件字典</returns>
private async Task<Dictionary<string, string>> RetryFailedFilesAsync(Dictionary<string, string> failedFiles)
{
var filesToRetry = new Dictionary<string, string>(failedFiles);
for (int i = 0; i < MaxDownloadRetries && filesToRetry.Any(); i++)
{
UpdateStatus($"第 {i + 1} 次重试,剩余 {filesToRetry.Count} 个文件...");
var failedThisRound = new ConcurrentDictionary<string, string>();
await PerformDownloads(filesToRetry, failedThisRound);
filesToRetry = new Dictionary<string, string>(failedThisRound);
if (filesToRetry.Any() && i < MaxDownloadRetries - 1)
{
UpdateStatus($"等待 3 秒后进行下一次重试...");
await Task.Delay(3000);
}
}
return filesToRetry;
}
/// <summary>
/// 使用多个DNS服务器解析域名获取IP地址列表提高解析成功率
/// </summary>
/// <param name="domain">要解析的目标域名</param>
/// <returns>解析成功的IP地址列表解析失败返回空列表</returns>
private async Task<List<string>> GetIpAddressesForDomain(string domain)
{
foreach (var dnsServer in _dnsServers)
{
try
{
var requestUri = $"http://{dnsServer}/resolve?name={domain}&type=1&short=1";
var request = new HttpRequestMessage(HttpMethod.Get, requestUri) { Version = HttpVersion.Version11 };
request.Headers.Add("User-Agent", "CheckDownload/1.0");
request.Headers.Host = dnsServer;
var response = await _httpClient.SendAsync(request);
response.EnsureSuccessStatusCode();
string responseBody = await response.Content.ReadAsStringAsync();
var ips = JsonConvert.DeserializeObject<List<string>>(responseBody);
if (ips != null && ips.Any())
{
UpdateStatus($"通过 {dnsServer} 成功解析域名 {domain}");
return ips;
}
}
catch (Exception ex)
{
UpdateStatus($"通过 {dnsServer} 解析域名失败,尝试下一个...");
await Task.Delay(500);
}
}
UpdateStatus($"所有DNS服务器均无法解析域名: {domain}");
return new List<string>();
}
/// <summary>
/// 验证所有下载文件的MD5完整性并保存到目标位置处理文件占用情况
/// </summary>
private async Task VerifyAndSaveAllFiles()
{
UpdateStatus("正在校验文件...");
var failedFiles = new ConcurrentBag<string>();
var filesForScripting = new ConcurrentBag<(string original, string newFile)>();
int totalFiles = _downloadedFiles.Count;
int completed = 0;
var semaphore = new SemaphoreSlim(Environment.ProcessorCount);
var tasks = _downloadedFiles.Select(async item =>
{
await semaphore.WaitAsync();
try
{
string relativePath = item.Key;
string expectedMd5 = item.Value;
string tempFilePath = Path.Combine(_tempDirectory, relativePath);
string actualMd5 = CalculateMD5FromFile(tempFilePath);
if (actualMd5 != expectedMd5.ToLower())
{
failedFiles.Add(relativePath);
return;
}
string localPath = Path.Combine(_baseDirectory, relativePath);
string localDir = Path.GetDirectoryName(localPath);
if (!Directory.Exists(localDir))
{
Directory.CreateDirectory(localDir);
}
if (!await TryMoveFileAsync(tempFilePath, localPath))
{
string backupPath = localPath + ".new";
File.Move(tempFilePath, backupPath);
filesForScripting.Add((localPath, backupPath));
}
}
finally
{
int done = Interlocked.Increment(ref completed);
int percentage = 95 + (int)(done * 5.0 / totalFiles);
UpdateProgressValue(Math.Min(100, percentage));
semaphore.Release();
}
});
await Task.WhenAll(tasks);
if (filesForScripting.Any())
{
CreateReplaceScriptForAll(filesForScripting.ToList());
}
foreach (var failed in failedFiles)
{
_downloadedFiles.Remove(failed);
}
if (failedFiles.Count > 0)
{
throw new Exception($"{failedFiles.Count}个文件校验失败");
}
else
{
UpdateStatus("所有文件校验和保存成功");
UpdateProgressValue(100);
}
}
/// <summary>
/// 尝试将文件从临时位置移动到目标位置(异步),若文件被占用则等待一秒后重试一次
/// </summary>
/// <param name="sourcePath">源文件的完整路径</param>
/// <param name="targetPath">目标文件的完整路径</param>
/// <returns>移动成功返回true失败返回false</returns>
private async Task<bool> TryMoveFileAsync(string sourcePath, string targetPath)
{
try
{
File.Move(sourcePath, targetPath);
return true;
}
catch (IOException)
{
UpdateStatus($"文件被占用,尝试解锁...");
try
{
await Task.Delay(1000);
File.Move(sourcePath, targetPath);
UpdateStatus($"文件解锁成功并已更新");
return true;
}
catch
{
UpdateStatus($"文件仍被占用,无法移动");
return false;
}
}
catch (Exception ex)
{
UpdateStatus($"移动文件时发生错误: {ex.Message}");
return false;
}
}
/// <summary>
/// 为被占用文件创建批处理脚本,在程序退出后自动完成文件替换
/// </summary>
/// <param name="files">包含原始文件路径和新文件路径的元组列表</param>
private void CreateReplaceScriptForAll(List<(string original, string newFile)> files)
{
if (!files.Any()) return;
try
{
string batchFilePath = Path.Combine(_baseDirectory, "update_files.bat");
string processId = Process.GetCurrentProcess().Id.ToString();
var batchContent = new StringBuilder();
batchContent.AppendLine("@echo off");
batchContent.AppendLine("chcp 65001 > nul");
batchContent.AppendLine(":check_process");
batchContent.AppendLine($"tasklist /FI \"PID eq {processId}\" 2>NUL | find /I \"{processId}\" >NUL");
batchContent.AppendLine("if %ERRORLEVEL% == 0 (");
batchContent.AppendLine(" timeout /t 1 /nobreak > NUL");
batchContent.AppendLine(" goto check_process");
batchContent.AppendLine(")");
batchContent.AppendLine("timeout /t 2 /nobreak > NUL");
foreach (var file in files)
{
batchContent.AppendLine($"del \"{file.original}\" /f /q");
batchContent.AppendLine($"move \"{file.newFile}\" \"{file.original}\"");
}
batchContent.AppendLine("del \"%~f0\" /f /q");
batchContent.AppendLine("exit");
File.WriteAllText(batchFilePath, batchContent.ToString(), new UTF8Encoding(false));
var startInfo = new ProcessStartInfo
{
FileName = "cmd.exe",
Arguments = $"/c \"{batchFilePath}\"",
CreateNoWindow = true,
UseShellExecute = false,
WindowStyle = ProcessWindowStyle.Hidden
};
Process.Start(startInfo);
}
catch (Exception ex)
{
UpdateStatus($"创建替换脚本时出错: {ex.Message}");
}
}
/// <summary>
/// 初始化程序使用的临时目录,用于存储下载过程中的临时文件
/// </summary>
private void InitializeTempDirectory()
{
try
{
_tempDirectory = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"Temp",
"CheckDownload"
);
if (!Directory.Exists(_tempDirectory))
{
Directory.CreateDirectory(_tempDirectory);
}
}
catch (Exception ex)
{
UpdateStatus($"初始化临时目录失败: {ex.Message}");
}
}
/// <summary>
/// 计算字符串的MD5哈希值返回32位小写十六进制字符串
/// </summary>
/// <param name="input">要计算哈希值的字符串</param>
/// <returns>32位小写十六进制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哈希值返回32位小写十六进制字符串
/// </summary>
/// <param name="data">要计算哈希值的字节数组</param>
/// <returns>32位小写十六进制MD5哈希值</returns>
private string CalculateMD5(byte[] data)
{
using (var md5 = MD5.Create())
{
byte[] hashBytes = md5.ComputeHash(data);
return BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant();
}
}
/// <summary>
/// 从阿里云OSS下载文件包含SDK直连和IP直连两种备用方案
/// </summary>
/// <param name="ossKey">OSS对象的存储键值</param>
/// <param name="localPath">文件保存的本地路径</param>
/// <returns>下载成功返回true所有方案都失败返回false</returns>
private async Task<bool> DownloadFileWithFallback(string ossKey, string localPath)
{
try
{
var obj = _ossClient.GetObject(OssBucketName, ossKey);
string localDir = Path.GetDirectoryName(localPath);
if (!Directory.Exists(localDir))
{
Directory.CreateDirectory(localDir);
}
using (var fileStream = File.Create(localPath))
{
obj.Content.CopyTo(fileStream);
}
return true;
}
catch (Exception ex) when (ex is OssException || ex is WebException || ex is IOException)
{
UpdateStatus($"主下载失败,尝试备用方案...");
}
var domain = new Uri("https://" + OssEndpoint).Host;
List<string> ips = await GetIpAddressesForDomain(domain);
if (ips == null || ips.Count == 0)
{
UpdateStatus($"无法获取IP地址");
return false;
}
var req = new GeneratePresignedUriRequest(OssBucketName, ossKey, SignHttpMethod.Get)
{
Expiration = DateTime.Now.AddMinutes(10)
};
var signedUrl = _ossClient.GeneratePresignedUri(req).ToString();
var signedUri = new Uri(signedUrl);
foreach (var ip in ips)
{
try
{
var ipUrl = signedUrl.Replace(signedUri.Host, ip);
var request = new HttpRequestMessage(HttpMethod.Get, ipUrl) { Version = HttpVersion.Version11 };
request.Headers.Host = signedUri.Host;
using (var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead))
{
response.EnsureSuccessStatusCode();
string localDir = Path.GetDirectoryName(localPath);
if (!Directory.Exists(localDir))
{
Directory.CreateDirectory(localDir);
}
using (var remote = await response.Content.ReadAsStreamAsync())
using (var localFile = File.Create(localPath))
{
await remote.CopyToAsync(localFile);
}
}
return true;
}
catch (Exception)
{
UpdateStatus($"备用方案用IP {ip} 下载失败");
}
}
return false;
}
/// <summary>
/// 生成基于当前UTC时间的Unix时间戳并添加指定的秒数偏移
/// </summary>
/// <param name="number">要在当前时间戳基础上增加的秒数</param>
/// <returns>Unix时间戳</returns>
private long GenerateTimestamp(int number)
{
return (long)(DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1))).TotalSeconds + number;
}
/// <summary>
/// 使用指定数字作为随机种子生成确定性的32位UUID字符串
/// </summary>
/// <param name="number">用作随机种子的数字</param>
/// <returns>不含连字符的32位UUID字符串</returns>
private string GenerateUUID(int number)
{
var random = new Random(number);
var guid = new byte[16];
random.NextBytes(guid);
guid[7] = (byte)((guid[7] & 0x0F) | 0x40);
guid[8] = (byte)((guid[8] & 0x3F) | 0x80);
var resultGuid = new Guid(guid);
return resultGuid.ToString("N").Replace("-", "");
}
/// <summary>
/// 计算字符串的MD5哈希值用于123盘鉴权签名生成
/// </summary>
/// <param name="input">要计算哈希值的输入字符串</param>
/// <returns>32位小写十六进制MD5哈希值</returns>
private string GenerateMD5(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>
/// 为123盘文件访问生成带有时间戳和签名验证的鉴权URL
/// </summary>
/// <param name="url">123盘的基础访问URL</param>
/// <param name="file">要访问的文件名或路径</param>
/// <returns>包含鉴权参数的完整访问URL</returns>
private string GenerateAuthUrl(string url, string file)
{
try
{
long timestamp = GenerateTimestamp(OneDriveAuthTimeout);
string rand = GenerateUUID(16);
string pathPart = ExtractPathFromUrl(url);
string fullUrl = $"{url}/{file}";
string signString = $"/{pathPart}/{file}-{timestamp}-{rand}-{OneDriveUid}-{OneDriveAuthKey}";
string signature = GenerateMD5(signString);
string authUrl = $"{fullUrl}?auth_key={timestamp}-{rand}-{OneDriveUid}-{signature}";
return authUrl;
}
catch (Exception ex)
{
UpdateStatus($"生成鉴权URL失败: {ex.Message}");
return $"{url}/{file}";
}
}
/// <summary>
/// 从完整URL中解析并提取域名后的路径部分用于生成123盘签名
/// </summary>
/// <param name="url">要解析的完整URL地址</param>
/// <returns>去除域名和协议后的路径字符串</returns>
private string ExtractPathFromUrl(string url)
{
string pathPart = "";
try
{
var uri = new Uri(url);
pathPart = uri.AbsolutePath.TrimStart('/');
if (string.IsNullOrEmpty(pathPart))
{
string basePattern = $"{uri.Scheme}://{uri.Host}";
if (url.StartsWith(basePattern))
{
pathPart = url.Substring(basePattern.Length).TrimStart('/');
}
}
}
catch
{
int protocolIndex = url.IndexOf("://");
if (protocolIndex > 0)
{
int domainEndIndex = url.IndexOf('/', protocolIndex + 3);
if (domainEndIndex > 0)
{
pathPart = url.Substring(domainEndIndex + 1);
}
}
}
return pathPart;
}
/// <summary>
/// 下载MD5配置文件优先使用123盘失败后切换到阿里云OSS备用方案
/// </summary>
/// <param name="fileName">MD5配置文件名</param>
/// <param name="localPath">文件保存的本地路径</param>
/// <returns>下载成功返回true所有方案都失败返回false</returns>
private async Task<bool> DownloadMd5FileWithFallback(string fileName, string localPath)
{
try
{
UpdateStatus("尝试从123盘下载MD5文件...");
if (await DownloadFromOneDrive($"http://{OneDriveMainDomain}{OneDrivePath}", fileName, localPath))
{
return true;
}
UpdateStatus("123盘下载失败尝试阿里云OSS备用方案...");
return await DownloadFileWithFallback(fileName, localPath);
}
catch (Exception ex)
{
UpdateStatus($"所有下载方式都失败: {ex.Message}");
return false;
}
}
/// <summary>
/// 清理程序使用的临时目录,删除所有下载过程中产生的临时文件
/// </summary>
private void CleanupTempDirectory()
{
try
{
string tempPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"Temp",
"CheckDownload"
);
if (Directory.Exists(tempPath))
{
UpdateStatus("清理临时文件...");
Directory.Delete(tempPath, true);
}
}
catch (Exception ex)
{
}
}
/// <summary>
/// 通过分析md5.json文件内容智能确定项目基准目录用于正确定位文件更新路径
/// </summary>
/// <param name="data">md5.json文件中的数据对象</param>
private void InitializeBaseDirectoryFromMd5Data(JObject data)
{
try
{
string currentProgramName = Path.GetFileName(Application.ExecutablePath);
string programPathInMd5 = FindFileInMd5Data(data, currentProgramName);
if (!string.IsNullOrEmpty(programPathInMd5))
{
string programDirInMd5 = Path.GetDirectoryName(programPathInMd5);
if (string.IsNullOrEmpty(programDirInMd5))
{
programDirInMd5 = "";
}
string currentProgramDir = Application.StartupPath;
if (string.IsNullOrEmpty(programDirInMd5))
{
_baseDirectory = currentProgramDir;
}
else
{
_baseDirectory = FindProjectBaseDirectory(currentProgramDir, programDirInMd5);
}
}
else
{
_baseDirectory = Application.StartupPath;
}
}
catch (Exception ex)
{
_baseDirectory = Application.StartupPath;
}
}
/// <summary>
/// 在md5.json的数据结构中递归搜索指定的文件名
/// </summary>
/// <param name="data">md5.json解析后的JSON对象</param>
/// <param name="fileName">要搜索的目标文件名</param>
/// <param name="currentPath">当前递归的路径,用于构建完整路径</param>
/// <returns>找到的文件在md5.json中的完整相对路径未找到返回null</returns>
private string FindFileInMd5Data(JObject data, string fileName, string currentPath = "")
{
foreach (var property in data.Properties())
{
string key = property.Name;
JToken value = property.Value;
string fullPath = string.IsNullOrEmpty(currentPath) ? key : Path.Combine(currentPath, key);
if (value.Type == JTokenType.String)
{
if (string.Equals(key, fileName, StringComparison.OrdinalIgnoreCase))
{
return fullPath;
}
}
else if (value.Type == JTokenType.Object)
{
string result = FindFileInMd5Data((JObject)value, fileName, fullPath);
if (!string.IsNullOrEmpty(result))
{
return result;
}
}
}
return null;
}
/// <summary>
/// 通过分析程序在md5.json中的相对路径向上递归查找项目根目录
/// </summary>
/// <param name="currentDir">当前程序运行的目录</param>
/// <param name="expectedRelativePath">程序在md5.json中的相对路径</param>
/// <returns>项目的基准目录路径</returns>
private string FindProjectBaseDirectory(string currentDir, string expectedRelativePath)
{
try
{
string[] expectedDirs = expectedRelativePath.Split(new char[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar },
StringSplitOptions.RemoveEmptyEntries);
string checkDir = currentDir;
for (int i = 0; i < expectedDirs.Length; i++)
{
string parentDir = Directory.GetParent(checkDir)?.FullName;
if (string.IsNullOrEmpty(parentDir))
{
break;
}
string currentDirName = Path.GetFileName(checkDir);
string expectedDirName = expectedDirs[expectedDirs.Length - 1 - i];
if (string.Equals(currentDirName, expectedDirName, StringComparison.OrdinalIgnoreCase))
{
if (i == expectedDirs.Length - 1)
{
return parentDir;
}
checkDir = parentDir;
}
else
{
break;
}
}
return currentDir;
}
catch (Exception ex)
{
return currentDir;
}
}
/// <summary>
/// 使用文件流计算指定文件的 MD5 哈希值32 位小写十六进制)。
/// </summary>
/// <param name="filePath">要计算 MD5 的文件完整路径</param>
/// <returns>32 位小写十六进制 MD5 字符串</returns>
private string CalculateMD5FromFile(string filePath)
{
using (var fs = File.OpenRead(filePath))
using (var md5 = MD5.Create())
{
var hashBytes = md5.ComputeHash(fs);
return BitConverter.ToString(hashBytes).Replace("-", string.Empty).ToLowerInvariant();
}
}
}
}