1258 lines
49 KiB
C#
1258 lines
49 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.ComponentModel;
|
||
using System.Data;
|
||
using System.Diagnostics;
|
||
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 LanzouCloudSolve;
|
||
using System.Collections.Concurrent;
|
||
|
||
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盘配置
|
||
// 注意:如需修改123盘路径,只需修改 OneDrivePath 常量即可,主备域名会自动使用新路径
|
||
// 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; // 10分钟
|
||
|
||
// 网络优化: 静态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 Dictionary<string, (byte[] Data, string ExpectedMd5)> _downloadedFiles = new Dictionary<string, (byte[], string)>();
|
||
// 已完成的下载数量
|
||
private int _completedCount = 0;
|
||
// 总下载数量
|
||
private int _totalCount = 0;
|
||
// 临时文件夹路径
|
||
private string _tempDirectory;
|
||
|
||
/// <summary>
|
||
/// 初始化窗体
|
||
/// </summary>
|
||
public Update()
|
||
{
|
||
InitializeComponent();
|
||
ConfigureProgressBar();
|
||
InitializeTempDirectory();
|
||
|
||
// 注册应用程序退出时清理临时文件
|
||
Application.ApplicationExit += (s, e) => CleanupTempDirectory();
|
||
}
|
||
|
||
/// <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
|
||
{
|
||
// 确保在更新完成后清理临时文件
|
||
CleanupTempDirectory();
|
||
}
|
||
}
|
||
|
||
/// <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;
|
||
}
|
||
|
||
UpdateStatus("比较本地和在线MD5文件...");
|
||
var compareResult = CompareMd5Data(onlineData.Data);
|
||
if (compareResult.Count == 0)
|
||
{
|
||
UpdateStatus("所有文件都是最新的,无需更新");
|
||
UpdateProgressValue(100);
|
||
await Task.Delay(2000);
|
||
this.Close();
|
||
return;
|
||
}
|
||
|
||
UpdateStatus("下载并验证文件...");
|
||
_totalCount = compareResult.Count;
|
||
await DownloadAndVerifyFiles(compareResult);
|
||
UpdateProgressValue(100);
|
||
|
||
UpdateStatus("更新完成");
|
||
await Task.Delay(1000);
|
||
this.Close();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
UpdateStatus($"更新失败: {ex.Message}");
|
||
await Task.Delay(3000);
|
||
this.Close();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 清理旧的更新文件(.new后缀文件)
|
||
/// </summary>
|
||
private void CleanupNewFiles()
|
||
{
|
||
try
|
||
{
|
||
string startupPath = Application.StartupPath;
|
||
var newFiles = Directory.GetFiles(startupPath, "*.new", SearchOption.AllDirectories);
|
||
if (newFiles.Length > 0)
|
||
{
|
||
UpdateStatus("正在清理旧的更新文件...");
|
||
foreach (var file in newFiles)
|
||
{
|
||
try
|
||
{
|
||
File.Delete(file);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Debug.WriteLine($"无法删除旧的更新文件: {file} - {ex.Message}");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Debug.WriteLine($"清理旧的更新文件时出错: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <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>
|
||
/// 验证在线数据的有效性
|
||
/// </summary>
|
||
/// <param name="version">版本号</param>
|
||
/// <param name="md5">MD5值</param>
|
||
/// <param name="data">数据对象</param>
|
||
/// <returns>数据是否有效</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">在线数据</param>
|
||
/// <param name="currentPath">当前路径</param>
|
||
/// <returns>需要更新的文件列表</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;
|
||
|
||
// 始终使用 Path.Combine 来构造路径,确保跨平台和正确性
|
||
string relativePath = string.IsNullOrEmpty(currentPath) ? key : Path.Combine(currentPath, key);
|
||
string localFullPath = Path.Combine(Application.StartupPath, relativePath);
|
||
|
||
if (onlineValue.Type == JTokenType.String)
|
||
{
|
||
string expectedMd5 = onlineValue.ToString();
|
||
if (!File.Exists(localFullPath))
|
||
{
|
||
if (!differences.ContainsKey(relativePath))
|
||
{
|
||
differences[relativePath] = expectedMd5;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
string localMd5 = CalculateMD5(File.ReadAllBytes(localFullPath));
|
||
if (localMd5.Equals(expectedMd5, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
// MD5一致,文件无需更新
|
||
}
|
||
else
|
||
{
|
||
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>
|
||
/// 下载并验证文件列表
|
||
/// </summary>
|
||
/// <param name="fileList">需要下载的文件列表</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("所有文件下载失败。");
|
||
}
|
||
|
||
VerifyAndSaveAllFiles();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 执行并发下载任务
|
||
/// </summary>
|
||
/// <param name="filesToDownload">要下载的文件列表</param>
|
||
/// <param name="failedDownloads">下载失败的文件集合</param>
|
||
private async Task PerformDownloads(IDictionary<string, string> filesToDownload, ConcurrentDictionary<string, string> failedDownloads)
|
||
{
|
||
var semaphore = new SemaphoreSlim(4); // 限制4个并发下载
|
||
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] = (null, file.Value);
|
||
}
|
||
Interlocked.Increment(ref _completedCount);
|
||
|
||
// 实时更新进度(0-95%用于下载)
|
||
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>下载是否成功</returns>
|
||
private async Task<bool> AttemptDownloadAsync(string filePath, string expectedMd5)
|
||
{
|
||
string tempFilePath = Path.Combine(_tempDirectory, filePath);
|
||
string fileName = Path.GetFileName(filePath);
|
||
|
||
try
|
||
{
|
||
// 1. 首先尝试从123盘下载
|
||
UpdateStatus($"{fileName} {_completedCount + 1}/{_totalCount}");
|
||
if (await DownloadFileFromOneDrive(filePath, expectedMd5, tempFilePath))
|
||
{
|
||
return true;
|
||
}
|
||
|
||
// 2. 如果123盘下载失败,使用阿里云OSS作为备用
|
||
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>
|
||
/// 从123盘下载在线MD5文件(主域名+备用域名)
|
||
/// </summary>
|
||
/// <param name="baseUrl">123盘基础链接地址</param>
|
||
/// <param name="fileName">要下载的文件名</param>
|
||
/// <param name="localPath">本地保存路径</param>
|
||
/// <returns>下载是否成功</returns>
|
||
private async Task<bool> DownloadFromOneDrive(string baseUrl, string fileName, string localPath)
|
||
{
|
||
// 1. 首先尝试主域名
|
||
try
|
||
{
|
||
UpdateStatus($"正在从123盘下载: {fileName}");
|
||
|
||
// 生成123盘鉴权URL(主域名)
|
||
string authUrl = GenerateAuthUrl($"http://{OneDriveMainDomain}{OneDrivePath}", fileName);
|
||
|
||
UpdateStatus($"使用主域名下载文件...");
|
||
|
||
// 创建HTTP请求
|
||
var request = new HttpRequestMessage(HttpMethod.Get, authUrl);
|
||
request.Headers.Add("User-Agent", "CheckDownload/1.0");
|
||
|
||
// 发送请求并获取响应
|
||
var response = await _httpClient.SendAsync(request);
|
||
response.EnsureSuccessStatusCode();
|
||
|
||
// 读取文件数据
|
||
byte[] fileData = await response.Content.ReadAsByteArrayAsync();
|
||
|
||
// 确保本地目录存在
|
||
string localDir = Path.GetDirectoryName(localPath);
|
||
if (!Directory.Exists(localDir))
|
||
{
|
||
Directory.CreateDirectory(localDir);
|
||
}
|
||
|
||
// 保存文件到本地
|
||
File.WriteAllBytes(localPath, fileData);
|
||
|
||
UpdateStatus($"123盘主域名下载成功: {fileName}");
|
||
return true;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
UpdateStatus($"123盘主域名下载失败,尝试备用域名: {fileName}");
|
||
Debug.WriteLine($"123盘主域名下载异常: {ex}");
|
||
}
|
||
|
||
// 2. 如果主域名失败,尝试备用域名
|
||
try
|
||
{
|
||
UpdateStatus($"正在从123盘备用域名下载: {fileName}");
|
||
|
||
// 生成123盘鉴权URL(备用域名)
|
||
string authUrl = GenerateAuthUrl($"http://{OneDriveBackupDomain}{OneDrivePath}", fileName);
|
||
|
||
UpdateStatus($"使用备用域名下载文件...");
|
||
|
||
// 创建HTTP请求
|
||
var request = new HttpRequestMessage(HttpMethod.Get, authUrl);
|
||
request.Headers.Add("User-Agent", "CheckDownload/1.0");
|
||
|
||
// 发送请求并获取响应
|
||
var response = await _httpClient.SendAsync(request);
|
||
response.EnsureSuccessStatusCode();
|
||
|
||
// 读取文件数据
|
||
byte[] fileData = await response.Content.ReadAsByteArrayAsync();
|
||
|
||
// 确保本地目录存在
|
||
string localDir = Path.GetDirectoryName(localPath);
|
||
if (!Directory.Exists(localDir))
|
||
{
|
||
Directory.CreateDirectory(localDir);
|
||
}
|
||
|
||
// 保存文件到本地
|
||
File.WriteAllBytes(localPath, fileData);
|
||
|
||
UpdateStatus($"123盘备用域名下载成功: {fileName}");
|
||
return true;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
UpdateStatus($"123盘备用域名下载失败: {fileName} - {ex.Message}");
|
||
Debug.WriteLine($"123盘备用域名下载异常: {ex}");
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从123盘下载单个文件(主域名+备用域名)
|
||
/// </summary>
|
||
/// <param name="filePath">文件相对路径</param>
|
||
/// <param name="expectedMd5">期望的MD5值</param>
|
||
/// <param name="localPath">本地保存路径</param>
|
||
/// <returns>下载是否成功</returns>
|
||
private async Task<bool> DownloadFileFromOneDrive(string filePath, string expectedMd5, string localPath)
|
||
{
|
||
// 在123盘中,文件是通过MD5值存储的,路径为 /File/{MD5值}
|
||
string fileName = $"File/{expectedMd5}";
|
||
|
||
// 1. 首先尝试主域名
|
||
try
|
||
{
|
||
// 生成123盘鉴权URL(主域名)
|
||
string authUrl = GenerateAuthUrl($"http://{OneDriveMainDomain}{OneDrivePath}", fileName);
|
||
|
||
// 创建HTTP请求
|
||
var request = new HttpRequestMessage(HttpMethod.Get, authUrl);
|
||
request.Headers.Add("User-Agent", "CheckDownload/1.0");
|
||
|
||
// 发送请求并获取响应
|
||
var response = await _httpClient.SendAsync(request);
|
||
response.EnsureSuccessStatusCode();
|
||
|
||
// 读取文件数据
|
||
byte[] fileData = await response.Content.ReadAsByteArrayAsync();
|
||
|
||
// 确保本地目录存在
|
||
string localDir = Path.GetDirectoryName(localPath);
|
||
if (!Directory.Exists(localDir))
|
||
{
|
||
Directory.CreateDirectory(localDir);
|
||
}
|
||
|
||
// 保存文件到本地
|
||
File.WriteAllBytes(localPath, fileData);
|
||
|
||
return true;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Debug.WriteLine($"123盘主域名下载文件失败: {filePath} - {ex.Message}");
|
||
}
|
||
|
||
// 2. 如果主域名失败,尝试备用域名
|
||
try
|
||
{
|
||
// 生成123盘鉴权URL(备用域名)
|
||
string authUrl = GenerateAuthUrl($"http://{OneDriveBackupDomain}{OneDrivePath}", fileName);
|
||
|
||
// 创建HTTP请求
|
||
var request = new HttpRequestMessage(HttpMethod.Get, authUrl);
|
||
request.Headers.Add("User-Agent", "CheckDownload/1.0");
|
||
|
||
// 发送请求并获取响应
|
||
var response = await _httpClient.SendAsync(request);
|
||
response.EnsureSuccessStatusCode();
|
||
|
||
// 读取文件数据
|
||
byte[] fileData = await response.Content.ReadAsByteArrayAsync();
|
||
|
||
// 确保本地目录存在
|
||
string localDir = Path.GetDirectoryName(localPath);
|
||
if (!Directory.Exists(localDir))
|
||
{
|
||
Directory.CreateDirectory(localDir);
|
||
}
|
||
|
||
// 保存文件到本地
|
||
File.WriteAllBytes(localPath, fileData);
|
||
|
||
return true;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Debug.WriteLine($"123盘备用域名下载文件失败: {filePath} - {ex.Message}");
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 重试下载失败的文件
|
||
/// </summary>
|
||
/// <param name="failedFiles">失败的文件列表</param>
|
||
/// <returns>仍然失败的文件列表</returns>
|
||
private async Task<Dictionary<string, string>> RetryFailedFilesAsync(Dictionary<string, string> failedFiles)
|
||
{
|
||
const int maxRetries = 2;
|
||
var filesToRetry = new Dictionary<string, string>(failedFiles);
|
||
|
||
for (int i = 0; i < maxRetries && filesToRetry.Any(); i++)
|
||
{
|
||
UpdateStatus($"第 {i + 1} 次重试,剩余 {filesToRetry.Count} 个文件...");
|
||
var failedThisRound = new ConcurrentDictionary<string, string>();
|
||
|
||
// BUG修复: 此处不应清空重试队列
|
||
await PerformDownloads(filesToRetry, failedThisRound);
|
||
|
||
filesToRetry = new Dictionary<string, string>(failedThisRound);
|
||
|
||
if (filesToRetry.Any() && i < maxRetries - 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 = $"https://{dnsServer}/resolve?name={domain}&type=1&short=1";
|
||
var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
|
||
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>
|
||
/// 验证并保存所有下载的文件
|
||
/// </summary>
|
||
private void VerifyAndSaveAllFiles()
|
||
{
|
||
UpdateStatus("正在校验文件...");
|
||
var failedFiles = new List<string>();
|
||
var filesForScripting = new List<(string original, string newFile)>();
|
||
int processedCount = 0;
|
||
int totalFiles = _downloadedFiles.Count;
|
||
|
||
foreach (var item in _downloadedFiles)
|
||
{
|
||
string relativePath = item.Key;
|
||
string expectedMd5 = item.Value.ExpectedMd5;
|
||
string tempFilePath = Path.Combine(_tempDirectory, relativePath);
|
||
|
||
try
|
||
{
|
||
processedCount++;
|
||
UpdateStatus($"正在校验和保存文件 ({processedCount}/{totalFiles}): {relativePath}");
|
||
|
||
// 计算临时文件的MD5
|
||
string actualMd5 = CalculateMD5(File.ReadAllBytes(tempFilePath));
|
||
if (actualMd5 != expectedMd5.ToLower())
|
||
{
|
||
throw new Exception($"MD5校验失败 (期望: {expectedMd5}, 实际: {actualMd5})");
|
||
}
|
||
|
||
string localPath = Path.Combine(Application.StartupPath, relativePath);
|
||
string localDir = Path.GetDirectoryName(localPath);
|
||
|
||
// 确保目标目录存在
|
||
if (!Directory.Exists(localDir))
|
||
{
|
||
Directory.CreateDirectory(localDir);
|
||
}
|
||
|
||
// 尝试移动文件,如果文件被占用则尝试解锁
|
||
if (!TryMoveFile(tempFilePath, localPath))
|
||
{
|
||
// 如果无法移动文件,则使用备用文件名
|
||
string backupPath = localPath + ".new";
|
||
File.Move(tempFilePath, backupPath);
|
||
filesForScripting.Add((localPath, backupPath));
|
||
UpdateStatus($"文件 {relativePath} 正在被占用,将在程序重启后更新");
|
||
}
|
||
else
|
||
{
|
||
UpdateStatus($"文件 {relativePath} 已更新");
|
||
}
|
||
|
||
int percentage = 95 + (int)(processedCount * 5.0 / totalFiles);
|
||
UpdateProgressValue(Math.Min(100, percentage));
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
UpdateStatus($"文件校验失败: {relativePath} - {ex.Message}");
|
||
failedFiles.Add(relativePath);
|
||
}
|
||
}
|
||
|
||
if (filesForScripting.Any())
|
||
{
|
||
CreateReplaceScriptForAll(filesForScripting);
|
||
}
|
||
|
||
foreach (var failedFile in failedFiles)
|
||
{
|
||
_downloadedFiles.Remove(failedFile);
|
||
}
|
||
|
||
if (failedFiles.Count > 0)
|
||
{
|
||
throw new Exception($"{failedFiles.Count}个文件校验失败");
|
||
}
|
||
else
|
||
{
|
||
UpdateStatus("所有文件校验和保存成功");
|
||
UpdateProgressValue(100);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 尝试移动文件,如果文件被占用则尝试解锁
|
||
/// </summary>
|
||
/// <param name="sourcePath">源文件路径</param>
|
||
/// <param name="targetPath">目标文件路径</param>
|
||
/// <returns>移动是否成功</returns>
|
||
private bool TryMoveFile(string sourcePath, string targetPath)
|
||
{
|
||
try
|
||
{
|
||
// 直接尝试移动文件
|
||
File.Move(sourcePath, targetPath);
|
||
return true;
|
||
}
|
||
catch (IOException)
|
||
{
|
||
// 文件被占用,尝试解锁
|
||
UpdateStatus($"文件被占用,尝试解锁...");
|
||
|
||
try
|
||
{
|
||
// 等待一段时间,让文件可能被释放
|
||
Thread.Sleep(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(Application.StartupPath, "update_files.bat");
|
||
string processId = Process.GetCurrentProcess().Id.ToString();
|
||
|
||
var batchContent = new StringBuilder();
|
||
batchContent.AppendLine("@echo off");
|
||
batchContent.AppendLine("chcp 65001 > nul"); // 确保正确处理UTF-8路径
|
||
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");
|
||
|
||
File.WriteAllText(batchFilePath, batchContent.ToString(), new UTF8Encoding(false));
|
||
|
||
var startInfo = new ProcessStartInfo
|
||
{
|
||
FileName = "cmd.exe",
|
||
Arguments = $"/c start \"\" /min \"{batchFilePath}\"",
|
||
CreateNoWindow = true,
|
||
UseShellExecute = true,
|
||
WindowStyle = ProcessWindowStyle.Hidden
|
||
};
|
||
Process.Start(startInfo);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
UpdateStatus($"创建替换脚本时出错: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 初始化临时目录
|
||
/// </summary>
|
||
private void InitializeTempDirectory()
|
||
{
|
||
try
|
||
{
|
||
// 获取用户临时目录
|
||
string userTempPath = Path.Combine(
|
||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||
"Temp",
|
||
"CheckDownload"
|
||
);
|
||
|
||
// 创建临时目录
|
||
_tempDirectory = CreateTempDirectory(userTempPath);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
UpdateStatus($"初始化临时目录失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建带时间戳的临时目录
|
||
/// </summary>
|
||
/// <param name="basePath">基础路径</param>
|
||
/// <returns>临时目录路径</returns>
|
||
private string CreateTempDirectory(string basePath)
|
||
{
|
||
try
|
||
{
|
||
// 确保基础目录存在
|
||
if (!Directory.Exists(basePath))
|
||
{
|
||
Directory.CreateDirectory(basePath);
|
||
}
|
||
|
||
// 创建带时间戳的临时目录
|
||
string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
|
||
string tempDir = Path.Combine(basePath, $"temp_{timestamp}");
|
||
|
||
if (!Directory.Exists(tempDir))
|
||
{
|
||
Directory.CreateDirectory(tempDir);
|
||
}
|
||
|
||
return tempDir;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
UpdateStatus($"创建临时目录失败: {ex.Message}");
|
||
throw;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 清理临时目录和历史遗留文件
|
||
/// </summary>
|
||
private void CleanupTempDirectory()
|
||
{
|
||
try
|
||
{
|
||
// 只删除当前实例创建的临时目录
|
||
if (!string.IsNullOrEmpty(_tempDirectory) && Directory.Exists(_tempDirectory))
|
||
{
|
||
Directory.Delete(_tempDirectory, true);
|
||
Debug.WriteLine($"已清理临时目录: {_tempDirectory}");
|
||
}
|
||
|
||
// 同时清理历史遗留的空目录或非常旧的目录
|
||
string baseTempPath = Path.Combine(
|
||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||
"Temp",
|
||
"CheckDownload"
|
||
);
|
||
|
||
if (!Directory.Exists(baseTempPath)) return;
|
||
|
||
foreach (var dir in Directory.GetDirectories(baseTempPath))
|
||
{
|
||
try
|
||
{
|
||
// 删除超过7天的旧临时文件夹
|
||
if ((DateTime.UtcNow - new DirectoryInfo(dir).CreationTimeUtc).TotalDays > 7)
|
||
{
|
||
Directory.Delete(dir, true);
|
||
Debug.WriteLine($"已清理旧的临时目录: {dir}");
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
// 忽略删除失败的,可能是其他实例正在使用
|
||
Debug.WriteLine($"删除旧临时目录失败: {dir} - {ex.Message}");
|
||
}
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Debug.WriteLine($"清理临时目录时发生错误: {ex.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="data">字节数组</param>
|
||
/// <returns>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文件下载方法
|
||
/// </summary>
|
||
/// <param name="ossKey">OSS对象键</param>
|
||
/// <param name="localPath">本地保存路径</param>
|
||
/// <returns>下载是否成功</returns>
|
||
private async Task<bool> DownloadFileWithFallback(string ossKey, string localPath)
|
||
{
|
||
// 1. 先尝试用 OSS SDK 下载
|
||
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($"主下载失败,尝试备用方案...");
|
||
}
|
||
|
||
// 2. 备用方案:用 IP 下载(带签名URL)
|
||
var domain = new Uri("https://" + OssEndpoint).Host;
|
||
List<string> ips = await GetIpAddressesForDomain(domain);
|
||
if (ips == null || ips.Count == 0)
|
||
{
|
||
UpdateStatus($"无法获取IP地址");
|
||
return false;
|
||
}
|
||
|
||
// 用 SDK 生成带签名的 URL
|
||
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
|
||
{
|
||
// 替换URL中的host为IP
|
||
var ipUrl = signedUrl.Replace(signedUri.Host, ip);
|
||
|
||
// 网络优化: 使用静态HttpClient实例
|
||
var request = new HttpRequestMessage(HttpMethod.Get, ipUrl);
|
||
request.Headers.Host = signedUri.Host; // Host头保持原域名
|
||
var response = await _httpClient.SendAsync(request);
|
||
response.EnsureSuccessStatusCode();
|
||
byte[] fileData = await response.Content.ReadAsByteArrayAsync();
|
||
string localDir = Path.GetDirectoryName(localPath);
|
||
if (!Directory.Exists(localDir))
|
||
{
|
||
Directory.CreateDirectory(localDir);
|
||
}
|
||
File.WriteAllBytes(localPath, fileData);
|
||
return true;
|
||
}
|
||
catch (Exception)
|
||
{
|
||
UpdateStatus($"备用方案用IP {ip} 下载失败");
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 以系统时间为准根据传入的数字生成一个时间戳(秒)
|
||
/// </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>
|
||
/// 根据传入的数字生成一个UUID,不能包含中划线,且长度为32位
|
||
/// </summary>
|
||
/// <param name="number">用于生成UUID的数字</param>
|
||
/// <returns>32位UUID字符串</returns>
|
||
private string GenerateUUID(int number)
|
||
{
|
||
// 使用数字作为种子生成确定性UUID
|
||
var random = new Random(number);
|
||
var guid = new byte[16];
|
||
random.NextBytes(guid);
|
||
|
||
// 设置版本号(4)和变体位
|
||
guid[7] = (byte)((guid[7] & 0x0F) | 0x40); // 版本4
|
||
guid[8] = (byte)((guid[8] & 0x3F) | 0x80); // 变体位
|
||
|
||
var resultGuid = new Guid(guid);
|
||
return resultGuid.ToString("N").Replace("-", "");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 通过MD5算法计算字符串的哈希值,由数字0~9和小写英文字母a~z组成,长度为32位
|
||
/// </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">原始URL</param>
|
||
/// <param name="file">要下载的文件名</param>
|
||
/// <returns>鉴权后的URL</returns>
|
||
private string GenerateAuthUrl(string url, string file)
|
||
{
|
||
try
|
||
{
|
||
// 使用配置的超时时间生成时间戳
|
||
long timestamp = GenerateTimestamp(OneDriveAuthTimeout);
|
||
|
||
// 生成随机UUID
|
||
string rand = GenerateUUID(16);
|
||
|
||
// 通用URL解析:提取域名后的路径部分
|
||
string pathPart = ExtractPathFromUrl(url);
|
||
|
||
// 构造完整的下载URL
|
||
string fullUrl = $"{url}/{file}";
|
||
|
||
// 构造签名字符串:/路径部分/文件名-时间戳-随机数-UID-密钥
|
||
string signString = $"/{pathPart}/{file}-{timestamp}-{rand}-{OneDriveUid}-{OneDriveAuthKey}";
|
||
|
||
// 计算MD5签名
|
||
string signature = GenerateMD5(signString);
|
||
|
||
// 构造鉴权后的URL:完整URL?auth_key=时间戳-随机数-UID-签名
|
||
string authUrl = $"{fullUrl}?auth_key={timestamp}-{rand}-{OneDriveUid}-{signature}";
|
||
|
||
// 调试信息(可选)
|
||
Debug.WriteLine($"鉴权URL生成: {authUrl}");
|
||
Debug.WriteLine($"签名字符串: {signString}");
|
||
|
||
return authUrl;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
UpdateStatus($"生成鉴权URL失败: {ex.Message}");
|
||
return $"{url}/{file}"; // 如果生成失败,返回原始URL
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从URL中提取路径部分(域名后的部分)
|
||
/// </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('/');
|
||
|
||
// 如果路径为空,尝试从完整URL中提取
|
||
if (string.IsNullOrEmpty(pathPart))
|
||
{
|
||
string basePattern = $"{uri.Scheme}://{uri.Host}";
|
||
if (url.StartsWith(basePattern))
|
||
{
|
||
pathPart = url.Substring(basePattern.Length).TrimStart('/');
|
||
}
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
// 如果URI解析失败,使用简单的字符串处理
|
||
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">要下载的文件名</param>
|
||
/// <param name="localPath">本地保存路径</param>
|
||
/// <returns>下载是否成功</returns>
|
||
private async Task<bool> DownloadMd5FileWithFallback(string fileName, string localPath)
|
||
{
|
||
try
|
||
{
|
||
// 1. 首先尝试从123盘下载
|
||
UpdateStatus("尝试从123盘下载MD5文件...");
|
||
if (await DownloadFromOneDrive($"http://{OneDriveMainDomain}{OneDrivePath}", fileName, localPath))
|
||
{
|
||
return true;
|
||
}
|
||
|
||
// 2. 如果123盘下载失败,使用阿里云OSS作为备用
|
||
UpdateStatus("123盘下载失败,尝试阿里云OSS备用方案...");
|
||
return await DownloadFileWithFallback(fileName, localPath);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
UpdateStatus($"所有下载方式都失败: {ex.Message}");
|
||
Debug.WriteLine($"下载MD5文件异常: {ex}");
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
} |