From eccae8e9f319664a27fc1ea42a3280b5cb257502 Mon Sep 17 00:00:00 2001 From: YangSpring114 Date: Sat, 16 Mar 2024 01:28:32 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E5=86=99=E8=B5=84=E6=BA=90=E8=A1=A5?= =?UTF-8?q?=E5=85=A8=E9=80=BB=E8=BE=91=E6=9E=81=E5=A4=A7=E6=8F=90=E9=AB=98?= =?UTF-8?q?=E4=BA=86=E4=B8=8B=E8=BD=BD=E9=80=9F=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MinecraftLaunch.Test/Program.cs | 79 ++- .../Models/Download/DownloadRequest.cs | 28 +- .../Classes/Models/Download/MultiPartRange.cs | 9 + .../Event/DownloadProgressChangedEventArgs.cs | 8 - .../Components/Downloader/BatchDownloader.cs | 453 +++++++++--------- .../Downloader/ResourceDownloader.cs | 86 ++++ .../Extensions/DownloadEntryExtension.cs | 3 - .../Extensions/DownloadExtension.cs | 75 ++- MinecraftLaunch/Extensions/StringExtension.cs | 2 - MinecraftLaunch/Utilities/DownloadUitl.cs | 187 ++++++++ 10 files changed, 608 insertions(+), 322 deletions(-) create mode 100644 MinecraftLaunch/Classes/Models/Download/MultiPartRange.cs create mode 100644 MinecraftLaunch/Components/Downloader/ResourceDownloader.cs create mode 100644 MinecraftLaunch/Utilities/DownloadUitl.cs diff --git a/MinecraftLaunch.Test/Program.cs b/MinecraftLaunch.Test/Program.cs index dd84eca..f4eba4f 100644 --- a/MinecraftLaunch.Test/Program.cs +++ b/MinecraftLaunch.Test/Program.cs @@ -6,46 +6,45 @@ using MinecraftLaunch.Components.Fetcher; using MinecraftLaunch.Components.Installer; using MinecraftLaunch.Extensions; - -MirrorDownloadManager.IsUseMirrorDownloadSource = true; - -foreach (var item in await ForgeInstaller.EnumerableFromVersionAsync("1.12.2")){ - Console.WriteLine(item.ForgeVersion); +using MinecraftLaunch.Components.Checker; +using MinecraftLaunch.Utilities; + +try { + MirrorDownloadManager.IsUseMirrorDownloadSource = true; + + var account = new OfflineAuthenticator("Yang114").Authenticate(); + var resolver = new GameResolver(".minecraft"); + var id = "1.12.2"; + + VanlliaInstaller installer = new(resolver, id, MirrorDownloadManager.Bmcl); + installer.ProgressChanged += (sender, args) => { + Console.WriteLine($"{args.Progress * 100:0.00}% --- {args.ProgressStatus} --- {args.Status}"); + }; + + await installer.InstallAsync(); + + var config = new LaunchConfig { + Account = account, + IsEnableIndependencyCore = true, + JvmConfig = new(JavaUtil.GetCurrentJava(new JavaFetcher().Fetch(), resolver.GetGameEntity(id)).JavaPath) { + MaxMemory = 1024, + } + }; + + Launcher launcher = new(resolver, config); + var gameProcessWatcher = await launcher.LaunchAsync(id); + + //获取输出日志 + gameProcessWatcher.OutputLogReceived += (sender, args) => { + Console.WriteLine(args.Text); + }; + + //检测游戏退出 + gameProcessWatcher.Exited += (sender, args) => { + Console.WriteLine("exit"); + }; +} catch (Exception ex) { + Console.WriteLine(ex.ToString()); } -var account = new OfflineAuthenticator("Yang114").Authenticate(); -var resolver = new GameResolver("C:\\Users\\w\\Desktop\\总整包\\MC\\mc启动器\\LauncherX\\.minecraft"); - -var installer = new VanlliaInstaller(resolver, "1.19.4", MirrorDownloadManager.Bmcl); -installer.ProgressChanged += (_, args) => { - Console.WriteLine($"{args.ProgressStatus} - {args.Progress * 100:0.00}%"); -}; - -installer.Completed += (_, _) => { - Console.WriteLine("Completed!"); -}; - -await installer.InstallAsync(); - -var config = new LaunchConfig { - Account = account, - IsEnableIndependencyCore = true, - JvmConfig = new(@"C:\Program Files\Java\jdk1.8.0_301\bin\javaw.exe") { - MaxMemory = 1024, - } -}; - -Launcher launcher = new(resolver, config); -var gameProcessWatcher = await launcher.LaunchAsync("1.12.2"); - -//获取输出日志 -gameProcessWatcher.OutputLogReceived += (sender, args) => { - Console.WriteLine(args.Text); -}; - -//检测游戏退出 -gameProcessWatcher.Exited += (sender, args) => { - Console.WriteLine("exit"); -}; - Console.ReadKey(); \ No newline at end of file diff --git a/MinecraftLaunch/Classes/Models/Download/DownloadRequest.cs b/MinecraftLaunch/Classes/Models/Download/DownloadRequest.cs index dd6b559..10b8e1a 100644 --- a/MinecraftLaunch/Classes/Models/Download/DownloadRequest.cs +++ b/MinecraftLaunch/Classes/Models/Download/DownloadRequest.cs @@ -4,15 +4,33 @@ /// 下载请求信息配置记录类 /// public sealed record DownloadRequest { - public int Size { get; set; } + /// + /// 大文件判断阈值 + /// + public long FileSizeThreshold { get; set; } - public bool IsCompleted { get; set; } + /// + /// 分片数量 + /// + public int MultiPartsCount { get; set; } - public int DownloadedBytes { get; set; } + /// + /// 最大并行下载线程数 + /// + public int MultiThreadsCount { get; set; } - public required string Url { get; init; } + /// + /// 下载链接 + /// + public string Url { get; init; } - public required FileInfo FileInfo { get; set; } + /// + /// 保存文件信息 + /// + public FileInfo FileInfo { get; set; } + /// + /// 是否启用大文件分片下载 + /// public bool IsPartialContentSupported { get; set; } } \ No newline at end of file diff --git a/MinecraftLaunch/Classes/Models/Download/MultiPartRange.cs b/MinecraftLaunch/Classes/Models/Download/MultiPartRange.cs new file mode 100644 index 0000000..efa9a06 --- /dev/null +++ b/MinecraftLaunch/Classes/Models/Download/MultiPartRange.cs @@ -0,0 +1,9 @@ +namespace MinecraftLaunch.Classes.Models.Download; + +public record MultiPartRange { + public long End { get; set; } + + public long Start { get; set; } + + public string TempFilePath { get; set; } +} diff --git a/MinecraftLaunch/Classes/Models/Event/DownloadProgressChangedEventArgs.cs b/MinecraftLaunch/Classes/Models/Event/DownloadProgressChangedEventArgs.cs index 0098202..26e6142 100644 --- a/MinecraftLaunch/Classes/Models/Event/DownloadProgressChangedEventArgs.cs +++ b/MinecraftLaunch/Classes/Models/Event/DownloadProgressChangedEventArgs.cs @@ -1,15 +1,7 @@ namespace MinecraftLaunch.Classes.Models.Event; public sealed class DownloadProgressChangedEventArgs : EventArgs { - public double Speed { get; set; } - public int TotalCount { get; set; } - public double TotalBytes { get; set; } - - public int FailedCount { get; set; } - public int CompletedCount { get; set; } - - public double DownloadedBytes { get; set; } } \ No newline at end of file diff --git a/MinecraftLaunch/Components/Downloader/BatchDownloader.cs b/MinecraftLaunch/Components/Downloader/BatchDownloader.cs index 145c80a..894229f 100644 --- a/MinecraftLaunch/Components/Downloader/BatchDownloader.cs +++ b/MinecraftLaunch/Components/Downloader/BatchDownloader.cs @@ -10,7 +10,8 @@ namespace MinecraftLaunch.Components.Downloader; -public sealed class BatchDownloader : IDownloader, IDisposable { +[Obsolete] +public sealed class BatchDownloader { private const int MAX_RETRY_COUNT = 3; private const int BUFFER_SIZE = 4096; // byte private const int SIZE_THRESHOLD = 1048576; @@ -40,230 +41,230 @@ public sealed class BatchDownloader : IDownloader, IDisposable { public event EventHandler ProgressChanged; - public BatchDownloader(int chunkCount = 8, bool useChunkedDownload = true) { - _client = new HttpClient(); - _client.DefaultRequestHeaders.Connection.Add("keep-alive"); - - _chunkCount = chunkCount; - _bufferPool = ArrayPool.Create(BUFFER_SIZE, Environment.ProcessorCount * 2); - _autoResetEvent = new AutoResetEvent(true); - - _parallelOptions = new ExecutionDataflowBlockOptions { - MaxDegreeOfParallelism = Environment.ProcessorCount * 2, - }; - - _userCts = new CancellationTokenSource(); - _parallelOptions.CancellationToken = _userCts.Token; - - _timer = new Timer { - Interval = TimeSpan.FromSeconds(UPDATE_INTERVAL).TotalMilliseconds - }; - - _timer.Elapsed += (sender, e) => UpdateDownloadProgress(); - } - - public void Setup(IEnumerable downloadItems) { - // Initialize states - _downloadItems = downloadItems.ToImmutableList(); - _totalBytes = _downloadItems.Sum(item => item.Size); - _downloadedBytes = 0; - _previousDownloadedBytes = 0; - - _totalCount = _downloadItems.Count(); - _completedCount = 0; - _failedCount = 0; - - if (_userCts.IsCancellationRequested) { - _userCts.Dispose(); - _userCts = new CancellationTokenSource(); - _parallelOptions.CancellationToken = _userCts.Token; - } - - _autoResetEvent.Reset(); - } - - public void Retry() { - _downloadItems = _downloadItems.Where(item => !item.IsCompleted).ToImmutableList(); - _failedCount = 0; - _autoResetEvent.Set(); - } - - public void Cancel() { - _userCts.Cancel(); - _timer.Stop(); - _autoResetEvent.Set(); - } - - public async ValueTask DownloadAsync() { - while (true) { - _timer.Start(); - - try { - var downloader = new ActionBlock(async item => - { - for (int i = 0; i < MAX_RETRY_COUNT && !_userCts.IsCancellationRequested; i++) { - if (await DownloadItemAsync(item, i)) { - break; - } - } - }, _parallelOptions); - - foreach (var item in _downloadItems) { - downloader.Post(item); - } - - downloader.Complete(); - await downloader.Completion; - } - catch (OperationCanceledException) { - //_logService.Info(nameof(DownloadService), "Download canceled"); - } - - _timer.Stop(); - // Ensure the last progress report is fired - UpdateDownloadProgress(); - - // Succeeded - if (_completedCount == _totalCount) { - Completed?.Invoke(true); - return true; - } - - - // Clean incomplete files - foreach (var item in _downloadItems) { - if (!item.IsCompleted && item.FileInfo.Exists) { - item.FileInfo.Delete(); - } - } - - if (_failedCount > 0 && !_userCts.IsCancellationRequested) { - Completed?.Invoke(false); - } - - // Wait for retry or cancel - _autoResetEvent.WaitOne(); - - // Canceled - if (_userCts.IsCancellationRequested) { - Completed?.Invoke(false); - return false; - } - } - } - - private void UpdateDownloadProgress() { - int diffBytes = _downloadedBytes - _previousDownloadedBytes; - _previousDownloadedBytes = _downloadedBytes; - - var progress = new DownloadProgressChangedEventArgs { - TotalCount = _totalCount, - CompletedCount = _completedCount, - FailedCount = _failedCount, - TotalBytes = _totalBytes, - DownloadedBytes = _downloadedBytes, - Speed = diffBytes / UPDATE_INTERVAL, - }; - - ProgressChanged?.Invoke(this, progress); - } - - private async ValueTask DownloadItemAsync(DownloadRequest item, int retryTimes) { - if (_userCts.IsCancellationRequested) { - return true; - } - - // Make sure directory exists - if (!item.FileInfo.Directory.Exists) { - item.FileInfo.Directory.Create(); - } - - if (!item.FileInfo.Exists) { - using var r = item.FileInfo.Create(); - } - - byte[] buffer = _bufferPool.Rent(BUFFER_SIZE); - - try { - var request = new HttpRequestMessage(HttpMethod.Get, item.Url); - var response = - await _client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, _userCts.Token); - - if (response.StatusCode == HttpStatusCode.Found) { - // Handle redirection - request = new HttpRequestMessage(HttpMethod.Get, response.Headers.Location); - response = await _client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, - _userCts.Token); - } - - if (item.Size == 0) { - item.Size = (int)(response.Content.Headers.ContentLength ?? 0); - Interlocked.Add(ref _totalBytes, item.Size); - } - - item.IsPartialContentSupported = response.Headers.AcceptRanges.Contains("bytes"); - - // Calculate the size of each chunk - int chunkSize = (int)Math.Ceiling((double)item.Size / _chunkCount); - - // Decide whether to use chunked download based on the size threshold - bool useChunkedDownload = item.Size > SIZE_THRESHOLD && item.IsPartialContentSupported; - - for (int i = 0; i < (useChunkedDownload ? _chunkCount : 1); i++) { - int chunkStart = i * chunkSize; - int chunkEnd = useChunkedDownload ? Math.Min(chunkStart + chunkSize, item.Size) - 1 : item.Size - 1; - - request = new HttpRequestMessage(HttpMethod.Get, item.Url); - request.Headers.Range = new RangeHeaderValue(chunkStart, chunkEnd); - - response = await _client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, _userCts.Token); - - await using var httpStream = await response.Content.ReadAsStreamAsync(); - await using var fileStream = new FileStream(item.FileInfo.FullName, FileMode.Open, FileAccess.Write, FileShare.Write); - fileStream.Position = chunkStart; - - int bytesRead; - while ((bytesRead = await httpStream.ReadAsync(buffer, _userCts.Token)) > 0) { - await fileStream.WriteAsync(buffer, 0, bytesRead, _userCts.Token); - item.DownloadedBytes += bytesRead; - Interlocked.Add(ref _downloadedBytes, bytesRead); - } - } - - // Download successful - item.IsCompleted = true; - Interlocked.Increment(ref _completedCount); - - request.Dispose(); - response.Dispose(); - return true; - } - catch (OperationCanceledException) { - if (!_userCts.IsCancellationRequested) { - throw; - } - } - catch (Exception) { - throw; - } - finally { - _bufferPool.Return(buffer); - } - - if (!_userCts.IsCancellationRequested) { - Interlocked.Increment(ref _failedCount); - Interlocked.Add(ref _downloadedBytes, -item.DownloadedBytes); - item.DownloadedBytes = 0; - Interlocked.Exchange(ref _previousDownloadedBytes, _downloadedBytes); - } - - return false; - } - - void IDisposable.Dispose() { - _client.Dispose(); - _userCts.Dispose(); - _autoResetEvent.Dispose(); - } + //public BatchDownloader(int chunkCount = 8, bool useChunkedDownload = true) { + // _client = new HttpClient(); + // _client.DefaultRequestHeaders.Connection.Add("keep-alive"); + + // _chunkCount = chunkCount; + // _bufferPool = ArrayPool.Create(BUFFER_SIZE, Environment.ProcessorCount * 2); + // _autoResetEvent = new AutoResetEvent(true); + + // _parallelOptions = new ExecutionDataflowBlockOptions { + // MaxDegreeOfParallelism = Environment.ProcessorCount * 2, + // }; + + // _userCts = new CancellationTokenSource(); + // _parallelOptions.CancellationToken = _userCts.Token; + + // _timer = new Timer { + // Interval = TimeSpan.FromSeconds(UPDATE_INTERVAL).TotalMilliseconds + // }; + + // _timer.Elapsed += (sender, e) => UpdateDownloadProgress(); + //} + + //public void Setup(IEnumerable downloadItems) { + // // Initialize states + // _downloadItems = downloadItems.ToImmutableList(); + // _totalBytes = _downloadItems.Sum(item => item.Size); + // _downloadedBytes = 0; + // _previousDownloadedBytes = 0; + + // _totalCount = _downloadItems.Count(); + // _completedCount = 0; + // _failedCount = 0; + + // if (_userCts.IsCancellationRequested) { + // _userCts.Dispose(); + // _userCts = new CancellationTokenSource(); + // _parallelOptions.CancellationToken = _userCts.Token; + // } + + // _autoResetEvent.Reset(); + //} + + //public void Retry() { + // _downloadItems = _downloadItems.Where(item => !item.IsCompleted).ToImmutableList(); + // _failedCount = 0; + // _autoResetEvent.Set(); + //} + + //public void Cancel() { + // _userCts.Cancel(); + // _timer.Stop(); + // _autoResetEvent.Set(); + //} + + //public async ValueTask DownloadAsync() { + // while (true) { + // _timer.Start(); + + // try { + // var downloader = new ActionBlock(async item => + // { + // for (int i = 0; i < MAX_RETRY_COUNT && !_userCts.IsCancellationRequested; i++) { + // if (await DownloadItemAsync(item, i)) { + // break; + // } + // } + // }, _parallelOptions); + + // foreach (var item in _downloadItems) { + // downloader.Post(item); + // } + + // downloader.Complete(); + // await downloader.Completion; + // } + // catch (OperationCanceledException) { + // //_logService.Info(nameof(DownloadService), "Download canceled"); + // } + + // _timer.Stop(); + // // Ensure the last progress report is fired + // UpdateDownloadProgress(); + + // // Succeeded + // if (_completedCount == _totalCount) { + // Completed?.Invoke(true); + // return true; + // } + + + // // Clean incomplete files + // foreach (var item in _downloadItems) { + // if (!item.IsCompleted && item.FileInfo.Exists) { + // item.FileInfo.Delete(); + // } + // } + + // if (_failedCount > 0 && !_userCts.IsCancellationRequested) { + // Completed?.Invoke(false); + // } + + // // Wait for retry or cancel + // _autoResetEvent.WaitOne(); + + // // Canceled + // if (_userCts.IsCancellationRequested) { + // Completed?.Invoke(false); + // return false; + // } + // } + //} + + //private void UpdateDownloadProgress() { + // //int diffBytes = _downloadedBytes - _previousDownloadedBytes; + // //_previousDownloadedBytes = _downloadedBytes; + + // //var progress = new DownloadProgressChangedEventArgs { + // // TotalCount = _totalCount, + // // CompletedCount = _completedCount, + // // FailedCount = _failedCount, + // // TotalBytes = _totalBytes, + // // DownloadedBytes = _downloadedBytes, + // // Speed = diffBytes / UPDATE_INTERVAL, + // //}; + + // //ProgressChanged?.Invoke(this, progress); + //} + + //private async ValueTask DownloadItemAsync(DownloadRequest item, int retryTimes) { + // if (_userCts.IsCancellationRequested) { + // return true; + // } + + // // Make sure directory exists + // if (!item.FileInfo.Directory.Exists) { + // item.FileInfo.Directory.Create(); + // } + + // if (!item.FileInfo.Exists) { + // using var r = item.FileInfo.Create(); + // } + + // byte[] buffer = _bufferPool.Rent(BUFFER_SIZE); + + // try { + // var request = new HttpRequestMessage(HttpMethod.Get, item.Url); + // var response = + // await _client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, _userCts.Token); + + // if (response.StatusCode == HttpStatusCode.Found) { + // // Handle redirection + // request = new HttpRequestMessage(HttpMethod.Get, response.Headers.Location); + // response = await _client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, + // _userCts.Token); + // } + + // if (item.Size == 0) { + // item.Size = (int)(response.Content.Headers.ContentLength ?? 0); + // Interlocked.Add(ref _totalBytes, item.Size); + // } + + // item.IsPartialContentSupported = response.Headers.AcceptRanges.Contains("bytes"); + + // // Calculate the size of each chunk + // int chunkSize = (int)Math.Ceiling((double)item.Size / _chunkCount); + + // // Decide whether to use chunked download based on the size threshold + // bool useChunkedDownload = item.Size > SIZE_THRESHOLD && item.IsPartialContentSupported; + + // for (int i = 0; i < (useChunkedDownload ? _chunkCount : 1); i++) { + // int chunkStart = i * chunkSize; + // int chunkEnd = useChunkedDownload ? Math.Min(chunkStart + chunkSize, item.Size) - 1 : item.Size - 1; + + // request = new HttpRequestMessage(HttpMethod.Get, item.Url); + // request.Headers.Range = new RangeHeaderValue(chunkStart, chunkEnd); + + // response = await _client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, _userCts.Token); + + // await using var httpStream = await response.Content.ReadAsStreamAsync(); + // await using var fileStream = new FileStream(item.FileInfo.FullName, FileMode.Open, FileAccess.Write, FileShare.Write); + // fileStream.Position = chunkStart; + + // int bytesRead; + // while ((bytesRead = await httpStream.ReadAsync(buffer, _userCts.Token)) > 0) { + // await fileStream.WriteAsync(buffer, 0, bytesRead, _userCts.Token); + // item.DownloadedBytes += bytesRead; + // Interlocked.Add(ref _downloadedBytes, bytesRead); + // } + // } + + // // Download successful + // item.IsCompleted = true; + // Interlocked.Increment(ref _completedCount); + + // request.Dispose(); + // response.Dispose(); + // return true; + // } + // catch (OperationCanceledException) { + // if (!_userCts.IsCancellationRequested) { + // throw; + // } + // } + // catch (Exception) { + // throw; + // } + // finally { + // _bufferPool.Return(buffer); + // } + + // if (!_userCts.IsCancellationRequested) { + // Interlocked.Increment(ref _failedCount); + // Interlocked.Add(ref _downloadedBytes, -item.DownloadedBytes); + // item.DownloadedBytes = 0; + // Interlocked.Exchange(ref _previousDownloadedBytes, _downloadedBytes); + // } + + // return false; + //} + + //void IDisposable.Dispose() { + // _client.Dispose(); + // _userCts.Dispose(); + // _autoResetEvent.Dispose(); + //} } diff --git a/MinecraftLaunch/Components/Downloader/ResourceDownloader.cs b/MinecraftLaunch/Components/Downloader/ResourceDownloader.cs new file mode 100644 index 0000000..e56ab2e --- /dev/null +++ b/MinecraftLaunch/Components/Downloader/ResourceDownloader.cs @@ -0,0 +1,86 @@ +using MinecraftLaunch.Classes.Interfaces; +using MinecraftLaunch.Classes.Models.Download; +using MinecraftLaunch.Classes.Models.Event; +using MinecraftLaunch.Extensions; +using MinecraftLaunch.Utilities; +using System.Diagnostics; +using System.Threading.Tasks.Dataflow; + +namespace MinecraftLaunch.Components.Downloader; + +public class ResourceDownloader(IEnumerable downloadEntries, + DownloadRequest request, + MirrorDownloadSource downloadSource = default, + CancellationTokenSource tokenSource = default) : IDownloader { + public event EventHandler ProgressChanged; + + public async ValueTask DownloadAsync() { + int completedCount = 0; + int totalCount = downloadEntries.Count(); + + var transformBlock = new TransformBlock(e => { + if (string.IsNullOrEmpty(e.Url)) { + return e; + } + + if (MirrorDownloadManager.IsUseMirrorDownloadSource) { + downloadEntries.Select(x => x.OfMirrorSource(default)); + } + + return e; + }, new ExecutionDataflowBlockOptions { + BoundedCapacity = request.MultiThreadsCount, + MaxDegreeOfParallelism = request.MultiThreadsCount, + CancellationToken = tokenSource is null ? default : tokenSource.Token + }); + + var actionBlock = new ActionBlock(async e => { + if (string.IsNullOrEmpty(e.Url)) { + return; + } + + await DownloadUitl.DownloadAsync(e, tokenSource: tokenSource).AsTask().ContinueWith(task => { + if (task.IsFaulted) { + if (!e.Verify()) { + Debug.WriteLine(task.Exception.Message); + return; + } + + return; + } + + var downloadResult = task.Result; + }); + + Interlocked.Increment(ref completedCount); + ProgressChanged?.Invoke(this, new DownloadProgressChangedEventArgs { + TotalCount = totalCount, + CompletedCount = completedCount + }); + }, + new ExecutionDataflowBlockOptions { + BoundedCapacity = request.MultiThreadsCount, + MaxDegreeOfParallelism = request.MultiThreadsCount, + CancellationToken = tokenSource is null ? default : tokenSource.Token + }); + + var transformManyBlock = new TransformManyBlock, IDownloadEntry>(chunk => chunk, + new ExecutionDataflowBlockOptions()); + + var linkOptions = new DataflowLinkOptions { PropagateCompletion = true }; + + transformManyBlock.LinkTo(transformBlock, linkOptions); + transformBlock.LinkTo(actionBlock, linkOptions); + + if (downloadEntries != null) transformManyBlock.Post(downloadEntries); + + transformManyBlock.Complete(); + + //DownloadElementsPosted?.Invoke(this, filteredLibraries.Count + filteredAssets.Count); + await actionBlock.Completion.WaitAsync(tokenSource is null ? default : tokenSource.Token); + return true; + + //foreach (var downloadResult in _errorDownload.Where(x => !x.DownloadElement.VerifyFile()).ToList()) + // _errorDownload.Remove(downloadResult); + } +} \ No newline at end of file diff --git a/MinecraftLaunch/Extensions/DownloadEntryExtension.cs b/MinecraftLaunch/Extensions/DownloadEntryExtension.cs index a90fb82..5c89d7d 100644 --- a/MinecraftLaunch/Extensions/DownloadEntryExtension.cs +++ b/MinecraftLaunch/Extensions/DownloadEntryExtension.cs @@ -11,9 +11,6 @@ public static class DownloadEntryExtension { public static DownloadRequest ToDownloadRequest(this IDownloadEntry entry) { return new DownloadRequest { Url = entry.Url, - Size = entry.Size, - IsCompleted = false, - DownloadedBytes = 0, FileInfo = entry.Path.ToFileInfo() }; } diff --git a/MinecraftLaunch/Extensions/DownloadExtension.cs b/MinecraftLaunch/Extensions/DownloadExtension.cs index 0a863cf..f582dca 100644 --- a/MinecraftLaunch/Extensions/DownloadExtension.cs +++ b/MinecraftLaunch/Extensions/DownloadExtension.cs @@ -4,6 +4,7 @@ using MinecraftLaunch.Classes.Models.Download; using MinecraftLaunch.Components.Downloader; using MinecraftLaunch.Classes.Models.Game; +using MinecraftLaunch.Utilities; namespace MinecraftLaunch.Extensions; @@ -31,66 +32,64 @@ public static IDownloadEntry OfMirrorSource(this IDownloadEntry entry, return entry; } - public static ValueTask DownloadAsync(this - DownloadRequest request, + public static ValueTask DownloadAsync(this DownloadRequest request, Action action = default!) { - DefaultDownloader.Setup(Enumerable.Repeat(request, 1)); + //DefaultDownloader.Setup(Enumerable.Repeat(request, 1)); - DefaultDownloader.ProgressChanged += (sender, args) => { - action(args); - }; + //DefaultDownloader.ProgressChanged += (sender, args) => { + // action(args); + //}; - return DefaultDownloader.DownloadAsync(); + //return DefaultDownloader.DownloadAsync(); + throw new NotImplementedException(); } public static ValueTask DownloadResourceEntryAsync(this - IDownloadEntry downloadEntry, + IDownloadEntry downloadEntry, MirrorDownloadSource source = default!) { - DefaultDownloader.Setup(Enumerable.Repeat(downloadEntry - .OfMirrorSource(source) - .ToDownloadRequest(), 1)); + return DownloadUitl.DownloadAsync(downloadEntry, DownloadUitl.DefaultDownloadRequest,default,x=>{}); - Console.WriteLine(downloadEntry.Path); - return DefaultDownloader.DownloadAsync(); + //DefaultDownloader.Setup(Enumerable.Repeat(downloadEntry + // .OfMirrorSource(source) + // .ToDownloadRequest(), 1)); + + //Console.WriteLine(downloadEntry.Path); + //return DefaultDownloader.DownloadAsync(); + //throw new NotImplementedException(); } public static ValueTask DownloadResourceEntrysAsync(this - IEnumerable entries, + IEnumerable entries, MirrorDownloadSource source = default!, - Action action = default!) { - DefaultDownloader.Setup(entries - .Select(x => x.OfMirrorSource(source)) - .Select(x => x.ToDownloadRequest())); + Action action = default!, + DownloadRequest downloadRequest = default!) { + downloadRequest ??= DownloadUitl.DefaultDownloadRequest; - DefaultDownloader.ProgressChanged += (sender, args) => { + if (MirrorDownloadManager.IsUseMirrorDownloadSource && source is not null) { + entries.Select(x => { + if (x.Type is DownloadEntryType.Jar) { + x.Url = $"{source.Host}/version/{(x as JarEntry).McVersion}/client"; + } else { + x.OfMirrorSource(source); + } + + return x; + }); + } + + ResourceDownloader downloader = new(entries, downloadRequest, source); + downloader.ProgressChanged += (sender, args) => { action(args); }; - return DefaultDownloader.DownloadAsync(); + return downloader.DownloadAsync(); } public static double ToPercentage(this DownloadProgressChangedEventArgs args) { - return (double)args.DownloadedBytes / (double)args.TotalBytes; + return (double)args.CompletedCount / (double)args.TotalCount; } public static double ToPercentage(this double progress, double mini, double max) { return mini + (max - mini) * progress; } - - public static string ToSpeedText(this DownloadProgressChangedEventArgs args) { - double speed = args.Speed; - if (speed < 1024.0) { - return speed.ToString("0") + " B/s"; - } - - if (speed < 1024.0 * 1024.0) { - return (speed / 1024.0).ToString("0.0") + " KB/s"; - } - - if (speed < 1024.0 * 1024.0 * 1024.0) { - return (speed / (1024.0 * 1024.0)).ToString("0.00") + " MB/s"; - } - - return "0"; - } } \ No newline at end of file diff --git a/MinecraftLaunch/Extensions/StringExtension.cs b/MinecraftLaunch/Extensions/StringExtension.cs index b51375c..b739693 100644 --- a/MinecraftLaunch/Extensions/StringExtension.cs +++ b/MinecraftLaunch/Extensions/StringExtension.cs @@ -30,8 +30,6 @@ public static DownloadRequest ToDownloadRequest(this string url, FileInfo path) return new DownloadRequest { Url = url, FileInfo = path, - IsCompleted = false, - DownloadedBytes = 0, }; } } \ No newline at end of file diff --git a/MinecraftLaunch/Utilities/DownloadUitl.cs b/MinecraftLaunch/Utilities/DownloadUitl.cs new file mode 100644 index 0000000..703b3e0 --- /dev/null +++ b/MinecraftLaunch/Utilities/DownloadUitl.cs @@ -0,0 +1,187 @@ +using Flurl.Http; +using System.Net; +using MinecraftLaunch.Classes.Interfaces; +using MinecraftLaunch.Classes.Models.Download; +using Timer = System.Timers.Timer; +using System.Net.Http.Headers; +using System.Threading.Tasks.Dataflow; +using System.Buffers; + +namespace MinecraftLaunch.Utilities; + +/// +/// 下载工具类 +/// +public static class DownloadUitl { + public static DownloadRequest DefaultDownloadRequest { get; set; } = new() { + IsPartialContentSupported = true, + FileSizeThreshold = 1024 * 1024 * 3, + MultiThreadsCount = 64, + MultiPartsCount = 8 + }; + + public static async ValueTask DownloadAsync( + IDownloadEntry downloadEntry, + DownloadRequest downloadRequest = default, + CancellationTokenSource tokenSource = default, + Action perSecondProgressChangedAction = default) { + + Timer timer = default; + downloadRequest ??= DefaultDownloadRequest; + tokenSource ??= new CancellationTokenSource(); + perSecondProgressChangedAction ??= x => { }; + var responseMessage = (await downloadEntry.Url.GetAsync(cancellationToken: tokenSource.Token)) + .ResponseMessage; + + if (responseMessage.StatusCode.Equals(HttpStatusCode.Found)) { + downloadEntry.Url = responseMessage.Headers.Location.AbsoluteUri; + return await DownloadAsync(downloadEntry, downloadRequest, tokenSource); + } + + if (perSecondProgressChangedAction != null) { + timer = new Timer(1000); + } + + responseMessage.EnsureSuccessStatusCode(); + var contentLength = responseMessage.Content.Headers.ContentLength ?? 0; + + //use multipart download method + if (downloadRequest.IsPartialContentSupported && contentLength > downloadRequest.FileSizeThreshold) { + var requestMessage = new HttpRequestMessage(HttpMethod.Get, responseMessage.RequestMessage.RequestUri.AbsoluteUri); + requestMessage.Headers.Range = new RangeHeaderValue(0, 1); + + return await MultiPartDownloadAsync(responseMessage, downloadRequest, downloadEntry.Path, tokenSource) + .AsTask() + .ContinueWith(task => { + return !task.IsFaulted; + }); + } + + return await WriteFileFromHttpResponseAsync(downloadEntry.Path, responseMessage, tokenSource, (timer, perSecondProgressChangedAction, contentLength)) + .AsTask() + .ContinueWith(task => { + if (timer != null) { + try { + perSecondProgressChangedAction(responseMessage.Content.Headers.ContentLength != null ? + task.Result / (double)responseMessage.Content.Headers.ContentLength : 0); + } catch (Exception) { + + } + + timer.Stop(); + timer.Dispose(); + } + + return !task.IsFaulted; + }); + } + + private async static ValueTask WriteFileFromHttpResponseAsync( + string path, + HttpResponseMessage responseMessage, + CancellationTokenSource tokenSource, + (Timer, Action perSecondProgressChangedAction, long?)? perSecondProgressChange = default) { + var parentFolder = Path.GetDirectoryName(path); + Directory.CreateDirectory(parentFolder); + + using var stream = await responseMessage.Content.ReadAsStreamAsync(); + using var fileStream = File.Create(path); + using var rentMemory = MemoryPool.Shared.Rent(1024); + + long totalReadMemory = 0; + int readMemory = 0; + + if (perSecondProgressChange != null) { + var (timer, action, length) = perSecondProgressChange.Value; + timer.Elapsed += (sender, e) => action(length != null ? (double)totalReadMemory / length.Value : 0); + timer.Start(); + } + + while ((readMemory = await stream.ReadAsync(rentMemory.Memory, tokenSource.Token)) > 0) { + await fileStream.WriteAsync(rentMemory.Memory[..readMemory], tokenSource.Token); + Interlocked.Add(ref totalReadMemory, readMemory); + } + + return totalReadMemory; + } + + + private static async ValueTask MultiPartDownloadAsync( + HttpResponseMessage responseMessage, + DownloadRequest downloadSetting, + string absolutePath, + CancellationTokenSource tokenSource) { + var requestMessage = new HttpRequestMessage(HttpMethod.Get, responseMessage.RequestMessage.RequestUri.AbsoluteUri); + requestMessage.Headers.Range = new RangeHeaderValue(0, 1); + + var httpResponse = (await responseMessage.RequestMessage.RequestUri.GetAsync(cancellationToken:tokenSource.Token)).ResponseMessage; + + if (!httpResponse.IsSuccessStatusCode || httpResponse.Content.Headers.ContentLength.Value != 2) + return await WriteFileFromHttpResponseAsync(absolutePath, responseMessage, tokenSource); + + var totalSize = responseMessage.Content.Headers.ContentLength.Value; + var singleSize = totalSize / downloadSetting.MultiPartsCount; + + var rangesList = new List(); + var folder = Path.GetDirectoryName(absolutePath); + + while (totalSize > 0) { + bool enough = totalSize - singleSize > 1024 * 10; + + var range = new MultiPartRange { + End = totalSize, + Start = enough ? totalSize - singleSize : 0 + }; + + range.TempFilePath = Path.Combine(folder, $"{range.Start}-{range.End}-" + Path.GetFileName(absolutePath)); + rangesList.Add(range); + + if (!enough) break; + + totalSize -= singleSize; + } + + var transformBlock = new TransformBlock(async range => { + var message = (await responseMessage.RequestMessage.RequestUri.WithHeader("Range", $"bytes={range.Start}-{range.End}") + .GetAsync(cancellationToken:tokenSource.Token)).ResponseMessage; + return (message, range); + }, new ExecutionDataflowBlockOptions { + BoundedCapacity = downloadSetting.MultiPartsCount, + MaxDegreeOfParallelism = downloadSetting.MultiPartsCount, + CancellationToken = tokenSource.Token + }); + + var actionBlock = new ActionBlock<(HttpResponseMessage, MultiPartRange)> + (async t => await WriteFileFromHttpResponseAsync(t.Item2.TempFilePath, t.Item1, tokenSource), + new ExecutionDataflowBlockOptions { + BoundedCapacity = downloadSetting.MultiPartsCount, + MaxDegreeOfParallelism = downloadSetting.MultiPartsCount, + CancellationToken = tokenSource.Token + }); + + var linkOptions = new DataflowLinkOptions { PropagateCompletion = true }; + var transformManyBlock = new TransformManyBlock, MultiPartRange>(chunk => chunk, + new ExecutionDataflowBlockOptions()); + + transformManyBlock.LinkTo(transformBlock, linkOptions); + transformBlock.LinkTo(actionBlock, linkOptions); + + transformManyBlock.Post(rangesList); + transformManyBlock.Complete(); + + await actionBlock.Completion; + + await using (var outputStream = File.Create(absolutePath)) { + foreach (var inputFile in rangesList) { + await using (var inputStream = File.OpenRead(inputFile.TempFilePath)) { + outputStream.Seek(inputFile.Start, SeekOrigin.Begin); + await inputStream.CopyToAsync(outputStream, tokenSource.Token); + } + + File.Delete(inputFile.TempFilePath); + } + } + + return new FileInfo(absolutePath).Length; + } +} \ No newline at end of file