using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Diagnostics.CodeAnalysis; using System.Drawing; using System.Globalization; using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Runtime.InteropServices; using System.Security.Cryptography; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; using Aliyun.OSS; using Aliyun.OSS.Common; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System.Collections.Concurrent; using System.Diagnostics; using System.Configuration; using SevenZipExtractor; using System.Reflection; using Microsoft.Win32; namespace CheckDownload { public partial class Update : Form { // MD5文件名称 private const string Md5File = "md5.json"; // 阿里云OSS访问地址 private const string OssEndpoint = "oss-cn-hongkong.aliyuncs.com"; // 阿里云OSS存储空间名称 private const string OssBucketName = "suwin-oss"; // 阿里云OSS访问密钥ID private const string OssAccessKeyId = "LTAI5tCwRcL5LUgyHB2j7w82"; // 阿里云OSS访问密钥Secret private const string OssAccessKeySecret = "7ClQns3wz6psmIp9T2OfuEn3tpzrCK"; // 123盘鉴权密钥 private const string OneDriveAuthKey = "6SwdpWdSJuJRSh"; // 123盘UID private const string OneDriveUid = "1826795402"; // 123盘路径(不包含域名)- 修改此处即可同时生效于主备域名 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; // 网络优化: 静态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 _dnsServers = new List { "223.5.5.5", "119.29.29.29" }; // 最大并发下载数量 private static readonly int MaxConcurrentDownloads = int.TryParse(ConfigurationManager.AppSettings["MaxConcurrentDownloads"], out var mcd) ? mcd : 4; // 最大下载重试次数 private static readonly int MaxDownloadRetries = int.TryParse(ConfigurationManager.AppSettings["MaxDownloadRetries"], out var mdr) ? mdr : 2; // 用于存储下载的文件数据 private Dictionary _downloadedFiles = new Dictionary(); // 已完成的下载数量 private int _completedCount = 0; // 总下载数量 private int _totalCount = 0; // 临时文件夹路径 private string _tempDirectory; // 基准目录路径(用于文件更新的目标目录) private string _baseDirectory; // === 新增: 用于显示整体下载大小与速度 === private long _totalDownloadedBytes = 0; // 已下载总字节数 private DateTime _downloadStartTime; // 下载开始时间 private DateTime _lastSpeedUpdateTime; // 上一次更新速度的时间 private readonly object _speedLock = new object(); // 锁,用于多线程更新 UI private long _bytesSinceLastSpeedCalc = 0; // 距离上次速度计算新增的字节数 /// /// 初始化窗体 /// public Update() { InitializeComponent(); _baseDirectory = Application.StartupPath; ConfigureProgressBar(); InitializeTempDirectory(); } /// /// 配置进度条的基本属性 /// private void ConfigureProgressBar() { Update_Pro.Minimum = 0; Update_Pro.Maximum = 100; Update_Pro.Value = 0; Update_Pro.Step = 1; } /// /// 更新进度条的值(线程安全) /// /// 进度百分比 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(); } } /// /// 更新状态显示文本(线程安全) /// /// 状态消息 private void UpdateStatus(string message) { if (this.InvokeRequired) { this.Invoke((MethodInvoker)delegate { Status_Box.Text = message; }); } else { Status_Box.Text = message; } } /// /// 更新计数显示文本(线程安全) /// /// 计数值 private void UpdateCount(string countMessage) { if (this.InvokeRequired) { this.Invoke((MethodInvoker)delegate { Count_Box.Text = countMessage; }); } else { Count_Box.Text = countMessage; } } /// /// 更新大小显示文本(线程安全) /// /// 大小值 private void UpdateSize(string sizeMessage) { if (this.InvokeRequired) { this.Invoke((MethodInvoker)delegate { Size_Box.Text = sizeMessage; }); } else { Size_Box.Text = sizeMessage; } } /// /// 窗体加载事件处理,启动更新流程 /// public async void Update_Load(object sender, EventArgs e) { PositionFormToBottomRight(); try { await UpdateFile(); } finally { } } /// /// 将窗体定位到屏幕右下角 /// private void PositionFormToBottomRight() { Rectangle workingArea = Screen.GetWorkingArea(this); this.Location = new Point(workingArea.Right - this.Width, workingArea.Bottom - this.Height); } /// /// 主要的文件更新流程 /// private async Task UpdateFile() { try { CleanupNewFiles(); UpdateStatus("下载在线MD5文件并读取..."); UpdateCount(""); UpdateSize(""); string tempFilePath = Path.Combine(_tempDirectory, Md5File); // 使用新的带123盘的下载方法 if (!await DownloadMd5FileWithFallback(Md5File, tempFilePath)) { UpdateStatus("下载在线MD5文件失败"); await Task.Delay(3000); this.Close(); return; } var onlineData = ReadOnlineMd5File(tempFilePath); if (!ValidateOnlineData(onlineData.Version, onlineData.Md5, onlineData.Data)) { UpdateStatus("在线MD5文件无效"); await Task.Delay(3000); this.Close(); return; } // 基于md5.json内容智能检测基准目录 InitializeBaseDirectoryFromMd5Data(onlineData.Data); UpdateStatus("比较本地和在线MD5文件..."); var compareResult = CompareMd5Data(onlineData.Data); // 过滤掉 .db 和 .db3 文件 compareResult = compareResult .Where(kvp => !IsDatabaseFile(kvp.Key)) .ToDictionary(kvp => kvp.Key, kvp => kvp.Value); if (compareResult.Count == 0) { UpdateStatus("所有文件都是最新的,无需更新"); UpdateCount(""); UpdateSize(""); UpdateProgressValue(100); // 无需更新时清理临时文件夹 CleanupTempDirectory(); // 显示更新完成并等待2秒 UpdateStatus("更新完成"); await Task.Delay(2000); this.Close(); return; } // 如果更新列表包含 tim.dll,则提前结束 tim.exe if (compareResult.Keys.Any(p => p.EndsWith("tim.dll", StringComparison.OrdinalIgnoreCase))) { KillProcessByBaseName("tim"); } UpdateStatus("下载并验证文件..."); // 根据路径长度排序,优先下载小文件/浅层文件,可加其它排序规则 var orderedFileList = compareResult.OrderBy(k => k.Key.Length) .ToDictionary(k => k.Key, v => v.Value); _totalCount = orderedFileList.Count; _completedCount = 0; _downloadedFiles.Clear(); // === 新增: 初始化速度统计 === _totalDownloadedBytes = 0; _downloadStartTime = DateTime.UtcNow; _lastSpeedUpdateTime = _downloadStartTime; _bytesSinceLastSpeedCalc = 0; var failedFiles = new ConcurrentDictionary(); await PerformDownloads(orderedFileList, failedFiles); if (!failedFiles.IsEmpty) { UpdateStatus($"有 {failedFiles.Count} 个文件下载失败,开始重试..."); UpdateCount(""); UpdateSize(""); var stillFailing = await RetryFailedFilesAsync(new Dictionary(failedFiles)); if (stillFailing.Any()) { UpdateStatus($"重试后仍有 {stillFailing.Count} 个文件下载失败。"); UpdateCount(""); UpdateSize(""); } } if (_completedCount == 0 && orderedFileList.Count > 0) { throw new Exception("所有文件下载失败。"); } await VerifyAndSaveAllFiles(); await DecompressTim7zAsync(); // 校验和保存成功后清理临时目录 CleanupTempDirectory(); // 显示完成状态并退出 UpdateStatus("更新完成"); await Task.Delay(3000); this.Close(); return; } catch (Exception ex) { UpdateStatus("更新失败"); MessageBox.Show($"更新失败:\n{ex.Message}", "Update Error", MessageBoxButtons.OK, MessageBoxIcon.Error); await Task.Delay(3000); this.Close(); } } /// /// 清理旧的更新文件,删除所有.new后缀的临时文件 /// private void CleanupNewFiles() { try { var newFiles = Directory.GetFiles(_baseDirectory, "*.new", SearchOption.AllDirectories); if (newFiles.Length > 0) { UpdateStatus("正在清理旧的更新文件..."); foreach (var file in newFiles) { try { File.Delete(file); } catch { } } } } catch { } } /// /// 读取在线MD5文件并解析JSON内容,提取版本信息和文件清单数据 /// /// MD5文件的本地路径 /// 包含版本号、MD5值和文件数据的元组 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); } } /// /// 验证在线MD5数据的完整性和有效性 /// /// 版本号字符串 /// 文件的MD5哈希值 /// 包含文件信息的JSON对象 /// 如果所有必需字段都有效则返回true,否则返回false private bool ValidateOnlineData(string version, string md5, JObject data) { if (string.IsNullOrEmpty(version) || string.IsNullOrEmpty(md5) || data == null) { UpdateStatus("在线MD5文件无效"); return false; } return true; } /// /// 递归比较本地文件和在线MD5数据,识别需要更新的文件 /// /// 在线文件清单的JSON数据 /// 当前递归路径,用于构建完整的文件路径 /// 包含需要更新的文件路径和对应MD5值的字典 private Dictionary CompareMd5Data(JObject onlineData, string currentPath = "") { var differences = new Dictionary(); foreach (var onlineProperty in onlineData.Properties()) { string key = onlineProperty.Name; JToken onlineValue = onlineProperty.Value; string relativePath = string.IsNullOrEmpty(currentPath) ? key : Path.Combine(currentPath, key); string localFullPath = Path.Combine(_baseDirectory, relativePath); // 若为数据库文件 (.db/.db3) 直接跳过比较 if (IsDatabaseFile(relativePath)) { continue; } if (onlineValue.Type == JTokenType.String) { string expectedMd5 = onlineValue.ToString(); if (!File.Exists(localFullPath)) { if (!differences.ContainsKey(relativePath)) { differences[relativePath] = expectedMd5; } } else { string localMd5 = CalculateMD5FromFile(localFullPath); if (!localMd5.Equals(expectedMd5, StringComparison.OrdinalIgnoreCase)) { if (!differences.ContainsKey(relativePath)) { differences[relativePath] = expectedMd5; } } } } else if (onlineValue.Type == JTokenType.Object) { var subDifferences = CompareMd5Data((JObject)onlineValue, relativePath); foreach (var diff in subDifferences) { if (!differences.ContainsKey(diff.Key)) { differences[diff.Key] = diff.Value; } } } } return differences; } /// /// 使用信号量控制并发数量,执行多个文件的并发下载任务 /// /// 包含文件路径和MD5值的下载任务字典 /// 用于收集下载失败文件的线程安全集合 private async Task PerformDownloads(IDictionary filesToDownload, ConcurrentDictionary failedDownloads) { var semaphore = new SemaphoreSlim(MaxConcurrentDownloads); var downloadTasks = filesToDownload.Select(async (file) => { await semaphore.WaitAsync(); try { bool success = await AttemptDownloadAsync(file.Key, file.Value); if (success) { lock (_downloadedFiles) { _downloadedFiles[file.Key] = file.Value; } Interlocked.Increment(ref _completedCount); int progress = (int)((double)_completedCount / _totalCount * 95); UpdateProgressValue(progress); string fileName = Path.GetFileName(file.Key); string truncatedFileName = TruncateString(fileName, 10); if (!string.IsNullOrWhiteSpace(truncatedFileName)) { UpdateStatus($"下载:{truncatedFileName}"); } UpdateCount($"{_completedCount}/{_totalCount}"); } else { failedDownloads.TryAdd(file.Key, file.Value); } } finally { semaphore.Release(); } }); await Task.WhenAll(downloadTasks); } /// /// 尝试从多个数据源下载单个文件,优先使用123盘,OSS作为备用 /// /// 文件的相对路径 /// 文件的期望MD5值,用于验证完整性 /// 下载成功返回true,否则返回false private async Task AttemptDownloadAsync(string filePath, string expectedMd5) { // kill exe if running if (Path.GetExtension(filePath).Equals(".exe", StringComparison.OrdinalIgnoreCase)) { KillProcessIfRunning(filePath); } string tempFilePath = Path.Combine(_tempDirectory, filePath); string fileName = Path.GetFileName(filePath); string truncatedFileName = TruncateString(fileName, 10); try { if (await CheckExistingTempFile(tempFilePath, expectedMd5, fileName)) { return true; } if (!string.IsNullOrWhiteSpace(truncatedFileName)) { UpdateStatus($"下载:{truncatedFileName}"); } UpdateCount($"{_completedCount + 1}/{_totalCount}"); if (await DownloadFileFromOneDrive(filePath, expectedMd5, tempFilePath)) { return true; } if (!string.IsNullOrWhiteSpace(truncatedFileName)) { UpdateStatus($"下载:{truncatedFileName}"); } UpdateCount($"{_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 DownloadWithProgressAsync(obj.Content, fileStream, obj.Metadata.ContentLength); } return true; } catch (Exception ex) when (ex is OssException || ex is WebException) { if (!string.IsNullOrWhiteSpace(truncatedFileName)) { UpdateStatus($"下载:{truncatedFileName}"); } UpdateCount($"{_completedCount + 1}/{_totalCount}"); UpdateSize(""); string ossKey = $"File/{expectedMd5}"; return await DownloadFileWithFallback(ossKey, tempFilePath); } catch (Exception ex) { if (!string.IsNullOrWhiteSpace(truncatedFileName)) { UpdateStatus($"下载异常: {truncatedFileName} - {ex.Message}"); } return false; } } /// /// 检查临时目录中是否已存在完整的文件,通过MD5验证文件完整性 /// /// 临时文件的完整路径 /// 文件的期望MD5哈希值 /// 文件名,用于状态显示 /// 如果文件存在且MD5匹配返回true,否则返回false private async Task CheckExistingTempFile(string tempFilePath, string expectedMd5, string fileName) { try { if (!File.Exists(tempFilePath)) { return false; } if (!string.IsNullOrWhiteSpace(fileName)) { UpdateStatus($"检查已存在的临时文件: {fileName}"); } string actualMd5 = await Task.Run(() => CalculateMD5FromFile(tempFilePath)); if (actualMd5.Equals(expectedMd5, StringComparison.OrdinalIgnoreCase)) { if (!string.IsNullOrWhiteSpace(fileName)) { UpdateStatus($"临时文件完整,跳过下载: {fileName}"); } // === 新增: 将已存在的有效临时文件大小计入总下载量 === try { var fileInfo = new FileInfo(tempFilePath); Interlocked.Add(ref _totalDownloadedBytes, fileInfo.Length); } catch { // 忽略获取文件大小的错误 } return true; } else { if (!string.IsNullOrWhiteSpace(fileName)) { UpdateStatus($"临时文件不完整,重新下载: {fileName}"); } File.Delete(tempFilePath); return false; } } catch (Exception ex) { if (!string.IsNullOrWhiteSpace(fileName)) { UpdateStatus($"检查临时文件时出错,将重新下载: {fileName} - {ex.Message}"); } try { if (File.Exists(tempFilePath)) { File.Delete(tempFilePath); } } catch { } return false; } } /// /// 从123盘下载文件,先尝试主域名,失败后自动切换到备用域名 /// /// 123盘的基础URL地址 /// 要下载的文件名 /// 文件保存的本地路径 /// 下载成功返回true,两个域名都失败则返回false private async Task DownloadFromOneDrive(string baseUrl, string fileName, string localPath) { try { UpdateStatus($"正在从123盘下载: {fileName}"); UpdateCount(""); UpdateSize(""); string authUrl = GenerateAuthUrl($"http://{OneDriveMainDomain}{OneDrivePath}", fileName); UpdateStatus($"使用主域名下载文件..."); UpdateCount(""); UpdateSize(""); var request = new HttpRequestMessage(HttpMethod.Get, authUrl) { Version = HttpVersion.Version11 }; request.Headers.Add("User-Agent", "CheckDownload/1.0"); using (var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)) { response.EnsureSuccessStatusCode(); string localDir = Path.GetDirectoryName(localPath); if (!Directory.Exists(localDir)) { Directory.CreateDirectory(localDir); } using (var remote = await response.Content.ReadAsStreamAsync()) using (var localFile = File.Create(localPath)) { await DownloadWithProgressAsync(remote, localFile, response.Content.Headers.ContentLength); } UpdateStatus($"123盘主域名下载成功: {fileName}"); UpdateCount(""); UpdateSize(""); return true; } } catch (Exception) { UpdateStatus($"123盘主域名下载失败,尝试备用域名: {fileName}"); UpdateCount(""); UpdateSize(""); } try { UpdateStatus($"正在从123盘备用域名下载: {fileName}"); UpdateCount(""); UpdateSize(""); string authUrl = GenerateAuthUrl($"http://{OneDriveBackupDomain}{OneDrivePath}", fileName); UpdateStatus($"使用备用域名下载文件..."); UpdateCount(""); UpdateSize(""); var request = new HttpRequestMessage(HttpMethod.Get, authUrl) { Version = HttpVersion.Version11 }; request.Headers.Add("User-Agent", "CheckDownload/1.0"); using (var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)) { response.EnsureSuccessStatusCode(); string localDir = Path.GetDirectoryName(localPath); if (!Directory.Exists(localDir)) { Directory.CreateDirectory(localDir); } using (var remote = await response.Content.ReadAsStreamAsync()) using (var localFile = File.Create(localPath)) { await DownloadWithProgressAsync(remote, localFile, response.Content.Headers.ContentLength); } UpdateStatus($"123盘备用域名下载成功: {fileName}"); UpdateCount(""); UpdateSize(""); return true; } } catch (Exception ex) { UpdateStatus($"123盘备用域名下载失败: {fileName} - {ex.Message}"); return false; } } /// /// 根据文件MD5从123盘下载文件,使用主备域名双重保障 /// /// 文件的相对路径(仅用于日志显示) /// 文件的MD5值,用于构建123盘存储路径 /// 文件保存的本地完整路径 /// 下载成功返回true,两个域名都失败则返回false private async Task DownloadFileFromOneDrive(string filePath, string expectedMd5, string localPath) { string fileName = $"File/{expectedMd5}"; try { string authUrl = GenerateAuthUrl($"http://{OneDriveMainDomain}{OneDrivePath}", fileName); var request = new HttpRequestMessage(HttpMethod.Get, authUrl) { Version = HttpVersion.Version11 }; request.Headers.Add("User-Agent", "CheckDownload/1.0"); using (var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)) { response.EnsureSuccessStatusCode(); string localDir = Path.GetDirectoryName(localPath); if (!Directory.Exists(localDir)) { Directory.CreateDirectory(localDir); } using (var remote = await response.Content.ReadAsStreamAsync()) using (var localFile = File.Create(localPath)) { await DownloadWithProgressAsync(remote, localFile, response.Content.Headers.ContentLength); } return true; } } catch (Exception) { } try { string authUrl = GenerateAuthUrl($"http://{OneDriveBackupDomain}{OneDrivePath}", fileName); var request = new HttpRequestMessage(HttpMethod.Get, authUrl) { Version = HttpVersion.Version11 }; request.Headers.Add("User-Agent", "CheckDownload/1.0"); using (var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)) { response.EnsureSuccessStatusCode(); string localDir = Path.GetDirectoryName(localPath); if (!Directory.Exists(localDir)) { Directory.CreateDirectory(localDir); } using (var remote = await response.Content.ReadAsStreamAsync()) using (var localFile = File.Create(localPath)) { await DownloadWithProgressAsync(remote, localFile, response.Content.Headers.ContentLength); } return true; } } catch (Exception) { return false; } } /// /// 对下载失败的文件进行重试,最多重试指定次数 /// /// 包含失败文件路径和MD5值的字典 /// 重试后仍然失败的文件字典 private async Task> RetryFailedFilesAsync(Dictionary failedFiles) { var filesToRetry = new Dictionary(failedFiles); for (int i = 0; i < MaxDownloadRetries && filesToRetry.Any(); i++) { UpdateStatus($"第 {i + 1} 次重试,剩余 {filesToRetry.Count} 个文件..."); UpdateCount(""); UpdateSize(""); var failedThisRound = new ConcurrentDictionary(); await PerformDownloads(filesToRetry, failedThisRound); filesToRetry = new Dictionary(failedThisRound); if (filesToRetry.Any() && i < MaxDownloadRetries - 1) { UpdateStatus($"等待 3 秒后进行下一次重试..."); UpdateCount(""); UpdateSize(""); await Task.Delay(3000); } } return filesToRetry; } /// /// 使用多个DNS服务器解析域名获取IP地址列表,提高解析成功率 /// /// 要解析的目标域名 /// 解析成功的IP地址列表,解析失败返回空列表 private async Task> GetIpAddressesForDomain(string domain) { foreach (var dnsServer in _dnsServers) { try { var requestUri = $"http://{dnsServer}/resolve?name={domain}&type=1&short=1"; var request = new HttpRequestMessage(HttpMethod.Get, requestUri) { Version = HttpVersion.Version11 }; request.Headers.Add("User-Agent", "CheckDownload/1.0"); request.Headers.Host = dnsServer; var response = await _httpClient.SendAsync(request); response.EnsureSuccessStatusCode(); string responseBody = await response.Content.ReadAsStringAsync(); var ips = JsonConvert.DeserializeObject>(responseBody); if (ips != null && ips.Any()) { UpdateStatus($"通过 {dnsServer} 成功解析域名 {domain}"); return ips; } } catch (Exception) { UpdateStatus($"通过 {dnsServer} 解析域名失败,尝试下一个..."); await Task.Delay(500); } } UpdateStatus($"所有DNS服务器均无法解析域名: {domain}"); return new List(); } /// /// 验证所有下载文件的MD5完整性并保存到目标位置,处理文件占用情况 /// private async Task VerifyAndSaveAllFiles() { UpdateStatus("正在校验文件..."); UpdateCount(""); UpdateSize(""); var failedFiles = new ConcurrentBag(); var filesForScripting = new ConcurrentBag<(string original, string newFile)>(); int totalFiles = _downloadedFiles.Count; int completed = 0; var semaphore = new SemaphoreSlim(Environment.ProcessorCount); var tasks = _downloadedFiles.Select(async item => { await semaphore.WaitAsync(); try { string relativePath = item.Key; string expectedMd5 = item.Value; string tempFilePath = Path.Combine(_tempDirectory, relativePath); string actualMd5 = CalculateMD5FromFile(tempFilePath); if (actualMd5 != expectedMd5.ToLower()) { failedFiles.Add(relativePath); return; } string localPath = Path.Combine(_baseDirectory, relativePath); string localDir = Path.GetDirectoryName(localPath); if (!Directory.Exists(localDir)) { Directory.CreateDirectory(localDir); } if (!await TryMoveFileAsync(tempFilePath, localPath)) { string backupPath = localPath + ".new"; File.Move(tempFilePath, backupPath); filesForScripting.Add((localPath, backupPath)); } } finally { int done = Interlocked.Increment(ref completed); int percentage = 95 + (int)(done * 5.0 / totalFiles); UpdateProgressValue(Math.Min(100, percentage)); semaphore.Release(); } }); await Task.WhenAll(tasks); if (filesForScripting.Any()) { CreateReplaceScriptForAll(filesForScripting.ToList()); } foreach (var failed in failedFiles) { _downloadedFiles.Remove(failed); } if (failedFiles.Count > 0) { throw new Exception($"{failedFiles.Count}个文件校验失败"); } else { UpdateStatus("所有文件校验和保存成功"); UpdateCount(""); UpdateSize(""); UpdateProgressValue(100); } } /// /// 尝试将文件从临时位置移动到目标位置(异步),若文件被占用则等待一秒后重试一次 /// /// 源文件的完整路径 /// 目标文件的完整路径 /// 移动成功返回true,失败返回false private async Task TryMoveFileAsync(string sourcePath, string targetPath) { try { File.Move(sourcePath, targetPath); return true; } catch (IOException) { UpdateStatus($"文件被占用,尝试解锁..."); try { await Task.Delay(1000); File.Move(sourcePath, targetPath); UpdateStatus($"文件解锁成功并已更新"); return true; } catch { UpdateStatus($"文件仍被占用,无法移动"); return false; } } catch (Exception ex) { UpdateStatus($"移动文件时发生错误: {ex.Message}"); return false; } } /// /// 为被占用文件创建批处理脚本,在程序退出后自动完成文件替换 /// /// 包含原始文件路径和新文件路径的元组列表 private void CreateReplaceScriptForAll(List<(string original, string newFile)> files) { if (!files.Any()) return; try { string batchFilePath = Path.Combine(_baseDirectory, "update_files.bat"); string processId = Process.GetCurrentProcess().Id.ToString(); var batchContent = new StringBuilder(); batchContent.AppendLine("@echo off"); batchContent.AppendLine("chcp 65001 > nul"); batchContent.AppendLine(":check_process"); batchContent.AppendLine($"tasklist /FI \"PID eq {processId}\" 2>NUL | find /I \"{processId}\" >NUL"); batchContent.AppendLine("if %ERRORLEVEL% == 0 ("); batchContent.AppendLine(" timeout /t 1 /nobreak > NUL"); batchContent.AppendLine(" goto check_process"); batchContent.AppendLine(")"); batchContent.AppendLine("timeout /t 2 /nobreak > NUL"); foreach (var file in files) { batchContent.AppendLine($"del \"{file.original}\" /f /q"); batchContent.AppendLine($"move \"{file.newFile}\" \"{file.original}\""); } batchContent.AppendLine("del \"%~f0\" /f /q"); batchContent.AppendLine("exit"); File.WriteAllText(batchFilePath, batchContent.ToString(), new UTF8Encoding(false)); var startInfo = new ProcessStartInfo { FileName = "cmd.exe", Arguments = $"/c \"{batchFilePath}\"", CreateNoWindow = true, UseShellExecute = false, WindowStyle = ProcessWindowStyle.Hidden }; Process.Start(startInfo); } catch (Exception ex) { UpdateStatus($"创建替换脚本时出错: {ex.Message}"); } } /// /// 初始化程序使用的临时目录,用于存储下载过程中的临时文件 /// private void InitializeTempDirectory() { try { _tempDirectory = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Temp", "CheckDownload" ); if (!Directory.Exists(_tempDirectory)) { Directory.CreateDirectory(_tempDirectory); } } catch (Exception ex) { UpdateStatus($"初始化临时目录失败: {ex.Message}"); } } /// /// 计算字符串的MD5哈希值,返回32位小写十六进制字符串 /// /// 要计算哈希值的字符串 /// 32位小写十六进制MD5哈希值 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(); } } /// /// 从阿里云OSS下载文件,包含SDK直连和IP直连两种备用方案 /// /// OSS对象的存储键值 /// 文件保存的本地路径 /// 下载成功返回true,所有方案都失败返回false private async Task DownloadFileWithFallback(string ossKey, string localPath) { try { var obj = _ossClient.GetObject(OssBucketName, ossKey); string localDir = Path.GetDirectoryName(localPath); if (!Directory.Exists(localDir)) { Directory.CreateDirectory(localDir); } using (var fileStream = File.Create(localPath)) { await DownloadWithProgressAsync(obj.Content, fileStream, obj.Metadata.ContentLength); } return true; } catch (Exception ex) when (ex is OssException || ex is WebException || ex is IOException) { UpdateStatus($"主下载失败,尝试备用方案..."); } var domain = new Uri("https://" + OssEndpoint).Host; List ips = await GetIpAddressesForDomain(domain); if (ips == null || ips.Count == 0) { UpdateStatus($"无法获取IP地址"); return false; } var req = new GeneratePresignedUriRequest(OssBucketName, ossKey, SignHttpMethod.Get) { Expiration = DateTime.Now.AddMinutes(10) }; var signedUrl = _ossClient.GeneratePresignedUri(req).ToString(); var signedUri = new Uri(signedUrl); foreach (var ip in ips) { try { var ipUrl = signedUrl.Replace(signedUri.Host, ip); var request = new HttpRequestMessage(HttpMethod.Get, ipUrl) { Version = HttpVersion.Version11 }; request.Headers.Host = signedUri.Host; using (var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)) { response.EnsureSuccessStatusCode(); string localDir = Path.GetDirectoryName(localPath); if (!Directory.Exists(localDir)) { Directory.CreateDirectory(localDir); } using (var remote = await response.Content.ReadAsStreamAsync()) using (var localFile = File.Create(localPath)) { await DownloadWithProgressAsync(remote, localFile, response.Content.Headers.ContentLength); } } return true; } catch (Exception) { UpdateStatus($"备用方案用IP {ip} 下载失败"); } } return false; } /// /// 生成基于当前UTC时间的Unix时间戳,并添加指定的秒数偏移 /// /// 要在当前时间戳基础上增加的秒数 /// Unix时间戳(秒) private long GenerateTimestamp(int number) { return (long)(DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1))).TotalSeconds + number; } /// /// 使用指定数字作为随机种子生成确定性的32位UUID字符串 /// /// 用作随机种子的数字 /// 不含连字符的32位UUID字符串 private string GenerateUUID(int number) { var random = new Random(number); var guid = new byte[16]; random.NextBytes(guid); guid[7] = (byte)((guid[7] & 0x0F) | 0x40); guid[8] = (byte)((guid[8] & 0x3F) | 0x80); var resultGuid = new Guid(guid); return resultGuid.ToString("N").Replace("-", ""); } /// /// 计算字符串的MD5哈希值,用于123盘鉴权签名生成 /// /// 要计算哈希值的输入字符串 /// 32位小写十六进制MD5哈希值 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(); } } /// /// 为123盘文件访问生成带有时间戳和签名验证的鉴权URL /// /// 123盘的基础访问URL /// 要访问的文件名或路径 /// 包含鉴权参数的完整访问URL private string GenerateAuthUrl(string url, string file) { try { long timestamp = GenerateTimestamp(OneDriveAuthTimeout); string rand = GenerateUUID(16); string pathPart = ExtractPathFromUrl(url); string fullUrl = $"{url}/{file}"; string signString = $"/{pathPart}/{file}-{timestamp}-{rand}-{OneDriveUid}-{OneDriveAuthKey}"; string signature = GenerateMD5(signString); string authUrl = $"{fullUrl}?auth_key={timestamp}-{rand}-{OneDriveUid}-{signature}"; return authUrl; } catch (Exception ex) { UpdateStatus($"生成鉴权URL失败: {ex.Message}"); return $"{url}/{file}"; } } /// /// 从完整URL中解析并提取域名后的路径部分,用于生成123盘签名 /// /// 要解析的完整URL地址 /// 去除域名和协议后的路径字符串 private string ExtractPathFromUrl(string url) { string pathPart = ""; try { var uri = new Uri(url); pathPart = uri.AbsolutePath.TrimStart('/'); if (string.IsNullOrEmpty(pathPart)) { string basePattern = $"{uri.Scheme}://{uri.Host}"; if (url.StartsWith(basePattern)) { pathPart = url.Substring(basePattern.Length).TrimStart('/'); } } } catch { int protocolIndex = url.IndexOf("://"); if (protocolIndex > 0) { int domainEndIndex = url.IndexOf('/', protocolIndex + 3); if (domainEndIndex > 0) { pathPart = url.Substring(domainEndIndex + 1); } } } return pathPart; } /// /// 下载MD5配置文件,优先使用123盘,失败后切换到阿里云OSS备用方案 /// /// MD5配置文件名 /// 文件保存的本地路径 /// 下载成功返回true,所有方案都失败返回false private async Task DownloadMd5FileWithFallback(string fileName, string localPath) { try { UpdateStatus("尝试从123盘下载MD5文件..."); if (await DownloadFromOneDrive($"http://{OneDriveMainDomain}{OneDrivePath}", fileName, localPath)) { return true; } UpdateStatus("123盘下载失败,尝试阿里云OSS备用方案..."); return await DownloadFileWithFallback(fileName, localPath); } catch (Exception ex) { UpdateStatus($"所有下载方式都失败: {ex.Message}"); return false; } } /// /// 清理程序使用的临时目录,删除所有下载过程中产生的临时文件 /// private void CleanupTempDirectory() { try { string tempPath = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Temp", "CheckDownload" ); if (Directory.Exists(tempPath)) { UpdateStatus("清理临时文件..."); Directory.Delete(tempPath, true); } } catch (Exception) { } } /// /// 通过分析md5.json文件内容智能确定项目基准目录,用于正确定位文件更新路径 /// /// md5.json文件中的数据对象 private void InitializeBaseDirectoryFromMd5Data(JObject data) { try { string currentProgramName = Path.GetFileName(Application.ExecutablePath); string programPathInMd5 = FindFileInMd5Data(data, currentProgramName); if (!string.IsNullOrEmpty(programPathInMd5)) { string programDirInMd5 = Path.GetDirectoryName(programPathInMd5); if (programDirInMd5 == null) { programDirInMd5 = ""; } if (string.IsNullOrEmpty(programDirInMd5)) { programDirInMd5 = ""; } string currentProgramDir = Application.StartupPath; if (string.IsNullOrEmpty(programDirInMd5)) { _baseDirectory = currentProgramDir; } else { _baseDirectory = FindProjectBaseDirectory(currentProgramDir, programDirInMd5); } } else { _baseDirectory = Application.StartupPath; } } catch (Exception) { _baseDirectory = Application.StartupPath; } finally { if (string.IsNullOrEmpty(_baseDirectory)) { _baseDirectory = Application.StartupPath; } } } /// /// 在md5.json的数据结构中递归搜索指定的文件名 /// /// md5.json解析后的JSON对象 /// 要搜索的目标文件名 /// 当前递归的路径,用于构建完整路径 /// 找到的文件在md5.json中的完整相对路径,未找到返回null private string FindFileInMd5Data(JObject data, string fileName, string currentPath = "") { foreach (var property in data.Properties()) { string key = property.Name; JToken value = property.Value; string fullPath = string.IsNullOrEmpty(currentPath) ? key : Path.Combine(currentPath, key); if (value.Type == JTokenType.String) { if (string.Equals(key, fileName, StringComparison.OrdinalIgnoreCase)) { return fullPath; } } else if (value.Type == JTokenType.Object) { string result = FindFileInMd5Data((JObject)value, fileName, fullPath); if (!string.IsNullOrEmpty(result)) { return result; } } } return null; } /// /// 通过分析程序在md5.json中的相对路径,向上递归查找项目根目录 /// /// 当前程序运行的目录 /// 程序在md5.json中的相对路径 /// 项目的基准目录路径 private string FindProjectBaseDirectory(string currentDir, string expectedRelativePath) { try { string[] expectedDirs = expectedRelativePath.Split(new char[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries); string checkDir = currentDir; for (int i = 0; i < expectedDirs.Length; i++) { string parentDir = Directory.GetParent(checkDir)?.FullName; if (string.IsNullOrEmpty(parentDir)) { break; } string currentDirName = Path.GetFileName(checkDir); string expectedDirName = expectedDirs[expectedDirs.Length - 1 - i]; if (string.Equals(currentDirName, expectedDirName, StringComparison.OrdinalIgnoreCase)) { if (i == expectedDirs.Length - 1) { return parentDir; } checkDir = parentDir; } else { break; } } return currentDir; } catch (Exception) { 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(); } } /// /// 异步将远程数据流写入本地文件流,并实时更新下载进度 /// /// 远程数据源的流 /// 本地文件的流 /// 文件的总字节数,用于计算进度 private async Task DownloadWithProgressAsync(Stream remoteStream, Stream localStream, long? totalBytes) { long totalRead = 0; byte[] buffer = new byte[8192]; // 8KB 缓冲区 int bytesRead; while ((bytesRead = await remoteStream.ReadAsync(buffer, 0, buffer.Length)) > 0) { await localStream.WriteAsync(buffer, 0, bytesRead); totalRead += bytesRead; // === 新增: 统计整体下载量 === Interlocked.Add(ref _totalDownloadedBytes, bytesRead); Interlocked.Add(ref _bytesSinceLastSpeedCalc, bytesRead); // === 修改: 显示整体下载进度和速度(每 500ms 更新一次,避免频繁刷新) === if (DateTime.UtcNow - _lastSpeedUpdateTime > TimeSpan.FromMilliseconds(500)) { lock (_speedLock) { if (DateTime.UtcNow - _lastSpeedUpdateTime > TimeSpan.FromMilliseconds(500)) { UpdateOverallSize(); } } } } } /// /// 将字节大小格式化为更易读的单位(B, KB, MB, GB, TB) /// /// 要格式化的字节数 /// 格式化后的字符串 private static string FormatBytes(long bytes) { string[] suffixes = { "B", "KB", "MB", "GB", "TB" }; int i = 0; double dblSByte = bytes; if (bytes > 1024) { for (i = 0; (bytes / 1024) > 0 && i < suffixes.Length - 1; i++, bytes /= 1024) { dblSByte = bytes / 1024.0; } } return $"{dblSByte:0.0}{suffixes[i]}"; } /// /// 异步解压 tim.7z 文件(如果存在)。 /// private async Task DecompressTim7zAsync() { try { var sevenZipFile = Directory.EnumerateFiles(_baseDirectory, "tim.7z", SearchOption.AllDirectories).FirstOrDefault(); if (sevenZipFile != null) { UpdateStatus("正在解压..."); await Task.Run(() => { try { string sevenZipDllPath = Extract7zDll(); if (string.IsNullOrEmpty(sevenZipDllPath)) { throw new Exception("无法提取7z.dll,解压中止。"); } string extractionPath = Path.GetDirectoryName(sevenZipFile); string tempExtractionDir = Path.Combine(_tempDirectory, Path.GetFileNameWithoutExtension(sevenZipFile) + "_temp"); if (Directory.Exists(tempExtractionDir)) { Directory.Delete(tempExtractionDir, true); } Directory.CreateDirectory(tempExtractionDir); using (var archiveFile = new ArchiveFile(sevenZipFile, sevenZipDllPath)) { archiveFile.Extract(tempExtractionDir, true); } bool requiresKill = Directory.EnumerateFiles(tempExtractionDir, "tim.dll", SearchOption.AllDirectories).Any(); if (requiresKill) { KillProcessByBaseName("tim"); } foreach (string file in Directory.GetFiles(tempExtractionDir, "*.*", SearchOption.AllDirectories)) { string relativePath = file.Substring(tempExtractionDir.Length + 1); string destFile = Path.Combine(extractionPath, relativePath); string destDir = Path.GetDirectoryName(destFile); if (!Directory.Exists(destDir)) { Directory.CreateDirectory(destDir); } File.Copy(file, destFile, true); } Directory.Delete(tempExtractionDir, true); UpdateStatus("为解压的程序设置管理员权限..."); var exeFiles = Directory.GetFiles(extractionPath, "*.exe", SearchOption.AllDirectories); foreach (var exeFile in exeFiles) { SetRunAsAdminCompatibility(exeFile); } } catch (Exception ex) { throw new Exception($"解压失败: {ex.Message}"); } }); UpdateStatus("tim.7z 解压完成。"); await Task.Delay(1000); } } catch (Exception ex) { UpdateStatus($"处理 tim.7z 时出错: {ex.Message}"); await Task.Delay(3000); } } /// /// 为指定程序路径在注册表中设置"以管理员身份运行"的兼容性标志。 /// /// 要设置的.exe文件的完整路径。 private void SetRunAsAdminCompatibility(string exePath) { const string keyPath = @"Software\Microsoft\Windows NT\CurrentVersion\AppCompatFlags\Layers"; try { using (RegistryKey key = Registry.CurrentUser.CreateSubKey(keyPath)) { if (key != null) { key.SetValue(exePath, "~ RUNASADMIN"); } else { UpdateStatus($"无法打开或创建注册表项: {keyPath}"); } } } catch (Exception ex) { UpdateStatus($"设置管理员权限失败: {exePath} - {ex.Message}"); } } /// /// 从嵌入的资源中提取与当前进程体系结构匹配的7z.dll到临时目录。 /// /// 提取的7z.dll的路径,如果失败则返回null。 private string Extract7zDll() { try { string dllName = Environment.Is64BitProcess ? "7z-x64.dll" : "7z-x86.dll"; string resourceName = $"CheckDownload.{dllName}"; string dllPath = Path.Combine(_tempDirectory, "7z.dll"); if (File.Exists(dllPath)) { // 可以选择在这里添加对现有DLL版本的校验,但为简化,我们先直接返回 return dllPath; } using (var resourceStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName)) { if (resourceStream == null) { UpdateStatus($"在嵌入资源中未找到 {dllName}。"); return null; } using (var fileStream = new FileStream(dllPath, FileMode.Create, FileAccess.Write)) { resourceStream.CopyTo(fileStream); } } return dllPath; } catch (Exception ex) { UpdateStatus($"提取7z.dll失败: {ex.Message}"); return null; } } // add helper to check db extension private bool IsDatabaseFile(string relativePath) { string ext = Path.GetExtension(relativePath)?.ToLowerInvariant(); return ext == ".db" || ext == ".db3"; } private void KillProcessIfRunning(string exeRelativePath) { try { string exeName = Path.GetFileNameWithoutExtension(exeRelativePath); foreach (var proc in Process.GetProcessesByName(exeName)) { try { proc.Kill(); proc.WaitForExit(5000); } catch { } } } catch { } } private void KillProcessByBaseName(string baseName) { try { foreach (var proc in Process.GetProcesses()) { if (string.Equals(proc.ProcessName, baseName, StringComparison.OrdinalIgnoreCase)) { try { proc.Kill(); proc.WaitForExit(5000); } catch { } } } } catch { } } /// /// Truncates a string to a maximum length and appends an ellipsis. /// /// The string to truncate. /// The maximum length of the string. /// The truncated string. private string TruncateString(string value, int maxLength) { if (string.IsNullOrEmpty(value)) return value; return value.Length <= maxLength ? value : value.Substring(0, maxLength) + "..."; } // === 新增: 更新整体下载大小与速度 === private void UpdateOverallSize() { DateTime now = DateTime.UtcNow; double intervalSeconds = (now - _lastSpeedUpdateTime).TotalSeconds; if (intervalSeconds <= 0) intervalSeconds = 0.1; // 防止除零 // 读取并清零自上次计算以来的字节数 long intervalBytes = Interlocked.Exchange(ref _bytesSinceLastSpeedCalc, 0); double bytesPerSec = intervalBytes / intervalSeconds; long downloaded = Interlocked.Read(ref _totalDownloadedBytes); string speedText = $"{FormatBytes((long)bytesPerSec)}/s"; string sizeText = $"{FormatBytes(downloaded)}({speedText})"; _lastSpeedUpdateTime = now; UpdateSize(sizeText); } } }