1726 lines
67 KiB
C#
1726 lines
67 KiB
C#
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<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;
|
||
|
||
// 7z.dll 库文件路径
|
||
private readonly string _sevenZipDllPath;
|
||
|
||
// 应用程序名称
|
||
private readonly string _appName;
|
||
|
||
// 当前进程ID
|
||
private readonly int _currentProcessId;
|
||
|
||
// === 新增: 用于显示整体下载大小与速度 ===
|
||
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();
|
||
|
||
// 设置 7z.dll 的路径为程序运行目录
|
||
_sevenZipDllPath = Path.Combine(Application.StartupPath, "7z.dll");
|
||
|
||
_appName = Assembly.GetExecutingAssembly().GetName().Name;
|
||
_currentProcessId = Process.GetCurrentProcess().Id;
|
||
_baseDirectory = Application.StartupPath;
|
||
ConfigureProgressBar();
|
||
}
|
||
|
||
/// <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
|
||
{
|
||
InitializeTempDirectory();
|
||
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("UpdateFile: 所有文件下载失败。");
|
||
}
|
||
|
||
await VerifyAndSaveAllFiles();
|
||
|
||
await DecompressTim7zAsync();
|
||
|
||
// 校验和保存成功后清理临时目录
|
||
CleanupTempDirectory();
|
||
|
||
// 显示完成状态并退出
|
||
UpdateStatus("更新完成");
|
||
await Task.Delay(3000);
|
||
this.Close();
|
||
return;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
HandleError("文件更新主流程 (UpdateFile)", ex);
|
||
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)
|
||
{
|
||
throw new Exception($"ReadOnlineMd5File: 读取或解析在线MD5文件 '{filePath}' 失败。", ex);
|
||
}
|
||
}
|
||
|
||
/// <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 };
|
||
|
||
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 };
|
||
|
||
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 };
|
||
|
||
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 };
|
||
|
||
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.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)
|
||
{
|
||
string failedFileList = string.Join(", ", failedFiles.Take(3));
|
||
if (failedFiles.Count > 3) failedFileList += "...";
|
||
throw new Exception($"VerifyAndSaveAllFiles: {failedFiles.Count}个文件校验失败: {failedFileList}");
|
||
}
|
||
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)
|
||
{
|
||
HandleError("创建文件替换脚本 (CreateReplaceScriptForAll)", ex);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 初始化程序使用的临时目录,用于存储下载过程中的临时文件
|
||
/// </summary>
|
||
private void InitializeTempDirectory()
|
||
{
|
||
try
|
||
{
|
||
_tempDirectory = Path.Combine(
|
||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||
"Temp",
|
||
_appName
|
||
);
|
||
|
||
if (!Directory.Exists(_tempDirectory))
|
||
{
|
||
Directory.CreateDirectory(_tempDirectory);
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
throw new Exception("初始化临时目录 (InitializeTempDirectory)", ex);
|
||
}
|
||
}
|
||
|
||
/// <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",
|
||
_appName
|
||
);
|
||
|
||
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 extractionPath = Path.GetDirectoryName(sevenZipFile);
|
||
string tempExtractionDir = Path.Combine(_tempDirectory, Path.GetFileNameWithoutExtension(sevenZipFile) + "_temp");
|
||
|
||
if (Directory.Exists(tempExtractionDir))
|
||
{
|
||
Directory.Delete(tempExtractionDir, true);
|
||
}
|
||
Directory.CreateDirectory(tempExtractionDir);
|
||
|
||
// 使用 SevenZipExtractor 解压文件到临时目录
|
||
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);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
throw new Exception("DecompressTim7zAsync: 解压失败。", ex);
|
||
}
|
||
});
|
||
|
||
UpdateStatus("tim.7z 解压完成。");
|
||
await Task.Delay(1000);
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
throw new Exception("DecompressTim7zAsync: 处理 tim.7z 时出错。", ex);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 为指定程序路径在注册表中设置"以管理员身份运行"的兼容性标志。
|
||
/// </summary>
|
||
/// <param name="exePath">要设置的.exe文件的完整路径。</param>
|
||
// 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);
|
||
string targetExeDir = Path.GetDirectoryName(Path.Combine(_baseDirectory, exeRelativePath));
|
||
|
||
foreach (var proc in Process.GetProcessesByName(exeName))
|
||
{
|
||
if (proc.Id == _currentProcessId) continue;
|
||
try
|
||
{
|
||
string runningProcessDir = Path.GetDirectoryName(proc.MainModule.FileName);
|
||
if (string.Equals(runningProcessDir, targetExeDir, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
proc.Kill();
|
||
proc.WaitForExit(5000);
|
||
}
|
||
}
|
||
catch { }
|
||
}
|
||
}
|
||
catch { }
|
||
}
|
||
|
||
private void KillProcessByBaseName(string baseName)
|
||
{
|
||
try
|
||
{
|
||
foreach (var proc in Process.GetProcessesByName(baseName))
|
||
{
|
||
if (proc.Id == _currentProcessId) continue;
|
||
try
|
||
{
|
||
if (proc.MainModule.FileName.StartsWith(_baseDirectory, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
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);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 统一处理并显示错误信息
|
||
/// </summary>
|
||
/// <param name="location">要显示给用户的基本错误信息</param>
|
||
/// <param name="ex">可选的异常对象,其消息将被附加</param>
|
||
private void HandleError(string location, Exception ex = null)
|
||
{
|
||
UpdateStatus("更新失败");
|
||
|
||
// 递归查找最深层的 InnerException 以获取最根本的错误信息
|
||
var innermostException = ex;
|
||
while (innermostException?.InnerException != null)
|
||
{
|
||
innermostException = innermostException.InnerException;
|
||
}
|
||
|
||
string errorMessage = innermostException?.Message ?? ex?.Message ?? "发生未知错误。";
|
||
MessageBox.Show(errorMessage, "更新错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||
}
|
||
}
|
||
} |