From 873601013fe32928f7e59397ac6e1af39fc58f26 Mon Sep 17 00:00:00 2001 From: dong <1278815766@qq.com> Date: Thu, 26 Jun 2025 18:41:30 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=86=85=E5=AD=98=E5=8D=A0?= =?UTF-8?q?=E7=94=A8=E8=BF=87=E5=A4=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CheckDownload.csproj | 4 +- Form1.cs | 535 ++++++++++++++++++------------------------- packages.config | 2 +- 3 files changed, 221 insertions(+), 320 deletions(-) diff --git a/CheckDownload.csproj b/CheckDownload.csproj index da5ff88..ec7963b 100644 --- a/CheckDownload.csproj +++ b/CheckDownload.csproj @@ -180,8 +180,8 @@ - + - + \ No newline at end of file diff --git a/Form1.cs b/Form1.cs index 2292008..cbc1095 100644 --- a/Form1.cs +++ b/Form1.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.ComponentModel; using System.Data; -using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Drawing; using System.Globalization; @@ -20,9 +19,9 @@ using Aliyun.OSS; using Aliyun.OSS.Common; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -// 蓝奏库 -using LanzouCloudSolve; + using System.Collections.Concurrent; +using System.Diagnostics; namespace CheckDownload { @@ -38,21 +37,18 @@ namespace CheckDownload private const string OssAccessKeyId = "LTAI5tCwRcL5LUgyHB2j7w82"; // 阿里云OSS访问密钥Secret private const string OssAccessKeySecret = "7ClQns3wz6psmIp9T2OfuEn3tpzrCK"; - - // 123盘配置 - // 注意:如需修改123盘路径,只需修改 OneDrivePath 常量即可,主备域名会自动使用新路径 // 123盘鉴权密钥 - private const string OneDriveAuthKey = "ZhwG3LxOtGJwM3ym"; + private const string OneDriveAuthKey = "6SwdpWdSJuJRSh"; // 123盘UID - private const string OneDriveUid = "1850250683"; + private const string OneDriveUid = "1826795402"; // 123盘路径(不包含域名)- 修改此处即可同时生效于主备域名 - private const string OneDrivePath = "/1850250683/SuWin"; + private const string OneDrivePath = "/1826795402/KeyAuth"; // 123盘主域名 private const string OneDriveMainDomain = "vip.123pan.cn"; // 123盘备用域名 private const string OneDriveBackupDomain = "vip.123yx.com"; // 123盘鉴权有效时间(秒) - private const int OneDriveAuthTimeout = 600; // 10分钟 + private const int OneDriveAuthTimeout = 600; // 网络优化: 静态HttpClient实例,避免套接字耗尽 private static readonly HttpClient _httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(20) }; @@ -60,9 +56,10 @@ namespace CheckDownload private static readonly OssClient _ossClient = new OssClient(OssEndpoint, OssAccessKeyId, OssAccessKeySecret); // 网络优化: 备用DNS服务列表,提高解析成功率 private static readonly List _dnsServers = new List { "223.5.5.5", "119.29.29.29" }; - + // 最大并发下载数量 + private static readonly int MaxConcurrentDownloads = 2; // 用于存储下载的文件数据 - private Dictionary _downloadedFiles = new Dictionary(); + private Dictionary _downloadedFiles = new Dictionary(); // 已完成的下载数量 private int _completedCount = 0; // 总下载数量 @@ -226,7 +223,7 @@ namespace CheckDownload } /// - /// 清理旧的更新文件(.new后缀文件) + /// 清理旧的更新文件,删除所有.new后缀的临时文件 /// private void CleanupNewFiles() { @@ -242,24 +239,22 @@ namespace CheckDownload { File.Delete(file); } - catch (Exception ex) + catch { - Debug.WriteLine($"无法删除旧的更新文件: {file} - {ex.Message}"); } } } } - catch (Exception ex) + catch { - Debug.WriteLine($"清理旧的更新文件时出错: {ex.Message}"); } } /// - /// 读取在线MD5文件并解析JSON内容 + /// 读取在线MD5文件并解析JSON内容,提取版本信息和文件清单数据 /// - /// MD5文件路径 - /// 版本号、MD5值和数据对象 + /// MD5文件的本地路径 + /// 包含版本号、MD5值和文件数据的元组 private (string Version, string Md5, JObject Data) ReadOnlineMd5File(string filePath) { try @@ -282,12 +277,12 @@ namespace CheckDownload } /// - /// 验证在线数据的有效性 + /// 验证在线MD5数据的完整性和有效性 /// - /// 版本号 - /// MD5值 - /// 数据对象 - /// 数据是否有效 + /// 版本号字符串 + /// 文件的MD5哈希值 + /// 包含文件信息的JSON对象 + /// 如果所有必需字段都有效则返回true,否则返回false private bool ValidateOnlineData(string version, string md5, JObject data) { if (string.IsNullOrEmpty(version) || string.IsNullOrEmpty(md5) || data == null) @@ -299,11 +294,11 @@ namespace CheckDownload } /// - /// 比较本地和在线MD5数据,找出需要更新的文件 + /// 递归比较本地文件和在线MD5数据,识别需要更新的文件 /// - /// 在线数据 - /// 当前路径 - /// 需要更新的文件列表 + /// 在线文件清单的JSON数据 + /// 当前递归路径,用于构建完整的文件路径 + /// 包含需要更新的文件路径和对应MD5值的字典 private Dictionary CompareMd5Data(JObject onlineData, string currentPath = "") { var differences = new Dictionary(); @@ -313,7 +308,6 @@ namespace CheckDownload string key = onlineProperty.Name; JToken onlineValue = onlineProperty.Value; - // 始终使用 Path.Combine 来构造路径,确保跨平台和正确性 string relativePath = string.IsNullOrEmpty(currentPath) ? key : Path.Combine(currentPath, key); string localFullPath = Path.Combine(_baseDirectory, relativePath); @@ -329,12 +323,8 @@ namespace CheckDownload } else { - string localMd5 = CalculateMD5(File.ReadAllBytes(localFullPath)); - if (localMd5.Equals(expectedMd5, StringComparison.OrdinalIgnoreCase)) - { - // MD5一致,文件无需更新 - } - else + string localMd5 = CalculateMD5FromFile(localFullPath); + if (!localMd5.Equals(expectedMd5, StringComparison.OrdinalIgnoreCase)) { if (!differences.ContainsKey(relativePath)) { @@ -359,9 +349,9 @@ namespace CheckDownload } /// - /// 下载并验证文件列表 + /// 批量下载文件列表并进行MD5验证,支持重试机制 /// - /// 需要下载的文件列表 + /// 包含文件路径和期望MD5值的字典 private async Task DownloadAndVerifyFiles(Dictionary fileList) { _totalCount = fileList.Count; @@ -377,7 +367,6 @@ namespace CheckDownload var stillFailing = await RetryFailedFilesAsync(new Dictionary(failedFiles)); if (stillFailing.Any()) { - // 即使重试后仍然有失败的,也继续处理已成功的 UpdateStatus($"重试后仍有 {stillFailing.Count} 个文件下载失败。"); } } @@ -387,17 +376,17 @@ namespace CheckDownload throw new Exception("所有文件下载失败。"); } - VerifyAndSaveAllFiles(); + await VerifyAndSaveAllFiles(); } /// - /// 执行并发下载任务 + /// 使用信号量控制并发数量,执行多个文件的并发下载任务 /// - /// 要下载的文件列表 - /// 下载失败的文件集合 + /// 包含文件路径和MD5值的下载任务字典 + /// 用于收集下载失败文件的线程安全集合 private async Task PerformDownloads(IDictionary filesToDownload, ConcurrentDictionary failedDownloads) { - var semaphore = new SemaphoreSlim(4); // 限制4个并发下载 + var semaphore = new SemaphoreSlim(MaxConcurrentDownloads); var downloadTasks = filesToDownload.Select(async (file) => { await semaphore.WaitAsync(); @@ -408,15 +397,13 @@ namespace CheckDownload { lock (_downloadedFiles) { - _downloadedFiles[file.Key] = (null, file.Value); + _downloadedFiles[file.Key] = 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 @@ -433,11 +420,11 @@ namespace CheckDownload } /// - /// 尝试下载单个文件(123盘主要,OSS备用),支持断点续传 + /// 尝试从多个数据源下载单个文件,优先使用123盘,OSS作为备用 /// - /// 文件路径 - /// 期望的MD5值 - /// 下载是否成功 + /// 文件的相对路径 + /// 文件的期望MD5值,用于验证完整性 + /// 下载成功返回true,否则返回false private async Task AttemptDownloadAsync(string filePath, string expectedMd5) { string tempFilePath = Path.Combine(_tempDirectory, filePath); @@ -445,20 +432,17 @@ namespace CheckDownload try { - // 检查临时文件是否已存在且完整 if (await CheckExistingTempFile(tempFilePath, expectedMd5, fileName)) { - return true; // 文件已存在且完整,跳过下载 + return true; } - // 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}"; @@ -490,38 +474,35 @@ namespace CheckDownload } /// - /// 检查现有临时文件是否存在且完整 + /// 检查临时目录中是否已存在完整的文件,通过MD5验证文件完整性 /// - /// 临时文件路径 - /// 期望的MD5值 - /// 文件名(用于显示) - /// 文件是否存在且完整 + /// 临时文件的完整路径 + /// 文件的期望MD5哈希值 + /// 文件名,用于状态显示 + /// 如果文件存在且MD5匹配返回true,否则返回false private async Task CheckExistingTempFile(string tempFilePath, string expectedMd5, string fileName) { try { - // 检查文件是否存在 if (!File.Exists(tempFilePath)) { - return false; // 文件不存在,需要下载 + return false; } UpdateStatus($"检查已存在的临时文件: {fileName}"); - // 异步计算文件MD5 - string actualMd5 = await Task.Run(() => CalculateMD5(File.ReadAllBytes(tempFilePath))); + string actualMd5 = await Task.Run(() => CalculateMD5FromFile(tempFilePath)); if (actualMd5.Equals(expectedMd5, StringComparison.OrdinalIgnoreCase)) { UpdateStatus($"临时文件完整,跳过下载: {fileName}"); - return true; // 文件完整,跳过下载 + return true; } else { UpdateStatus($"临时文件不完整,重新下载: {fileName}"); - // 删除不完整的文件 File.Delete(tempFilePath); - return false; // 文件不完整,需要重新下载 + return false; } } catch (Exception ex) @@ -529,7 +510,6 @@ namespace CheckDownload UpdateStatus($"检查临时文件时出错,将重新下载: {fileName} - {ex.Message}"); try { - // 如果出错,尝试删除可能损坏的文件 if (File.Exists(tempFilePath)) { File.Delete(tempFilePath); @@ -537,190 +517,168 @@ namespace CheckDownload } catch { - // 忽略删除失败的错误 } - return false; // 出错时重新下载 + return false; } } /// - /// 从123盘下载在线MD5文件(主域名+备用域名) + /// 从123盘下载文件,先尝试主域名,失败后自动切换到备用域名 /// - /// 123盘基础链接地址 + /// 123盘的基础URL地址 /// 要下载的文件名 - /// 本地保存路径 - /// 下载是否成功 + /// 文件保存的本地路径 + /// 下载成功返回true,两个域名都失败则返回false private async Task 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)) + using (var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)) { - Directory.CreateDirectory(localDir); + 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; } - - // 保存文件到本地 - 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)) + using (var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)) { - Directory.CreateDirectory(localDir); + 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; } - - // 保存文件到本地 - File.WriteAllBytes(localPath, fileData); - - UpdateStatus($"123盘备用域名下载成功: {fileName}"); - return true; } catch (Exception ex) { UpdateStatus($"123盘备用域名下载失败: {fileName} - {ex.Message}"); - Debug.WriteLine($"123盘备用域名下载异常: {ex}"); return false; } } /// - /// 从123盘下载单个文件(主域名+备用域名) + /// 根据文件MD5从123盘下载文件,使用主备域名双重保障 /// - /// 文件相对路径 - /// 期望的MD5值 - /// 本地保存路径 - /// 下载是否成功 + /// 文件的相对路径(仅用于日志显示) + /// 文件的MD5值,用于构建123盘存储路径 + /// 文件保存的本地完整路径 + /// 下载成功返回true,两个域名都失败则返回false private async Task 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)) + using (var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)) { - Directory.CreateDirectory(localDir); + 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; } - - // 保存文件到本地 - 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)) + using (var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)) { - Directory.CreateDirectory(localDir); + 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; } - - // 保存文件到本地 - File.WriteAllBytes(localPath, fileData); - - return true; } catch (Exception ex) { - Debug.WriteLine($"123盘备用域名下载文件失败: {filePath} - {ex.Message}"); return false; } } /// - /// 重试下载失败的文件 + /// 对下载失败的文件进行重试,最多重试指定次数 /// - /// 失败的文件列表 - /// 仍然失败的文件列表 + /// 包含失败文件路径和MD5值的字典 + /// 重试后仍然失败的文件字典 private async Task> RetryFailedFilesAsync(Dictionary failedFiles) { const int maxRetries = 2; @@ -731,7 +689,6 @@ namespace CheckDownload UpdateStatus($"第 {i + 1} 次重试,剩余 {filesToRetry.Count} 个文件..."); var failedThisRound = new ConcurrentDictionary(); - // BUG修复: 此处不应清空重试队列 await PerformDownloads(filesToRetry, failedThisRound); filesToRetry = new Dictionary(failedThisRound); @@ -747,17 +704,17 @@ namespace CheckDownload } /// - /// 通过DNS服务器解析域名获取IP地址列表 + /// 使用多个DNS服务器解析域名获取IP地址列表,提高解析成功率 /// - /// 要解析的域名 - /// IP地址列表 + /// 要解析的目标域名 + /// 解析成功的IP地址列表,解析失败返回空列表 private async Task> GetIpAddressesForDomain(string domain) { foreach (var dnsServer in _dnsServers) { try { - var requestUri = $"https://{dnsServer}/resolve?name={domain}&type=1&short=1"; + var requestUri = $"http://{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; @@ -776,7 +733,7 @@ namespace CheckDownload catch (Exception ex) { UpdateStatus($"通过 {dnsServer} 解析域名失败,尝试下一个..."); - await Task.Delay(500); // 短暂延迟后重试 + await Task.Delay(500); } } @@ -785,9 +742,9 @@ namespace CheckDownload } /// - /// 验证并保存所有下载的文件 + /// 验证所有下载文件的MD5完整性并保存到目标位置,处理文件占用情况 /// - private void VerifyAndSaveAllFiles() + private async Task VerifyAndSaveAllFiles() { UpdateStatus("正在校验文件..."); var failedFiles = new List(); @@ -798,7 +755,7 @@ namespace CheckDownload foreach (var item in _downloadedFiles) { string relativePath = item.Key; - string expectedMd5 = item.Value.ExpectedMd5; + string expectedMd5 = item.Value; string tempFilePath = Path.Combine(_tempDirectory, relativePath); try @@ -806,8 +763,7 @@ namespace CheckDownload processedCount++; UpdateStatus($"正在校验和保存文件 ({processedCount}/{totalFiles}): {relativePath}"); - // 计算临时文件的MD5 - string actualMd5 = CalculateMD5(File.ReadAllBytes(tempFilePath)); + string actualMd5 = CalculateMD5FromFile(tempFilePath); if (actualMd5 != expectedMd5.ToLower()) { throw new Exception($"MD5校验失败 (期望: {expectedMd5}, 实际: {actualMd5})"); @@ -816,16 +772,13 @@ namespace CheckDownload string localPath = Path.Combine(_baseDirectory, relativePath); string localDir = Path.GetDirectoryName(localPath); - // 确保目标目录存在 if (!Directory.Exists(localDir)) { Directory.CreateDirectory(localDir); } - // 尝试移动文件,如果文件被占用则尝试解锁 - if (!TryMoveFile(tempFilePath, localPath)) + if (!await TryMoveFileAsync(tempFilePath, localPath)) { - // 如果无法移动文件,则使用备用文件名 string backupPath = localPath + ".new"; File.Move(tempFilePath, backupPath); filesForScripting.Add((localPath, backupPath)); @@ -868,37 +821,32 @@ namespace CheckDownload } /// - /// 尝试移动文件,如果文件被占用则尝试解锁 + /// 尝试将文件从临时位置移动到目标位置(异步),若文件被占用则等待一秒后重试一次 /// - /// 源文件路径 - /// 目标文件路径 - /// 移动是否成功 - private bool TryMoveFile(string sourcePath, string targetPath) + /// 源文件的完整路径 + /// 目标文件的完整路径 + /// 移动成功返回true,失败返回false + private async Task TryMoveFileAsync(string sourcePath, string targetPath) { try { - // 直接尝试移动文件 File.Move(sourcePath, targetPath); return true; } catch (IOException) { - // 文件被占用,尝试解锁 UpdateStatus($"文件被占用,尝试解锁..."); - + try { - // 等待一段时间,让文件可能被释放 - Thread.Sleep(1000); - - // 再次尝试移动文件 + await Task.Delay(1000); + File.Move(sourcePath, targetPath); UpdateStatus($"文件解锁成功并已更新"); return true; } catch { - // 如果仍然无法移动,返回失败 UpdateStatus($"文件仍被占用,无法移动"); return false; } @@ -911,9 +859,9 @@ namespace CheckDownload } /// - /// 为所有需要替换的文件创建批处理脚本 + /// 为被占用文件创建批处理脚本,在程序退出后自动完成文件替换 /// - /// 需要替换的文件列表 + /// 包含原始文件路径和新文件路径的元组列表 private void CreateReplaceScriptForAll(List<(string original, string newFile)> files) { if (!files.Any()) return; @@ -925,7 +873,7 @@ namespace CheckDownload var batchContent = new StringBuilder(); batchContent.AppendLine("@echo off"); - batchContent.AppendLine("chcp 65001 > nul"); // 确保正确处理UTF-8路径 + 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 ("); @@ -961,20 +909,18 @@ namespace CheckDownload } /// - /// 初始化临时目录 + /// 初始化程序使用的临时目录,用于存储下载过程中的临时文件 /// private void InitializeTempDirectory() { try { - // 直接使用 %LocalAppData%\Temp\CheckDownload 作为临时目录 _tempDirectory = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Temp", "CheckDownload" ); - // 确保临时目录存在 if (!Directory.Exists(_tempDirectory)) { Directory.CreateDirectory(_tempDirectory); @@ -987,10 +933,10 @@ namespace CheckDownload } /// - /// 计算字符串的MD5值 + /// 计算字符串的MD5哈希值,返回32位小写十六进制字符串 /// - /// 输入字符串 - /// MD5哈希值 + /// 要计算哈希值的字符串 + /// 32位小写十六进制MD5哈希值 private string CalculateMD5(string input) { using (var md5 = MD5.Create()) @@ -1002,10 +948,10 @@ namespace CheckDownload } /// - /// 计算字节数组的MD5值 + /// 计算字节数组的MD5哈希值,返回32位小写十六进制字符串 /// - /// 字节数组 - /// MD5哈希值 + /// 要计算哈希值的字节数组 + /// 32位小写十六进制MD5哈希值 private string CalculateMD5(byte[] data) { using (var md5 = MD5.Create()) @@ -1016,14 +962,13 @@ namespace CheckDownload } /// - /// 通用的带备用方案的OSS文件下载方法 + /// 从阿里云OSS下载文件,包含SDK直连和IP直连两种备用方案 /// - /// OSS对象键 - /// 本地保存路径 - /// 下载是否成功 + /// OSS对象的存储键值 + /// 文件保存的本地路径 + /// 下载成功返回true,所有方案都失败返回false private async Task DownloadFileWithFallback(string ossKey, string localPath) { - // 1. 先尝试用 OSS SDK 下载 try { var obj = _ossClient.GetObject(OssBucketName, ossKey); @@ -1043,7 +988,6 @@ namespace CheckDownload UpdateStatus($"主下载失败,尝试备用方案..."); } - // 2. 备用方案:用 IP 下载(带签名URL) var domain = new Uri("https://" + OssEndpoint).Host; List ips = await GetIpAddressesForDomain(domain); if (ips == null || ips.Count == 0) @@ -1052,7 +996,6 @@ namespace CheckDownload return false; } - // 用 SDK 生成带签名的 URL var req = new GeneratePresignedUriRequest(OssBucketName, ossKey, SignHttpMethod.Get) { Expiration = DateTime.Now.AddMinutes(10) @@ -1064,21 +1007,25 @@ namespace CheckDownload { 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)) + request.Headers.Host = signedUri.Host; + using (var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)) { - Directory.CreateDirectory(localDir); + 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); + } } - File.WriteAllBytes(localPath, fileData); return true; } catch (Exception) @@ -1090,40 +1037,38 @@ namespace CheckDownload } /// - /// 以系统时间为准根据传入的数字生成一个时间戳(秒) + /// 生成基于当前UTC时间的Unix时间戳,并添加指定的秒数偏移 /// - /// 要增加的秒数 - /// Unix时间戳 + /// 要在当前时间戳基础上增加的秒数 + /// Unix时间戳(秒) private long GenerateTimestamp(int number) { return (long)(DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1))).TotalSeconds + number; } /// - /// 根据传入的数字生成一个UUID,不能包含中划线,且长度为32位 + /// 使用指定数字作为随机种子生成确定性的32位UUID字符串 /// - /// 用于生成UUID的数字 - /// 32位UUID字符串 + /// 用作随机种子的数字 + /// 不含连字符的32位UUID字符串 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); // 变体位 + guid[7] = (byte)((guid[7] & 0x0F) | 0x40); + guid[8] = (byte)((guid[8] & 0x3F) | 0x80); var resultGuid = new Guid(guid); return resultGuid.ToString("N").Replace("-", ""); } /// - /// 通过MD5算法计算字符串的哈希值,由数字0~9和小写英文字母a~z组成,长度为32位 + /// 计算字符串的MD5哈希值,用于123盘鉴权签名生成 /// - /// 输入字符串 - /// 32位MD5哈希值 + /// 要计算哈希值的输入字符串 + /// 32位小写十六进制MD5哈希值 private string GenerateMD5(string input) { using (var md5 = MD5.Create()) @@ -1135,64 +1080,51 @@ namespace CheckDownload } /// - /// 生成123云盘鉴权后的URL + /// 为123盘文件访问生成带有时间戳和签名验证的鉴权URL /// - /// 原始URL - /// 要下载的文件名 - /// 鉴权后的URL + /// 123盘的基础访问URL + /// 要访问的文件名或路径 + /// 包含鉴权参数的完整访问URL 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 + return $"{url}/{file}"; } } /// - /// 从URL中提取路径部分(域名后的部分) + /// 从完整URL中解析并提取域名后的路径部分,用于生成123盘签名 /// - /// 完整URL - /// 路径部分 + /// 要解析的完整URL地址 + /// 去除域名和协议后的路径字符串 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}"; @@ -1204,7 +1136,6 @@ namespace CheckDownload } catch { - // 如果URI解析失败,使用简单的字符串处理 int protocolIndex = url.IndexOf("://"); if (protocolIndex > 0) { @@ -1219,42 +1150,38 @@ namespace CheckDownload } /// - /// 带备用方案的在线MD5文件下载方法(123盘主要,OSS备用) + /// 下载MD5配置文件,优先使用123盘,失败后切换到阿里云OSS备用方案 /// - /// 要下载的文件名 - /// 本地保存路径 - /// 下载是否成功 + /// MD5配置文件名 + /// 文件保存的本地路径 + /// 下载成功返回true,所有方案都失败返回false private async Task 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; } } /// - /// 清理临时文件夹,直接删除整个CheckDownload文件夹 + /// 清理程序使用的临时目录,删除所有下载过程中产生的临时文件 /// private void CleanupTempDirectory() { try { - // 临时目录路径 string tempPath = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Temp", @@ -1265,85 +1192,62 @@ namespace CheckDownload { UpdateStatus("清理临时文件..."); Directory.Delete(tempPath, true); - Debug.WriteLine($"已清理临时目录: {tempPath}"); } } catch (Exception ex) { - Debug.WriteLine($"清理临时目录时发生错误: {ex.Message}"); - // 清理失败时不影响程序运行,只记录日志 } } /// - /// 基于md5.json内容智能检测基准目录 + /// 通过分析md5.json文件内容智能确定项目基准目录,用于正确定位文件更新路径 /// - /// md5.json的数据对象 + /// md5.json文件中的数据对象 private void InitializeBaseDirectoryFromMd5Data(JObject data) { try { - // 获取当前程序的名称(不包含路径) string currentProgramName = Path.GetFileName(Application.ExecutablePath); - Debug.WriteLine($"当前程序名称: {currentProgramName}"); - // 在md5.json中查找当前程序文件 string programPathInMd5 = FindFileInMd5Data(data, currentProgramName); if (!string.IsNullOrEmpty(programPathInMd5)) { - Debug.WriteLine($"程序在MD5中的路径: {programPathInMd5}"); - - // 获取程序在md5.json中所在的目录 string programDirInMd5 = Path.GetDirectoryName(programPathInMd5); if (string.IsNullOrEmpty(programDirInMd5)) { - // 程序在根目录 programDirInMd5 = ""; } - Debug.WriteLine($"程序在MD5中的目录: '{programDirInMd5}'"); - - // 获取当前程序实际运行的目录 string currentProgramDir = Application.StartupPath; - Debug.WriteLine($"程序实际运行目录: {currentProgramDir}"); - // 检查当前程序是否运行在md5.json指定的目录中 if (string.IsNullOrEmpty(programDirInMd5)) { - // 程序应该在根目录,使用当前目录作为基准目录 _baseDirectory = currentProgramDir; } else { - // 程序应该在某个子目录中,需要向上查找基准目录 _baseDirectory = FindProjectBaseDirectory(currentProgramDir, programDirInMd5); } } else { - // 在md5.json中没有找到当前程序,使用当前目录作为基准目录 _baseDirectory = Application.StartupPath; - Debug.WriteLine($"MD5文件中未找到程序 '{currentProgramName}',使用当前目录作为基准目录"); } - - Debug.WriteLine($"最终确定的基准目录: {_baseDirectory}"); } catch (Exception ex) { - // 如果出错,使用当前目录作为fallback _baseDirectory = Application.StartupPath; - Debug.WriteLine($"基准目录检测失败,使用默认: {_baseDirectory} - {ex.Message}"); } } /// - /// 在md5.json数据中递归查找指定文件名 + /// 在md5.json的数据结构中递归搜索指定的文件名 /// - /// md5数据对象 - /// 要查找的文件名 - /// 当前路径 - /// 文件在md5.json中的完整路径,如果未找到返回null + /// md5.json解析后的JSON对象 + /// 要搜索的目标文件名 + /// 当前递归的路径,用于构建完整路径 + /// 找到的文件在md5.json中的完整相对路径,未找到返回null private string FindFileInMd5Data(JObject data, string fileName, string currentPath = "") { foreach (var property in data.Properties()) @@ -1351,12 +1255,10 @@ namespace CheckDownload 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; @@ -1364,7 +1266,6 @@ namespace CheckDownload } else if (value.Type == JTokenType.Object) { - // 这是一个目录,递归查找 string result = FindFileInMd5Data((JObject)value, fileName, fullPath); if (!string.IsNullOrEmpty(result)) { @@ -1376,66 +1277,66 @@ namespace CheckDownload } /// - /// 根据程序在md5.json中的路径,查找项目的基准目录 + /// 通过分析程序在md5.json中的相对路径,向上递归查找项目根目录 /// - /// 当前程序所在目录 + /// 当前程序运行的目录 /// 程序在md5.json中的相对路径 - /// 项目基准目录 + /// 项目的基准目录路径 private string FindProjectBaseDirectory(string currentDir, string expectedRelativePath) { try { - // 将路径分解为目录层级 string[] expectedDirs = expectedRelativePath.Split(new char[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries); - Debug.WriteLine($"期望的相对路径层级: {string.Join(" -> ", expectedDirs)}"); - 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]; - Debug.WriteLine($"检查: 当前目录名 '{currentDirName}' vs 期望目录名 '{expectedDirName}'"); - if (string.Equals(currentDirName, expectedDirName, StringComparison.OrdinalIgnoreCase)) { - // 如果这是期望路径的第一个目录,说明找到了基准目录 if (i == expectedDirs.Length - 1) { - Debug.WriteLine($"找到基准目录: {parentDir}"); return parentDir; } - // 继续向上查找 checkDir = parentDir; } else { - // 目录名不匹配,说明程序不在期望的路径中 break; } } - // 如果没有找到匹配的路径结构,使用当前目录 - Debug.WriteLine("未找到匹配的路径结构,使用当前目录"); return currentDir; } catch (Exception ex) { - Debug.WriteLine($"查找基准目录时出错: {ex.Message}"); return currentDir; } } + + /// + /// 使用文件流计算指定文件的 MD5 哈希值(32 位小写十六进制)。 + /// + /// 要计算 MD5 的文件完整路径 + /// 32 位小写十六进制 MD5 字符串 + 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(); + } + } } } \ No newline at end of file diff --git a/packages.config b/packages.config index 8ca4bff..2c4649d 100644 --- a/packages.config +++ b/packages.config @@ -3,7 +3,7 @@ - +