using System.Diagnostics; using System.Net; using System.Net.Http.Json; using System.Security.Cryptography; namespace XFEToolBox.Core.Tools; /// /// Small client shared by the WPF host and other consumers of the tool catalog. /// public sealed class ToolCatalogClient(HttpClient httpClient) { private readonly HttpClient _httpClient = httpClient; public async Task> GetToolsAsync( string? search = null, string? category = null, CancellationToken cancellationToken = default) { using var response = await _httpClient.PostAsJsonAsync( "api/v1/tools/list", new { search, category }, cancellationToken); response.EnsureSuccessStatusCode(); return await response.Content.ReadFromJsonAsync(cancellationToken) ?? []; } public async Task GetToolAsync( string toolId, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrWhiteSpace(toolId); using var response = await _httpClient.PostAsJsonAsync( "api/v1/tools/get", new { toolId }, cancellationToken); if (response.StatusCode == HttpStatusCode.NotFound) return null; response.EnsureSuccessStatusCode(); return await response.Content.ReadFromJsonAsync(cancellationToken); } /// /// Downloads a package and verifies it against the catalog SHA-256 before returning. /// public Task DownloadPackageAsync( ToolPackageVersionInfo package, Stream destination, CancellationToken cancellationToken = default) => DownloadPackageAsync(package, destination, progress: null, cancellationToken); /// /// Downloads a package, reports streamed byte progress, and verifies the catalog SHA-256. /// public async Task DownloadPackageAsync( ToolPackageVersionInfo package, Stream destination, IProgress? progress, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(package); ArgumentNullException.ThrowIfNull(destination); using var request = new HttpRequestMessage(HttpMethod.Post, package.DownloadUrl) { Content = JsonContent.Create(new { toolId = package.ToolId, version = package.Version }) }; using var response = await _httpClient.SendAsync( request, HttpCompletionOption.ResponseHeadersRead, cancellationToken); response.EnsureSuccessStatusCode(); await using var source = await response.Content.ReadAsStreamAsync(cancellationToken); var totalBytes = response.Content.Headers.ContentLength is > 0 ? response.Content.Headers.ContentLength : package.PackageSize > 0 ? package.PackageSize : null; progress?.Report(new ToolPackageDownloadProgress(0, totalBytes)); using var sha256 = IncrementalHash.CreateHash(HashAlgorithmName.SHA256); var buffer = new byte[81920]; var received = 0L; var reportTimer = Stopwatch.StartNew(); var lastReportAt = TimeSpan.Zero; int read; while ((read = await source.ReadAsync(buffer, cancellationToken)) > 0) { await destination.WriteAsync(buffer.AsMemory(0, read), cancellationToken); sha256.AppendData(buffer, 0, read); received += read; if (reportTimer.Elapsed - lastReportAt >= TimeSpan.FromMilliseconds(100)) { progress?.Report(new ToolPackageDownloadProgress(received, totalBytes)); lastReportAt = reportTimer.Elapsed; } } progress?.Report(new ToolPackageDownloadProgress(received, totalBytes)); var actualHash = Convert.ToHexString(sha256.GetHashAndReset()).ToLowerInvariant(); if (!string.Equals(actualHash, package.Sha256, StringComparison.OrdinalIgnoreCase)) throw new InvalidDataException($"工具包校验失败。期望 SHA-256:{package.Sha256},实际:{actualHash}。"); } }