Files

224 lines
8.7 KiB
C#
Raw Permalink Normal View History

using System.IO;
using System.Net.Http;
using Downloader;
using PCL.Core.IO.Net;
namespace PCL.Network;
public static class FileDownloader
{
public static async Task DownloadAsync(string url, string localPath, bool useBrowserUserAgent = false,
string customUserAgent = "", CancellationToken cancellationToken = default,
bool enableParallelChunks = true, DownloadFile? trackedFile = null)
{
await DownloadCoreAsync([url], localPath, useBrowserUserAgent, customUserAgent, cancellationToken,
enableParallelChunks, trackedFile).ConfigureAwait(false);
}
public static async Task DownloadAsync(IEnumerable<string> urls, string localPath, bool useBrowserUserAgent = false,
string customUserAgent = "", CancellationToken cancellationToken = default,
bool enableParallelChunks = true, DownloadFile? trackedFile = null)
{
await DownloadCoreAsync(urls, localPath, useBrowserUserAgent, customUserAgent, cancellationToken,
enableParallelChunks, trackedFile).ConfigureAwait(false);
}
public static void DownloadByLoader(string url, string localPath, bool useBrowserUserAgent = false,
string customUserAgent = "")
{
DownloadAsync(url, localPath, useBrowserUserAgent, customUserAgent).GetAwaiter().GetResult();
}
public static void DownloadByLoader(IEnumerable<string> urls, string localPath, bool useBrowserUserAgent = false,
string customUserAgent = "")
{
DownloadAsync(urls, localPath, useBrowserUserAgent, customUserAgent).GetAwaiter().GetResult();
}
private static async Task DownloadCoreAsync(IEnumerable<string> urls, string localPath, bool useBrowserUserAgent,
string customUserAgent, CancellationToken cancellationToken, bool enableParallelChunks, DownloadFile? trackedFile)
{
var urlList = urls.Select(url => RequestSigning.SecretCdnSign(url.Trim())).Where(url => !string.IsNullOrWhiteSpace(url))
.Distinct().ToList();
if (urlList.Count == 0)
throw new ArgumentException("未提供可用的下载地址", nameof(urls));
Directory.CreateDirectory(Path.GetDirectoryName(localPath) ?? throw new ArgumentException("下载路径无效", nameof(localPath)));
Exception? lastException = null;
foreach (var url in urlList)
{
try
{
await DownloadSingleAsync(url, localPath, useBrowserUserAgent, customUserAgent, cancellationToken,
enableParallelChunks, trackedFile).ConfigureAwait(false);
return;
}
catch (OperationCanceledException)
{
CleanupTempFiles(localPath);
throw;
}
catch (Exception ex)
{
lastException = ex;
CleanupTempFiles(localPath);
ModBase.Log(ex, $"[Download] 下载失败,尝试下一个源:{url}", ModBase.LogLevel.Debug);
}
}
throw new IOException($"下载失败:{localPath}", lastException);
}
private static async Task DownloadSingleAsync(string url, string localPath, bool useBrowserUserAgent,
string customUserAgent, CancellationToken cancellationToken, bool enableParallelChunks, DownloadFile? trackedFile)
{
ModBase.Log($"[Download] 开始下载:{url} -> {localPath}");
CleanupTempFiles(localPath);
var perFileThreadLimit = enableParallelChunks ? Math.Max(1, ModNet.NetTaskThreadLimit) : 1;
// 限制最大分块数,防止大文件下载时内存爆炸
var chunkCount = Math.Min(perFileThreadLimit, 4);
var configuration = new DownloadConfiguration
{
ChunkCount = chunkCount,
ParallelCount = chunkCount,
ParallelDownload = chunkCount > 1,
MaximumBytesPerSecond = ModNet.NetTaskSpeedLimitHigh > 0 ? ModNet.NetTaskSpeedLimitHigh : 0,
MaxTryAgainOnFailure = 2,
BlockTimeout = 60000,
DownloadFileExtension = ModNet.netDownloadEnd,
EnableAutoResumeDownload = false,
CustomHttpClientFactory = () => GetHttpClient(url),
MinimumSizeOfChunking = 1024 * 1024L,
MaximumMemoryBufferBytes = 256L * 1024 * 1024,
};
using var downloader = new DownloadService(configuration);
using var cancelReg = cancellationToken.Register(() =>
{
try { downloader.CancelAsync(); } catch { } // 忽略
});
var tcs = new TaskCompletionSource<bool>();
void UpdateDownloadStat(DownloadProgressChangedEventArgs args)
{
if (trackedFile is null)
return;
trackedFile.State = PCL.Network.NetState.Downloading;
trackedFile.TotalSize = Math.Max(trackedFile.TotalSize, args.TotalBytesToReceive);
trackedFile.IsUnknownSize = trackedFile.TotalSize <= 0;
trackedFile.DownloadedBytes = Math.Max(trackedFile.DownloadedBytes, args.ReceivedBytesSize);
trackedFile.Speed = Math.Max(0L, (long)Math.Round(args.BytesPerSecondSpeed));
trackedFile.ActiveThreads = Math.Max(0, args.ActiveChunks);
}
downloader.DownloadStarted += (_, args) =>
{
if (trackedFile is null)
return;
trackedFile.State = PCL.Network.NetState.Reading;
trackedFile.TotalSize = Math.Max(trackedFile.TotalSize, args.TotalBytesToReceive);
trackedFile.IsUnknownSize = args.TotalBytesToReceive <= 0;
trackedFile.DownloadedBytes = 0;
trackedFile.Speed = 0;
trackedFile.ActiveThreads = 0;
};
downloader.DownloadProgressChanged += (_, args) => UpdateDownloadStat(args);
downloader.ChunkDownloadProgressChanged += (_, args) => UpdateDownloadStat(args);
downloader.DownloadFileCompleted += (_, args) =>
{
if (trackedFile is not null)
{
trackedFile.Speed = 0;
trackedFile.ActiveThreads = 0;
trackedFile.DownloadedBytes = Math.Max(trackedFile.DownloadedBytes, trackedFile.TotalSize);
}
if (args.Cancelled)
tcs.TrySetCanceled();
else if (args.Error != null)
tcs.TrySetException(args.Error);
else
tcs.TrySetResult(true);
};
try
{
await downloader.DownloadFileTaskAsync(url, localPath, cancellationToken).ConfigureAwait(false);
await tcs.Task.ConfigureAwait(false);
var tempPath = localPath + ModNet.netDownloadEnd;
if (!File.Exists(localPath) && File.Exists(tempPath))
{
for (var retry = 0; retry < 5; retry++)
{
try
{
File.Move(tempPath, localPath, true);
break;
}
catch (IOException)
{
Thread.Sleep(100);
}
}
}
if (!File.Exists(localPath))
throw new IOException($"下载未产生任何文件:{localPath}");
ModBase.Log($"[Download] 下载成功:{localPath}");
}
catch (TaskCanceledException ex) when (cancellationToken.IsCancellationRequested)
{
throw new OperationCanceledException(cancellationToken);
}
catch (TaskCanceledException ex)
{
throw new TimeoutException($"下载超时({url}", ex);
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception ex)
{
throw new IOException($"下载失败:{url}", ex);
}
}
private static void CleanupTempFiles(string localPath)
{
var tempPath = localPath + ModNet.netDownloadEnd;
TryDeleteFile(localPath);
TryDeleteFile(tempPath);
}
private static void TryDeleteFile(string path)
{
for (var retry = 0; retry < 5; retry++)
{
try
{
if (File.Exists(path))
File.Delete(path);
return;
}
catch (IOException)
{
Thread.Sleep(100);
}
}
}
private static HttpClient GetHttpClient(string url)
{
if (Uri.TryCreate(url, UriKind.Absolute, out var parsedUri)
&& parsedUri.Host is "edge.forgecdn.net" or "mediafilez.forgecdn.net" or "forgecdn.net" or "api.curseforge.com")
{
return NetworkService.GetClient(NetworkService.CurseForgeApi);
}
return NetworkService.GetClient();
}
}