using System.Text.Json; namespace VideoDownloader.Core.Update; /// 릴리스의 설치 자산 하나. public sealed record UpdateAsset(string Name, string DownloadUrl); /// 업데이트 확인 결과. LatestVersion 은 태그의 v 접두를 뗀 정규형. public sealed record UpdateCheckResult(bool IsNewer, string CurrentVersion, string LatestVersion, string ReleaseUrl, UpdateAsset? Asset); /// /// DEPLOYMENT_PLAN §6 자동 업데이트 v1 — Releases latest 태그 폴링(무의존·전 OS 공통). /// 기본 채널은 자체 Forgejo 공개 릴리스 저장소(§5-bis)이고, apiBase 로 GitHub 등 다른 채널도 쓸 수 있다 /// (Forgejo 와 GitHub 의 releases/latest 응답 형태가 동일). 설치는 사람 관문 — URL 만 돌려준다. /// public sealed class UpdateChecker { /// 기본 업데이트 채널 API 베이스 — Forgejo 는 /api/v1 접두 포함. public const string DefaultApiBase = "https://git.chanpaca.net/api/v1"; private readonly HttpClient _http; public UpdateChecker(HttpMessageHandler? handler = null) { _http = handler == null ? new HttpClient() : new HttpClient(handler); _http.DefaultRequestHeaders.UserAgent.ParseAdd("VideoDownloader-UpdateCheck"); // GitHub API 는 UA 필수 _http.Timeout = TimeSpan.FromSeconds(15); } /// "v1.2.3"/"1.2.3.0" 을 관대하게 비교. 어느 쪽이든 파싱 불가면 false(갱신 아님). public static bool IsNewer(string current, string latest) => TryParse(current, out var cur) && TryParse(latest, out var lat) && lat > cur; private static bool TryParse(string s, out Version version) { s = s.Trim().TrimStart('v', 'V'); if (!s.Contains('.')) s += ".0"; // Version.TryParse 는 최소 2파트 요구 return Version.TryParse(s, out version!); } /// RID(win-x64 등)에 맞는 설치 자산 선택. "x64" 판정에서 arm64 를 먼저 배제한다. public static UpdateAsset? SelectAsset(IEnumerable assets, string rid) { var parts = rid.Split('-'); var os = parts[0]; var arch = parts[^1]; foreach (var asset in assets) { var name = asset.Name.ToLowerInvariant(); var isArm = name.Contains("arm64"); if (arch == "arm64" ? !isArm : isArm || !name.Contains(arch)) continue; var osMatch = os switch { "win" => name.EndsWith(".exe") || name.EndsWith(".msi"), "linux" => name.EndsWith(".tar.gz") || name.EndsWith(".appimage") || name.EndsWith(".deb"), "osx" => name.EndsWith(".dmg") || name.EndsWith(".tar.gz"), // 프리뷰는 tar.gz 배포(§5-bis), 정식 서명판은 dmg _ => false, }; if (osMatch) return asset; } return null; } /// /// repo("owner/repo")의 latest 릴리스를 조회한다. API 실패는 HttpRequestException 으로 드러난다. /// apiBase 미지정이면 기본 채널(자체 Forgejo). GitHub 는 "https://api.github.com" 을 넘긴다. /// public async Task CheckAsync(string repo, string currentVersion, string rid, string? apiBase = null, CancellationToken ct = default) { using var res = await _http.GetAsync($"{(apiBase ?? DefaultApiBase).TrimEnd('/')}/repos/{repo}/releases/latest", ct); res.EnsureSuccessStatusCode(); using var doc = JsonDocument.Parse(await res.Content.ReadAsStringAsync(ct)); var root = doc.RootElement; var tag = root.GetProperty("tag_name").GetString() ?? ""; var releaseUrl = root.TryGetProperty("html_url", out var h) ? h.GetString() ?? "" : ""; var assets = new List(); if (root.TryGetProperty("assets", out var arr) && arr.ValueKind == JsonValueKind.Array) { foreach (var a in arr.EnumerateArray()) { assets.Add(new UpdateAsset( a.GetProperty("name").GetString() ?? "", a.GetProperty("browser_download_url").GetString() ?? "")); } } return new UpdateCheckResult( IsNewer(currentVersion, tag), currentVersion, tag.TrimStart('v', 'V'), releaseUrl, SelectAsset(assets, rid)); } }