using System.Diagnostics; using System.IO.Compression; using Pilz.Runtime; 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; } public bool MakeAppBinaryExecutable { get; set; } = RuntimeInformationsEx.OSType == OSType.Linux || RuntimeInformationsEx.OSType == OSType.OSX; // 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); switch (package.UpdateType) { case UpdateType.Folder: { var dataPathDir = Directory.CreateDirectory(dataPath); var destDir = Directory.CreateDirectory(localInstallPath); Utils.CopyFiles(dataPathDir, destDir); break; } case UpdateType.File: { string? srcFilePath = null; if (packageSource.PackageType == PackageType.File) srcFilePath = dataPath; else if (!string.IsNullOrWhiteSpace(packageSource.Path)) srcFilePath = Path.Combine(dataPath, packageSource.Path); else if (Directory.GetFiles(dataPath).FirstOrDefault() is { } firstFile) srcFilePath = firstFile; if (srcFilePath != null) Utils.CopyFile(new FileInfo(srcFilePath), new FileInfo(localInstallPath)); break; } } RaiseStatusChanged(UpdateStatus.Copying, UpdateStatusEvent.PostEvent); // Make executable if (MakeAppBinaryExecutable) { switch (package.UpdateType) { case UpdateType.File: Utils.MakeExecutable(localInstallPath); break; case UpdateType.Folder when !string.IsNullOrWhiteSpace(package.ExePath): Utils.MakeExecutable(Path.Combine(localInstallPath, package.ExePath)); break; } } // Delete Package RaiseStatusChanged(UpdateStatus.Cleanup, UpdateStatusEvent.PreEvent); switch (packageSource.PackageType) { case PackageType.Zip: File.Delete(packagePath); Directory.Delete(dataPath, true); break; case PackageType.File when packageSource.AddressType == PackageAddressType.Http: File.Delete(dataPath); break; } 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)); } }