256 lines
8.7 KiB
C#
256 lines
8.7 KiB
C#
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<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; 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.Windows;
|
|
|
|
// 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);
|
|
|
|
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.Address))
|
|
srcFilePath = Path.Combine(dataPath, packageSource.Address);
|
|
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<bool> InstallPackageAsync(UpdatePackageInfo package, string? localInstallPath)
|
|
{
|
|
return Task.Run(() => InstallPackage(package, localInstallPath));
|
|
}
|
|
} |