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; } }