- Große Kopien laufen im Hintergrund mit Fortschritt und Abbrechen; abgebrochene Kopien werden aufgeräumt, das Original bleibt unverändert - Sichern/Wiederherstellen mit Fortschritt, keine halben ZIP-Dateien - Installer auf .NET Framework 4.8: erkennt fehlende .NET 9 Desktop Runtime und installiert sie auf Wunsch (Microsoft-Signatur wird geprüft) - Update-Hinweis über Gitea-Releases, Update mit automatischem Neustart - Fenstergröße und -position werden gemerkt - Desktop-Boxen: umbenennen, löschen, verschieben, herausziehen - Fenster und Boxen kehren auf sichtbare Bildschirme zurück - README ergänzt Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
103 lines
4.4 KiB
C#
103 lines
4.4 KiB
C#
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);
|
|
|
|
/// <summary>Fragt das neueste Release auf Gitea ab (https://git.viper.ipv64.net/M_Viper/DeskCore/releases).</summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>Neuere Version oder null. Wirft bei Netzwerk-/Serverfehlern.</summary>
|
|
public static async Task<UpdateInfo?> 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);
|
|
}
|
|
|
|
/// <summary>Lädt den Installer in den Temp-Ordner und gibt den Pfad zurück.</summary>
|
|
public static async Task<string> DownloadSetupAsync(UpdateInfo info, Action<long, long>? 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;
|
|
}
|
|
}
|