Files
CheckDownload/Form1.cs
qinsi_travel 0891d7534a 恢复密钥
2025-07-02 21:23:36 +08:00

1764 lines
68 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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