using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Net.Http; using System.Security.Cryptography; using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Deploymentcenter.Client.Models; namespace Deploymentcenter.Client { public class UpdateCheckResult { public bool UpdateAvailable { get; set; } public bool IsCritical { get; set; } public VersionInfo? LatestRelease { get; set; } public ReleaseManifest? FullManifest { get; set; } public string Message { get; set; } = string.Empty; public Exception? Error { get; set; } } public class IntegrityCheckResult { public bool IsValid { get; set; } = true; public List MissingFiles { get; } = new List(); public List CorruptedFiles { get; } = new List(); public int TotalCheckedFiles { get; set; } } public class UpdateClient { private static readonly HttpClient SharedHttpClient = new HttpClient(); private readonly HttpClient _httpClient; public UpdateClient(HttpClient? httpClient = null) { _httpClient = httpClient ?? SharedHttpClient; } /// /// Checks for update availability against LEMP static latest.json or Deploymentcenter API. /// public async Task CheckForUpdateAsync( string baseUrl, string projectId, string currentVersion, string channel = "prod", CancellationToken cancellationToken = default) { var result = new UpdateCheckResult(); try { string cleanBaseUrl = baseUrl.TrimEnd('/'); // Primary check: LEMP static channel latest.json // Path pattern: https://domain/releases/{ProjectId}/{channel}/latest.json string staticUrl = $"{cleanBaseUrl}/releases/{projectId}/{channel}/latest.json"; HttpResponseMessage response; try { response = await _httpClient.GetAsync(staticUrl, cancellationToken).ConfigureAwait(false); } catch { // Fallback check: Deploymentcenter API endpoint // Path pattern: https://domain/api/updateservice/v1/check?product={projectId}&version={currentVersion} string apiUrl = $"{cleanBaseUrl}/api/updateservice/v1/check?product={Uri.EscapeDataString(projectId)}&version={Uri.EscapeDataString(currentVersion)}"; response = await _httpClient.GetAsync(apiUrl, cancellationToken).ConfigureAwait(false); } if (!response.IsSuccessStatusCode) { result.Message = $"HTTP Error {response.StatusCode} during update check"; return result; } string json = await response.Content.ReadAsStringAsync().ConfigureAwait(false); using var doc = JsonDocument.Parse(json); var root = doc.RootElement; // Handle static latest.json format if (root.TryGetProperty("latest", out var latestProp) && latestProp.ValueKind == JsonValueKind.Object) { var manifest = JsonSerializer.Deserialize(json); if (manifest?.Latest != null) { result.FullManifest = manifest; result.LatestRelease = manifest.Latest; if (IsVersionNewer(currentVersion, manifest.Latest.Version)) { result.UpdateAvailable = true; result.IsCritical = manifest.Latest.IsCritical; result.Message = $"New release v{manifest.Latest.Version} available."; } else { result.Message = "Application is up to date."; } } } // Handle API response format else if (root.TryGetProperty("update_available", out var availProp)) { bool available = availProp.GetBoolean(); result.UpdateAvailable = available; if (root.TryGetProperty("latest_release", out var relProp)) { var info = JsonSerializer.Deserialize(relProp.GetRawText()); result.LatestRelease = info; result.IsCritical = info?.IsCritical ?? false; } result.Message = available ? "Update available." : "Application is up to date."; } } catch (Exception ex) { result.Error = ex; result.Message = $"Update check failed: {ex.Message}"; } return result; } /// /// Validates local application integrity against manifest.json. /// public static IntegrityCheckResult VerifyIntegrity(string localAppDir, PackageManifest manifest) { var result = new IntegrityCheckResult(); if (manifest == null || manifest.Files == null || manifest.Files.Count == 0) { return result; } foreach (var entry in manifest.Files) { result.TotalCheckedFiles++; string fullPath = Path.Combine(localAppDir, entry.Path.Replace('/', Path.DirectorySeparatorChar)); if (!File.Exists(fullPath)) { result.IsValid = false; result.MissingFiles.Add(entry.Path); continue; } if (!string.IsNullOrEmpty(entry.Sha256)) { string computedHash = ComputeSha256(fullPath); if (!string.Equals(computedHash, entry.Sha256, StringComparison.OrdinalIgnoreCase)) { result.IsValid = false; result.CorruptedFiles.Add(entry.Path); } } } return result; } /// /// Launches UpdateAgent process with appropriate parameters and optionally exits current application. /// public static bool LaunchUpdateAgent( string agentPath, string projectId, string channel = "prod", string action = "update", string version = "latest", string? targetDir = null, bool exitCurrentApp = true) { if (!File.Exists(agentPath)) { return false; } targetDir ??= AppDomain.CurrentDomain.BaseDirectory; var args = new StringBuilder(); args.Append($"--project \"{projectId}\" "); args.Append($"--channel \"{channel}\" "); args.Append($"--action \"{action}\" "); args.Append($"--version \"{version}\" "); args.Append($"--target-dir \"{targetDir}\""); var startInfo = new ProcessStartInfo { FileName = agentPath, Arguments = args.ToString(), UseShellExecute = true }; Process.Start(startInfo); if (exitCurrentApp) { Environment.Exit(0); } return true; } public static string ComputeSha256(string filePath) { using var sha256 = SHA256.Create(); using var stream = File.OpenRead(filePath); byte[] hash = sha256.ComputeHash(stream); return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); } public static bool IsVersionNewer(string currentVer, string remoteVer) { if (string.IsNullOrEmpty(remoteVer)) return false; if (string.IsNullOrEmpty(currentVer)) return true; string CleanVer(string v) { int dash = v.IndexOf('-'); return dash > 0 ? v.Substring(0, dash) : v; } if (Version.TryParse(CleanVer(currentVer), out var cVer) && Version.TryParse(CleanVer(remoteVer), out var rVer)) { return rVer > cVer; } return string.Compare(remoteVer, currentVer, StringComparison.OrdinalIgnoreCase) > 0; } } }