XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEToolBox

【WPF】XFE工具箱

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Diagnostics;
using System.Net;
using System.Net.Http.Json;
using System.Security.Cryptography;

namespace XFEToolBox.Core.Tools;

/// <summary>
/// Small client shared by the WPF host and other consumers of the tool catalog.
/// </summary>
public sealed class ToolCatalogClient(HttpClient httpClient)
{
    private readonly HttpClient _httpClient = httpClient;

    public async Task<IReadOnlyList<ToolPackageSummary>> 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<ToolPackageSummary[]>(cancellationToken) ?? [];
    }

    public async Task<ToolPackageDetails?> 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<ToolPackageDetails>(cancellationToken);
    }

    /// <summary>
    /// Downloads a package and verifies it against the catalog SHA-256 before returning.
    /// </summary>
    public Task DownloadPackageAsync(
        ToolPackageVersionInfo package,
        Stream destination,
        CancellationToken cancellationToken = default)
        => DownloadPackageAsync(package, destination, progress: null, cancellationToken);

    /// <summary>
    /// Downloads a package, reports streamed byte progress, and verifies the catalog SHA-256.
    /// </summary>
    public async Task DownloadPackageAsync(
        ToolPackageVersionInfo package,
        Stream destination,
        IProgress<ToolPackageDownloadProgress>? 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}。");
    }
}