using System.IO;
using System.Net.Http;
using System.Text.Json;
namespace DeskCore;
public record UpdateInfo(Version Version, string Tag, string PageUrl, string? SetupUrl, string? Notes);
/// Fragt das neueste Release auf Gitea ab (https://git.viper.ipv64.net/M_Viper/DeskCore/releases).
public static class UpdateChecker
{
private const string Host = "git.viper.ipv64.net";
private const string LatestApi = $"https://{Host}/api/v1/repos/M_Viper/DeskCore/releases/latest";
public const string ReleasesPage = $"https://{Host}/M_Viper/DeskCore/releases";
private const string SetupAsset = "DeskCore-Setup.exe";
public static Version Current
{
get
{
var v = typeof(UpdateChecker).Assembly.GetName().Version ?? new Version(1, 0, 0);
return new Version(v.Major, v.Minor, Math.Max(0, v.Build));
}
}
private static HttpClient NewClient(TimeSpan timeout)
{
var http = new HttpClient { Timeout = timeout };
http.DefaultRequestHeaders.UserAgent.ParseAdd($"DeskCore/{Current}");
return http;
}
/// Neuere Version oder null. Wirft bei Netzwerk-/Serverfehlern.
public static async Task CheckAsync(CancellationToken ct = default)
{
using var http = NewClient(TimeSpan.FromSeconds(15));
using var resp = await http.GetAsync(LatestApi, ct);
if (resp.StatusCode == System.Net.HttpStatusCode.NotFound)
throw new InvalidOperationException("Es ist noch kein Release veröffentlicht (oder das Repository ist nicht öffentlich).");
resp.EnsureSuccessStatusCode();
using var doc = JsonDocument.Parse(await resp.Content.ReadAsStringAsync(ct));
var root = doc.RootElement;
var tag = root.GetProperty("tag_name").GetString() ?? "";
if (!TryParseVersion(tag, out var version) || version <= Current) return null;
string? setupUrl = null;
if (root.TryGetProperty("assets", out var assets))
foreach (var a in assets.EnumerateArray())
if (string.Equals(a.GetProperty("name").GetString(), SetupAsset, StringComparison.OrdinalIgnoreCase))
setupUrl = a.GetProperty("browser_download_url").GetString();
// Nur Downloads vom eigenen Server zulassen
if (setupUrl is not null && !IsTrusted(setupUrl)) setupUrl = null;
var page = root.TryGetProperty("html_url", out var h) ? h.GetString() : null;
return new UpdateInfo(version, tag, page is not null && IsTrusted(page) ? page : ReleasesPage, setupUrl,
root.TryGetProperty("body", out var b) ? b.GetString() : null);
}
/// Lädt den Installer in den Temp-Ordner und gibt den Pfad zurück.
public static async Task DownloadSetupAsync(UpdateInfo info, Action? progress, CancellationToken ct)
{
if (info.SetupUrl is null) throw new InvalidOperationException("Im Release fehlt die Datei DeskCore-Setup.exe.");
using var http = NewClient(TimeSpan.FromMinutes(10));
using var resp = await http.GetAsync(info.SetupUrl, HttpCompletionOption.ResponseHeadersRead, ct);
resp.EnsureSuccessStatusCode();
long total = resp.Content.Headers.ContentLength ?? 0;
var path = Path.Combine(Path.GetTempPath(), $"DeskCore-Setup-{info.Version}.exe");
await using (var input = await resp.Content.ReadAsStreamAsync(ct))
await using (var output = File.Create(path))
{
var buffer = new byte[81920];
long done = 0;
int read;
while ((read = await input.ReadAsync(buffer, ct)) > 0)
{
await output.WriteAsync(buffer.AsMemory(0, read), ct);
done += read;
progress?.Invoke(done, total);
}
}
return path;
}
private static bool IsTrusted(string url) =>
Uri.TryCreate(url, UriKind.Absolute, out var u) && u.Scheme == Uri.UriSchemeHttps &&
string.Equals(u.Host, Host, StringComparison.OrdinalIgnoreCase);
public static bool TryParseVersion(string tag, out Version version)
{
var t = tag.Trim().TrimStart('v', 'V');
if (Version.TryParse(t, out var v))
{
version = new Version(v.Major, v.Minor, Math.Max(0, v.Build));
return true;
}
version = new Version(0, 0, 0);
return false;
}
}