Files
CheckDownload/Form1.cs
2025-06-23 00:55:13 +08:00

1258 lines
49 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;
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;
}
}
}
}