Editor/ModelStore.cs
using System;
using System.IO;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;

namespace HumanoidMocapX.EditorTools;

/// <summary>Downloads a selected upstream checkpoint into a verified local cache.</summary>
public sealed partial class ModelStore
{
    // Pin the upstream revision so a later model update cannot silently change reconstruction.
    public const string Revision = "1eb105e554e0cfc1846ea5feac8c2d516a36d319";
    const string Repository = "https://huggingface.co/fafsaf1/flowhmr-0.46B/";
    const int BufferBytes = 1024 * 1024;
    const long MaximumCheckpointBytes = 4L * 1024 * 1024 * 1024;
    readonly HttpClient http;
    readonly string cache;

    /// <summary>Uses the caller-owned HTTP client and cache directory. Rejects a null
    /// client or blank directory; does not create files until a download is requested.</summary>
    public ModelStore(HttpClient http, string cache)
    {
        ArgumentNullException.ThrowIfNull(http);
        ArgumentException.ThrowIfNullOrWhiteSpace(cache);
        this.http = http;
        this.cache = Path.GetFullPath(cache);
    }

    /// <summary>Explicitly downloads Base or Latest; call only after a user download action.
    /// Verifies SHA-256 before installation or reuse.
    /// Network, integrity and cancellation failures propagate without replacing a cached file.</summary>
    public async Task<string> EnsureAsync(FlowModel model, Action<string> progress, CancellationToken token)
    {
        var name = model switch
        {
            FlowModel.Base => "flowhmr_base",
            FlowModel.Latest => "flowhmr_latest",
            _ => throw new ArgumentOutOfRangeException(nameof(model))
        };
        var relative = $"{name}/{name}.ckpt";
        using var metadata = await http.GetAsync(Repository + $"raw/{Revision}/" + relative,
            HttpCompletionOption.ResponseHeadersRead, token);
        metadata.EnsureSuccessStatusCode();
        await metadata.Content.LoadIntoBufferAsync(4096, token);
        var pointer = await metadata.Content.ReadAsStringAsync(token);
        var digest = Regex.Match(pointer, @"(?m)^oid sha256:([a-f0-9]{64})\r?$");
        var length = Regex.Match(pointer, @"(?m)^size ([0-9]+)\r?$");
        if (!digest.Success || !length.Success || !long.TryParse(length.Groups[1].Value, out var bytes)
            || bytes <= 0 || bytes > MaximumCheckpointBytes)
            throw new InvalidDataException("Invalid FlowHMR checkpoint metadata: " + relative);
        var directory = Path.Combine(cache, Revision); Directory.CreateDirectory(directory);
        var path = Path.Combine(directory, name + ".ckpt");
        var sha256 = digest.Groups[1].Value;
        await EnsureFileAsync(Repository + $"resolve/{Revision}/" + relative, path, bytes, sha256, progress, token);
        return path;
    }

    async Task DownloadAsync(string url, string path, long expectedBytes, string expectedHash,
        Action<string> progress, CancellationToken token)
    {
        var partial = path + "." + Guid.NewGuid().ToString("N") + ".partial";
        try
        {
            using var response = await http.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, token);
            response.EnsureSuccessStatusCode();
            if (response.Content.Headers.ContentLength is long length && length != expectedBytes)
                throw new InvalidDataException("Unexpected download length for " + Path.GetFileName(path));
            await using (var input = await response.Content.ReadAsStreamAsync(token))
            await using (var output = new FileStream(partial, FileMode.CreateNew, FileAccess.Write, FileShare.None, BufferBytes, true))
            using (var hash = IncrementalHash.CreateHash(HashAlgorithmName.SHA256))
            {
                var buffer = new byte[BufferBytes]; long total = 0; int previous = -1;
                while (await input.ReadAsync(buffer, token) is var count && count > 0)
                {
                    total += count;
                    if (total > expectedBytes) throw new InvalidDataException("Download exceeds declared length for " + Path.GetFileName(path));
                    await output.WriteAsync(buffer.AsMemory(0, count), token);
                    hash.AppendData(buffer, 0, count);
                    var percent = (int)(total * 100 / expectedBytes);
                    if (percent != previous) { previous = percent; progress?.Invoke($"Downloading weights · {percent}%"); }
                }
                if (total != expectedBytes || !Convert.ToHexString(hash.GetHashAndReset()).Equals(expectedHash, StringComparison.OrdinalIgnoreCase))
                    throw new InvalidDataException("Checksum or length mismatch for " + Path.GetFileName(path) + ". Retry the download.");
            }
            token.ThrowIfCancellationRequested();
            File.Move(partial, path, true);
        }
        finally { if (File.Exists(partial)) File.Delete(partial); }
    }

    static async Task<bool> MatchesAsync(string path, long bytes, string sha256, CancellationToken token)
    {
        await using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, BufferBytes, true);
        return stream.Length == bytes && Convert.ToHexString(await SHA256.HashDataAsync(stream, token))
            .Equals(sha256, StringComparison.OrdinalIgnoreCase);
    }
}