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;
}
}
}