Files
xyou f70b061d1a
CI / Go Backend (push) Canceled after 0s
feat: 项目初始化 + 3D方块世界原型 + AI助搭系统
初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器

HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块

Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚

AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
2026-08-08 14:07:56 +08:00

224 lines
8.7 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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();
}
}