refactor(releases): move release-API + checksum types to ClaudeDo.Releases

This commit is contained in:
mika kuns
2026-04-23 14:09:40 +02:00
parent 41e0bea162
commit 46e01aefed
7 changed files with 8 additions and 3 deletions

View File

@@ -0,0 +1,37 @@
using System.IO;
using System.Security.Cryptography;
namespace ClaudeDo.Releases;
public static class ChecksumVerifier
{
public static string ComputeSha256(string filePath)
{
using var stream = File.OpenRead(filePath);
using var sha = SHA256.Create();
var hash = sha.ComputeHash(stream);
return Convert.ToHexString(hash).ToLowerInvariant();
}
public static bool Verify(string filePath, string expectedSha256)
{
var actual = ComputeSha256(filePath);
return string.Equals(actual, expectedSha256.Trim(), StringComparison.OrdinalIgnoreCase);
}
public static IReadOnlyDictionary<string, string> ParseChecksumsFile(string content)
{
var map = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (var rawLine in content.Split('\n'))
{
var line = rawLine.Trim();
if (line.Length == 0) continue;
var parts = line.Split(new[] { ' ', '\t' }, 2, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length != 2) continue;
var hashPart = parts[0].Trim();
if (hashPart.Length != 64) continue;
map[parts[1].Trim()] = hashPart;
}
return map;
}
}

View File

@@ -0,0 +1,15 @@
namespace ClaudeDo.Releases;
public sealed record ReleaseAsset(string Name, string BrowserDownloadUrl, long Size);
public sealed record GiteaRelease(
string TagName,
string Name,
IReadOnlyList<ReleaseAsset> Assets);
public interface IReleaseClient
{
Task<GiteaRelease?> GetLatestReleaseAsync(CancellationToken ct);
Task DownloadAsync(string url, string destPath, IProgress<long> progress, CancellationToken ct);
}

View File

@@ -0,0 +1,85 @@
using System.IO;
using System.Net.Http;
using System.Text.Json;
namespace ClaudeDo.Releases;
public sealed class ReleaseClient : IReleaseClient
{
public const string DefaultApiBase = "https://git.kuns.dev/api/v1/repos/releases/ClaudeDo";
private readonly HttpClient _http;
private readonly string _apiBase;
public ReleaseClient(HttpClient http, string apiBase = DefaultApiBase)
{
_http = http;
_apiBase = apiBase.TrimEnd('/');
}
public async Task<GiteaRelease?> GetLatestReleaseAsync(CancellationToken ct)
{
try
{
using var response = await _http.GetAsync($"{_apiBase}/releases/latest", ct);
if (!response.IsSuccessStatusCode) return null;
var json = await response.Content.ReadAsStringAsync(ct);
return ParseRelease(json);
}
catch (HttpRequestException) { return null; }
catch (TaskCanceledException) when (!ct.IsCancellationRequested) { return null; }
}
public async Task DownloadAsync(string url, string destPath, IProgress<long> progress, CancellationToken ct)
{
using var response = await _http.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, ct);
response.EnsureSuccessStatusCode();
await using var input = await response.Content.ReadAsStreamAsync(ct);
await using var output = File.Create(destPath);
var buffer = new byte[81920];
long total = 0;
int read;
while ((read = await input.ReadAsync(buffer, ct)) > 0)
{
await output.WriteAsync(buffer.AsMemory(0, read), ct);
total += read;
progress.Report(total);
}
}
private static GiteaRelease? ParseRelease(string json)
{
try
{
using var doc = JsonDocument.Parse(json);
var root = doc.RootElement;
if (!root.TryGetProperty("tag_name", out var tagEl)) return null;
var tag = tagEl.GetString() ?? "";
var name = root.TryGetProperty("name", out var nameEl) ? (nameEl.GetString() ?? "") : "";
var assets = new List<ReleaseAsset>();
if (root.TryGetProperty("assets", out var arr) && arr.ValueKind == JsonValueKind.Array)
{
foreach (var item in arr.EnumerateArray())
{
if (!item.TryGetProperty("name", out var nameField)) continue;
if (!item.TryGetProperty("browser_download_url", out var urlField)) continue;
var aName = nameField.GetString() ?? "";
var aUrl = urlField.GetString() ?? "";
var aSize = item.TryGetProperty("size", out var s) ? s.GetInt64() : 0L;
assets.Add(new ReleaseAsset(aName, aUrl, aSize));
}
}
return new GiteaRelease(tag, name, assets);
}
catch (JsonException)
{
return null;
}
}
}