初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器 HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块 Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚 AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
This commit is contained in:
+27
@@ -0,0 +1,27 @@
|
||||
using System.Net.Http;
|
||||
using Downloader;
|
||||
|
||||
namespace PCL.Network;
|
||||
|
||||
internal static class DownloadRequestFactory
|
||||
{
|
||||
internal static RequestConfiguration Create(string url, bool useBrowserUserAgent, string customUserAgent = "")
|
||||
{
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, url);
|
||||
RequestSigning.SecretHeadersSign(url, ref request, useBrowserUserAgent, customUserAgent);
|
||||
try
|
||||
{
|
||||
var configuration = new RequestConfiguration();
|
||||
if (request.Headers.UserAgent.Count > 0)
|
||||
configuration.UserAgent = request.Headers.UserAgent.ToString();
|
||||
foreach (var header in request.Headers)
|
||||
if (!header.Key.Equals("User-Agent", StringComparison.OrdinalIgnoreCase))
|
||||
configuration.Headers.Add($"{header.Key}: {string.Join(", ", header.Value)}");
|
||||
return configuration;
|
||||
}
|
||||
finally
|
||||
{
|
||||
request.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user