using System.Net; using System.Text.Json; using WorkCameraExport.Services.Interfaces; namespace WorkCameraExport.Services { /// /// 可恢复下载器 - 支持断点续传 /// public class ResumableDownloader : IDisposable { private readonly HttpClient _httpClient; private readonly ILogService? _logService; private readonly string _downloadStateDir; private bool _disposed; // 下载状态文件扩展名 private const string StateFileExtension = ".download"; // 临时文件扩展名 private const string TempFileExtension = ".tmp"; // 分块大小(1MB) private const int ChunkSize = 1024 * 1024; public ResumableDownloader(IConfigService configService, ILogService? logService = null) { _logService = logService; _downloadStateDir = Path.Combine(configService.TempPath, "downloads"); if (!Directory.Exists(_downloadStateDir)) { Directory.CreateDirectory(_downloadStateDir); } _httpClient = new HttpClient { Timeout = TimeSpan.FromMinutes(5) }; _httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("WorkCameraExport/2.0 (Windows; .NET)"); } /// /// 下载文件(支持断点续传) /// public async Task DownloadFileAsync( string url, string outputPath, IProgress? progress = null, CancellationToken cancellationToken = default) { var downloadId = GetDownloadId(url); var stateFilePath = Path.Combine(_downloadStateDir, $"{downloadId}{StateFileExtension}"); var tempFilePath = outputPath + TempFileExtension; try { // 加载或创建下载状态 var state = LoadDownloadState(stateFilePath) ?? new DownloadState { Url = url, OutputPath = outputPath, DownloadedBytes = 0, TotalBytes = 0, StartTime = DateTime.Now }; // 检查临时文件是否存在,获取已下载的字节数 long downloadedBytes = 0; if (File.Exists(tempFilePath)) { var fileInfo = new FileInfo(tempFilePath); downloadedBytes = fileInfo.Length; state.DownloadedBytes = downloadedBytes; } // 获取文件总大小 var totalBytes = await GetFileSizeAsync(url, cancellationToken); if (totalBytes <= 0) { _logService?.Warn($"无法获取文件大小: {url}"); // 如果无法获取文件大小,尝试普通下载 return await DownloadWithoutResumeAsync(url, outputPath, progress, cancellationToken); } state.TotalBytes = totalBytes; // 如果已经下载完成 if (downloadedBytes >= totalBytes) { if (File.Exists(tempFilePath)) { File.Move(tempFilePath, outputPath, true); } CleanupDownloadState(stateFilePath); progress?.Report(new DownloadProgressInfo(totalBytes, totalBytes, true)); return true; } // 创建带 Range 头的请求 using var request = new HttpRequestMessage(HttpMethod.Get, url); if (downloadedBytes > 0) { request.Headers.Range = new System.Net.Http.Headers.RangeHeaderValue(downloadedBytes, null); _logService?.Info($"断点续传: 从 {downloadedBytes} 字节开始下载"); } using var response = await _httpClient.SendAsync( request, HttpCompletionOption.ResponseHeadersRead, cancellationToken); // 检查服务器是否支持断点续传 if (response.StatusCode == HttpStatusCode.PartialContent || response.StatusCode == HttpStatusCode.OK) { // 如果服务器返回 200 OK(不支持 Range),需要重新下载 if (response.StatusCode == HttpStatusCode.OK && downloadedBytes > 0) { _logService?.Info("服务器不支持断点续传,重新下载"); downloadedBytes = 0; if (File.Exists(tempFilePath)) { File.Delete(tempFilePath); } } // 打开文件流(追加模式) using var fileStream = new FileStream( tempFilePath, downloadedBytes > 0 ? FileMode.Append : FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: ChunkSize, useAsync: true); using var contentStream = await response.Content.ReadAsStreamAsync(cancellationToken); var buffer = new byte[ChunkSize]; int bytesRead; var lastProgressReport = DateTime.Now; while ((bytesRead = await contentStream.ReadAsync(buffer, cancellationToken)) > 0) { await fileStream.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken); downloadedBytes += bytesRead; state.DownloadedBytes = downloadedBytes; // 每秒报告一次进度 if ((DateTime.Now - lastProgressReport).TotalMilliseconds >= 500) { progress?.Report(new DownloadProgressInfo(downloadedBytes, totalBytes, false)); SaveDownloadState(stateFilePath, state); lastProgressReport = DateTime.Now; } } // 下载完成,移动临时文件到目标位置 fileStream.Close(); File.Move(tempFilePath, outputPath, true); CleanupDownloadState(stateFilePath); progress?.Report(new DownloadProgressInfo(totalBytes, totalBytes, true)); _logService?.Info($"下载完成: {url}"); return true; } else { _logService?.Warn($"下载失败,状态码: {response.StatusCode}"); return false; } } catch (OperationCanceledException) { _logService?.Info($"下载已取消: {url}"); return false; } catch (Exception ex) { _logService?.Error($"下载异常: {url}", ex); return false; } } /// /// 不支持断点续传的普通下载 /// private async Task DownloadWithoutResumeAsync( string url, string outputPath, IProgress? progress, CancellationToken cancellationToken) { try { using var response = await _httpClient.GetAsync(url, cancellationToken); if (response.IsSuccessStatusCode) { var data = await response.Content.ReadAsByteArrayAsync(cancellationToken); await File.WriteAllBytesAsync(outputPath, data, cancellationToken); progress?.Report(new DownloadProgressInfo(data.Length, data.Length, true)); return true; } return false; } catch (Exception ex) { _logService?.Error($"普通下载失败: {url}", ex); return false; } } /// /// 获取文件大小 /// private async Task GetFileSizeAsync(string url, CancellationToken cancellationToken) { try { using var request = new HttpRequestMessage(HttpMethod.Head, url); using var response = await _httpClient.SendAsync(request, cancellationToken); if (response.IsSuccessStatusCode && response.Content.Headers.ContentLength.HasValue) { return response.Content.Headers.ContentLength.Value; } } catch (Exception ex) { _logService?.Warn($"获取文件大小失败: {ex.Message}"); } return -1; } /// /// 获取下载 ID(基于 URL 的哈希) /// private static string GetDownloadId(string url) { using var sha256 = System.Security.Cryptography.SHA256.Create(); var hashBytes = sha256.ComputeHash(System.Text.Encoding.UTF8.GetBytes(url)); return Convert.ToHexString(hashBytes)[..16]; } /// /// 加载下载状态 /// private DownloadState? LoadDownloadState(string stateFilePath) { try { if (File.Exists(stateFilePath)) { var json = File.ReadAllText(stateFilePath); return JsonSerializer.Deserialize(json); } } catch (Exception ex) { _logService?.Warn($"加载下载状态失败: {ex.Message}"); } return null; } /// /// 保存下载状态 /// private void SaveDownloadState(string stateFilePath, DownloadState state) { try { var json = JsonSerializer.Serialize(state); File.WriteAllText(stateFilePath, json); } catch (Exception ex) { _logService?.Warn($"保存下载状态失败: {ex.Message}"); } } /// /// 清理下载状态 /// private void CleanupDownloadState(string stateFilePath) { try { if (File.Exists(stateFilePath)) { File.Delete(stateFilePath); } } catch { // 忽略清理错误 } } /// /// 清理所有未完成的下载 /// public void CleanupIncompleteDownloads() { try { if (Directory.Exists(_downloadStateDir)) { foreach (var file in Directory.GetFiles(_downloadStateDir, $"*{StateFileExtension}")) { try { File.Delete(file); } catch { } } } } catch (Exception ex) { _logService?.Warn($"清理未完成下载失败: {ex.Message}"); } } /// /// 获取未完成的下载列表 /// public List GetIncompleteDownloads() { var result = new List(); try { if (Directory.Exists(_downloadStateDir)) { foreach (var file in Directory.GetFiles(_downloadStateDir, $"*{StateFileExtension}")) { var state = LoadDownloadState(file); if (state != null) { result.Add(state); } } } } catch (Exception ex) { _logService?.Warn($"获取未完成下载列表失败: {ex.Message}"); } return result; } #region IDisposable public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (!_disposed) { if (disposing) { _httpClient.Dispose(); } _disposed = true; } } #endregion } /// /// 下载状态 /// public class DownloadState { public string Url { get; set; } = ""; public string OutputPath { get; set; } = ""; public long DownloadedBytes { get; set; } public long TotalBytes { get; set; } public DateTime StartTime { get; set; } } /// /// 下载进度信息 /// public class DownloadProgressInfo { public long DownloadedBytes { get; } public long TotalBytes { get; } public bool IsCompleted { get; } public double ProgressPercent => TotalBytes > 0 ? (double)DownloadedBytes / TotalBytes * 100 : 0; public DownloadProgressInfo(long downloadedBytes, long totalBytes, bool isCompleted) { DownloadedBytes = downloadedBytes; TotalBytes = totalBytes; IsCompleted = isCompleted; } } }