using System.IO.Compression; namespace Pilz.Updating.Client; public class UpdateClient(string updateUrl, AppVersion currentVersion, Channels 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 Channels MinimumChannel { get; private set; } = (Channels)Math.Max((int)minimumChannel, (int)currentVersion.Channel); public UpdateInfo? UpdateInfo { get; private set; } public UpdatePackageInfo? UpdatePackageInfo { get; private set; } public string HostApplicationPath { get; set; } = string.Empty; public string ApplicationName { get; set; } = string.Empty; public bool InstallAsAdmin { get; set; } = false; public bool UIDarkMode { get; set; } = false; public bool HasUpdates => UpdatePackageInfo != null; // 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) await UpdateInteractive(latestVersion); } public async Task UpdateInteractive(UpdatePackageInfo package) { if (await DownloadPackageAsync(package)) await InstallPackageAsync(package); } // 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; } private 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); if (package.AddressType == PackageAddressType.Http) { var dirPath = Path.Combine(MyPaths.GetMyAppDataPath(), package.GetHashCode().ToString()); var zipPath = Path.Combine(dirPath, "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.Address); await zipStream.CopyToAsync(zipFile); // Remember path to package directory dicPackagePaths.Add(package, dirPath); } catch (Exception) { return false; } } else if (package.AddressType == PackageAddressType.Local) dicPackagePaths.Add(package, package.Address); else return false; if (!RaiseStatusChanged(UpdateStatus.Downloading, UpdateStatusEvent.PostEvent, true)) { RaiseStatusChanged(UpdateStatus.Canceled); return false; } return true; } private void InstallPackage(UpdatePackageInfo package) { // Extract Package if (!RaiseStatusChanged(UpdateStatus.Extracting, UpdateStatusEvent.PreEvent, true)) { RaiseStatusChanged(UpdateStatus.Canceled); return; } if (!dicPackagePaths.TryGetValue(package, out var packagePath)) { RaiseStatusChanged(UpdateStatus.Failed); return; } string dataPath; if (package.Type == PackageType.Zip) { dataPath = packagePath + ".extracted"; var packagePathDir = new DirectoryInfo(packagePath); if (packagePathDir.Exists) { packagePathDir.Delete(true); Task.Delay(1000); } ZipFile.ExtractToDirectory(packagePath, dataPath); } else if (package.Type == PackageType.Folder) dataPath = packagePath; else throw new NotImplementedException("This PackageType is not defined and not support!"); RaiseStatusChanged(UpdateStatus.Extracting, UpdateStatusEvent.PostEvent); // Install Package RaiseStatusChanged(UpdateStatus.Copying, UpdateStatusEvent.PreEvent); var dataPathDir = new DirectoryInfo(dataPath); var destDir = new DirectoryInfo(HostApplicationPath); Utils.CopyFiles(dataPathDir, destDir); RaiseStatusChanged(UpdateStatus.Copying, UpdateStatusEvent.PostEvent); // Delete Package RaiseStatusChanged(UpdateStatus.Cleanup, UpdateStatusEvent.PreEvent); File.Delete(packagePath); Directory.Delete(dataPath, true); RaiseStatusChanged(UpdateStatus.Cleanup, UpdateStatusEvent.PostEvent); // Finish RaiseStatusChanged(UpdateStatus.Done, UpdateStatusEvent.Default); } public Task InstallPackageAsync(UpdatePackageInfo package) { return Task.Run(() => InstallPackage(package)); } }