using System.IO.Compression; namespace Pilz.Updating.Client; public class UpdateClient(string updateUrl, AppVersion currentVersion, AppChannel minimumChannel) { // E v e n t s public event UpdateClientStatusChangedEventHandler? OnStatusChanged; // F i e l d s private readonly Dictionary dicPackagePaths = []; // P r o p e r t i e s public HttpClient WebClient { get; private set; } = new(); public string UpdateUrl { get; private set; } = updateUrl; public AppVersion CurrentVersion { get; private set; } = currentVersion; public AppChannel MinimumChannel { get; set; } = minimumChannel; public UpdateInfo? UpdateInfo { get; private set; } public UpdatePackageInfo? UpdatePackageInfo { get; private set; } public string? HostApplicationPath { get; set; } public string? ApplicationName { get; set; } public bool InstallAsAdmin { get; set; } public bool UIDarkMode { get; set; } public bool HasUpdates => UpdatePackageInfo != null; public string? Distro { get; set; } // E v e n t M e t h o d s private void RaiseStatusChanged(UpdateStatus status) { RaiseStatusChanged(status, UpdateStatusEvent.Default); } private void RaiseStatusChanged(UpdateStatus status, UpdateStatusEvent statusEvent) { RaiseStatusChanged(status, statusEvent, false); } private bool RaiseStatusChanged(UpdateStatus status, UpdateStatusEvent statusEvent, bool canCancel) { var args = new UpdateStatusEventArgs(this, status, statusEvent, canCancel); OnStatusChanged?.Invoke(this, args); return args.CanCancel && args.Cancel; } // U p d a t e r o u t i n e s public async Task UpdateInteractive() { var latestVersion = await CheckForUpdate(); if (HasUpdates && latestVersion is not null) return await UpdateInteractive(latestVersion); return false; } public async Task UpdateInteractive(UpdatePackageInfo package) { if (await DownloadPackageAsync(package)) return await InstallPackageAsync(package, HostApplicationPath); return false; } // F e a t u r e s public async Task GetUpdateInfo() { string str = await WebClient.GetStringAsync(UpdateUrl); var info = UpdateInfo.Parse(str); return info; } public async Task CheckForUpdate() { RaiseStatusChanged(UpdateStatus.Searching, UpdateStatusEvent.PreEvent); UpdateInfo = await GetUpdateInfo(); if (UpdateInfo is null) return null; return CheckForUpdate(UpdateInfo); } private UpdatePackageInfo? CheckForUpdate(UpdateInfo updateInfo) { var latestVersion = CurrentVersion; foreach (UpdatePackageInfo pkgInfo in updateInfo.Packages) { if (pkgInfo.Version.Channel <= MinimumChannel && pkgInfo.Version > latestVersion) { UpdatePackageInfo = pkgInfo; latestVersion = pkgInfo.Version; } } if (RaiseStatusChanged(UpdateStatus.Searching, UpdateStatusEvent.PostEvent, true)) { UpdatePackageInfo = null; return null; } return UpdatePackageInfo; } public async Task DownloadPackageAsync(UpdatePackageInfo package) { RaiseStatusChanged(UpdateStatus.Downloading, UpdateStatusEvent.PreEvent); var packageSource = package.GetSource(Distro); if (packageSource.AddressType == PackageAddressType.Http) { var dirPath = Path.Combine(MyPaths.GetMyAppDataPath(), package.GetHashCode().ToString()); var zipPath = Path.Combine(dirPath, packageSource.PackageType == PackageType.File ? "app.exe" : "package.zip"); var dir = new DirectoryInfo(dirPath); try { // Ensure existing and empty directory for the Zip File if (dir.Exists) dir.Delete(true); dir.Create(); // Download zip package using var zipFile = new FileStream(zipPath, FileMode.Create, FileAccess.ReadWrite); using var zipStream = await WebClient.GetStreamAsync(package.GetAddress(Distro)); await zipStream.CopyToAsync(zipFile); // Remember path to package directory dicPackagePaths.Add(package, zipPath); } catch (Exception) { return false; } } else if (packageSource.AddressType == PackageAddressType.Local) dicPackagePaths.Add(package, package.GetAddress(Distro)); else return false; if (RaiseStatusChanged(UpdateStatus.Downloading, UpdateStatusEvent.PostEvent, true)) { RaiseStatusChanged(UpdateStatus.Canceled); return false; } return true; } public bool InstallPackage(UpdatePackageInfo package, string? localInstallPath) { if (string.IsNullOrWhiteSpace(localInstallPath) || !dicPackagePaths.TryGetValue(package, out var packagePath)) { RaiseStatusChanged(UpdateStatus.Failed); return false; } // Extract Package if (RaiseStatusChanged(UpdateStatus.Extracting, UpdateStatusEvent.PreEvent, true)) { RaiseStatusChanged(UpdateStatus.Canceled); return false; } string dataPath; var packageSource = package.GetSource(Distro); if (packageSource.PackageType == PackageType.Zip) { dataPath = packagePath + ".extracted"; var packagePathDir = new DirectoryInfo(packagePath); if (packagePathDir.Exists) { packagePathDir.Delete(true); Task.Delay(1000); } ZipFile.ExtractToDirectory(packagePath, dataPath); } else dataPath = packagePath; RaiseStatusChanged(UpdateStatus.Extracting, UpdateStatusEvent.PostEvent); // Install Package RaiseStatusChanged(UpdateStatus.Copying, UpdateStatusEvent.PreEvent); if (package.UpdateType == UpdateType.Folder) { var dataPathDir = Directory.CreateDirectory(dataPath); var destDir = Directory.CreateDirectory(localInstallPath); Utils.CopyFiles(dataPathDir, destDir); } else if (package.UpdateType == UpdateType.File) Utils.CopyFile(new FileInfo(dataPath), new FileInfo(localInstallPath)); RaiseStatusChanged(UpdateStatus.Copying, UpdateStatusEvent.PostEvent); // Delete Package RaiseStatusChanged(UpdateStatus.Cleanup, UpdateStatusEvent.PreEvent); if (packageSource.PackageType == PackageType.Zip) { File.Delete(packagePath); Directory.Delete(dataPath, true); } else if (packageSource.PackageType == PackageType.File && packageSource.AddressType == PackageAddressType.Http) File.Delete(dataPath); RaiseStatusChanged(UpdateStatus.Cleanup, UpdateStatusEvent.PostEvent); // Finish RaiseStatusChanged(UpdateStatus.Done, UpdateStatusEvent.Default); return true; } public Task InstallPackageAsync(UpdatePackageInfo package, string? localInstallPath) { return Task.Run(() => InstallPackage(package, localInstallPath)); } }