Files
Pilz.Updating/Pilz.Updating.Client/UpdateClient.cs
2024-09-04 11:24:20 +02:00

216 lines
7.2 KiB
C#

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<UpdatePackageInfo, string> 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; private set; } = (AppChannel)Math.Max((int)minimumChannel, (int)currentVersion.Channel);
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;
// 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<bool> UpdateInteractive()
{
var latestVersion = await CheckForUpdate();
if (HasUpdates && latestVersion is not null)
return await UpdateInteractive(latestVersion);
return false;
}
public async Task<bool> 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<UpdateInfo?> GetUpdateInfo()
{
string str = await WebClient.GetStringAsync(UpdateUrl);
var info = UpdateInfo.Parse(str);
return info;
}
public async Task<UpdatePackageInfo?> 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<bool> 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.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.Address);
await zipStream.CopyToAsync(zipFile);
// Remember path to package directory
dicPackagePaths.Add(package, zipPath);
}
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;
}
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;
if (package.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 (package.PackageType == PackageType.Zip)
{
File.Delete(packagePath);
Directory.Delete(dataPath, true);
}
else if (package.PackageType == PackageType.File)
File.Delete(dataPath);
RaiseStatusChanged(UpdateStatus.Cleanup, UpdateStatusEvent.PostEvent);
// Finish
RaiseStatusChanged(UpdateStatus.Done, UpdateStatusEvent.Default);
return true;
}
public Task<bool> InstallPackageAsync(UpdatePackageInfo package, string? localInstallPath)
{
return Task.Run(() => InstallPackage(package, localInstallPath));
}
}