aboutsummaryrefslogtreecommitdiff
path: root/Emby.Server.Implementations/Updates/InstallationManager.cs
diff options
context:
space:
mode:
authorJoshua M. Boniface <joshua@boniface.me>2021-08-18 02:46:59 -0400
committerGitHub <noreply@github.com>2021-08-18 02:46:59 -0400
commit72d3f7020ad80ce1a53eeae8c5d57abeb22a4679 (patch)
treedd43e663838cdc7d99a4af565523df58ae23c856 /Emby.Server.Implementations/Updates/InstallationManager.cs
parent7aef0fce444e6d8e06386553ec7ea1401a01bbb1 (diff)
parente5cbafdb6b47377052e0d638908ef96e30a997d6 (diff)
Merge branch 'master' into patch-2
Diffstat (limited to 'Emby.Server.Implementations/Updates/InstallationManager.cs')
-rw-r--r--Emby.Server.Implementations/Updates/InstallationManager.cs545
1 files changed, 309 insertions, 236 deletions
diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs
index 4f54c06dd..7b0afa4e2 100644
--- a/Emby.Server.Implementations/Updates/InstallationManager.cs
+++ b/Emby.Server.Implementations/Updates/InstallationManager.cs
@@ -1,24 +1,26 @@
-#pragma warning disable CS1591
-
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
-using System.Runtime.Serialization;
+using System.Net.Http.Json;
using System.Security.Cryptography;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Common;
+using Jellyfin.Data.Events;
using MediaBrowser.Common.Configuration;
+using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Net;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Common.Updates;
+using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Events;
+using MediaBrowser.Controller.Events.Updates;
using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Net;
-using MediaBrowser.Model.Serialization;
+using MediaBrowser.Model.Plugins;
using MediaBrowser.Model.Updates;
using Microsoft.Extensions.Logging;
@@ -34,19 +36,18 @@ namespace Emby.Server.Implementations.Updates
/// </summary>
private readonly ILogger<InstallationManager> _logger;
private readonly IApplicationPaths _appPaths;
- private readonly IHttpClient _httpClient;
- private readonly IJsonSerializer _jsonSerializer;
+ private readonly IEventManager _eventManager;
+ private readonly IHttpClientFactory _httpClientFactory;
private readonly IServerConfigurationManager _config;
- private readonly IFileSystem _fileSystem;
+ private readonly JsonSerializerOptions _jsonSerializerOptions;
+ private readonly IPluginManager _pluginManager;
/// <summary>
/// Gets the application host.
/// </summary>
/// <value>The application host.</value>
- private readonly IApplicationHost _applicationHost;
-
+ private readonly IServerApplicationHost _applicationHost;
private readonly IZipClient _zipClient;
-
private readonly object _currentInstallationsLock = new object();
/// <summary>
@@ -59,93 +60,103 @@ namespace Emby.Server.Implementations.Updates
/// </summary>
private readonly ConcurrentBag<InstallationInfo> _completedInstallationsInternal;
+ /// <summary>
+ /// Initializes a new instance of the <see cref="InstallationManager"/> class.
+ /// </summary>
+ /// <param name="logger">The <see cref="ILogger{InstallationManager}"/>.</param>
+ /// <param name="appHost">The <see cref="IServerApplicationHost"/>.</param>
+ /// <param name="appPaths">The <see cref="IApplicationPaths"/>.</param>
+ /// <param name="eventManager">The <see cref="IEventManager"/>.</param>
+ /// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/>.</param>
+ /// <param name="config">The <see cref="IServerConfigurationManager"/>.</param>
+ /// <param name="zipClient">The <see cref="IZipClient"/>.</param>
+ /// <param name="pluginManager">The <see cref="IPluginManager"/>.</param>
public InstallationManager(
ILogger<InstallationManager> logger,
- IApplicationHost appHost,
+ IServerApplicationHost appHost,
IApplicationPaths appPaths,
- IHttpClient httpClient,
- IJsonSerializer jsonSerializer,
+ IEventManager eventManager,
+ IHttpClientFactory httpClientFactory,
IServerConfigurationManager config,
- IFileSystem fileSystem,
- IZipClient zipClient)
+ IZipClient zipClient,
+ IPluginManager pluginManager)
{
- if (logger == null)
- {
- throw new ArgumentNullException(nameof(logger));
- }
-
_currentInstallations = new List<(InstallationInfo, CancellationTokenSource)>();
_completedInstallationsInternal = new ConcurrentBag<InstallationInfo>();
_logger = logger;
_applicationHost = appHost;
_appPaths = appPaths;
- _httpClient = httpClient;
- _jsonSerializer = jsonSerializer;
+ _eventManager = eventManager;
+ _httpClientFactory = httpClientFactory;
_config = config;
- _fileSystem = fileSystem;
_zipClient = zipClient;
+ _jsonSerializerOptions = JsonDefaults.Options;
+ _pluginManager = pluginManager;
}
/// <inheritdoc />
- public event EventHandler<InstallationInfo> PackageInstalling;
-
- /// <inheritdoc />
- public event EventHandler<InstallationInfo> PackageInstallationCompleted;
-
- /// <inheritdoc />
- public event EventHandler<InstallationFailedEventArgs> PackageInstallationFailed;
-
- /// <inheritdoc />
- public event EventHandler<InstallationInfo> PackageInstallationCancelled;
-
- /// <inheritdoc />
- public event EventHandler<IPlugin> PluginUninstalled;
-
- /// <inheritdoc />
- public event EventHandler<InstallationInfo> PluginUpdated;
-
- /// <inheritdoc />
- public event EventHandler<InstallationInfo> PluginInstalled;
-
- /// <inheritdoc />
public IEnumerable<InstallationInfo> CompletedInstallations => _completedInstallationsInternal;
/// <inheritdoc />
- public async Task<IReadOnlyList<PackageInfo>> GetPackages(string manifest, CancellationToken cancellationToken = default)
+ public async Task<PackageInfo[]> GetPackages(string manifestName, string manifest, bool filterIncompatible, CancellationToken cancellationToken = default)
{
try
{
- using (var response = await _httpClient.SendAsync(
- new HttpRequestOptions
- {
- Url = manifest,
- CancellationToken = cancellationToken,
- CacheMode = CacheMode.Unconditional,
- CacheLength = TimeSpan.FromMinutes(3)
- },
- HttpMethod.Get).ConfigureAwait(false))
- using (Stream stream = response.Content)
+ PackageInfo[]? packages = await _httpClientFactory.CreateClient(NamedClient.Default)
+ .GetFromJsonAsync<PackageInfo[]>(new Uri(manifest), _jsonSerializerOptions, cancellationToken).ConfigureAwait(false);
+
+ if (packages == null)
{
- try
- {
- return await _jsonSerializer.DeserializeFromStreamAsync<IReadOnlyList<PackageInfo>>(stream).ConfigureAwait(false);
- }
- catch (SerializationException ex)
+ return Array.Empty<PackageInfo>();
+ }
+
+ var minimumVersion = new Version(0, 0, 0, 1);
+ // Store the repository and repository url with each version, as they may be spread apart.
+ foreach (var entry in packages)
+ {
+ for (int a = entry.Versions.Count - 1; a >= 0; a--)
{
- _logger.LogError(ex, "Failed to deserialize the plugin manifest retrieved from {Manifest}", manifest);
- return Array.Empty<PackageInfo>();
+ var ver = entry.Versions[a];
+ ver.RepositoryName = manifestName;
+ ver.RepositoryUrl = manifest;
+
+ if (!filterIncompatible)
+ {
+ continue;
+ }
+
+ if (!Version.TryParse(ver.TargetAbi, out var targetAbi))
+ {
+ targetAbi = minimumVersion;
+ }
+
+ // Only show plugins that are greater than or equal to targetAbi.
+ if (_applicationHost.ApplicationVersion >= targetAbi)
+ {
+ continue;
+ }
+
+ // Not compatible with this version so remove it.
+ entry.Versions.Remove(ver);
}
}
+
+ return packages;
}
- catch (UriFormatException ex)
+ catch (IOException ex)
{
- _logger.LogError(ex, "The URL configured for the plugin repository manifest URL is not valid: {Manifest}", manifest);
+ _logger.LogError(ex, "Cannot locate the plugin manifest {Manifest}", manifest);
return Array.Empty<PackageInfo>();
}
- catch (HttpException ex)
+ catch (JsonException ex)
{
- _logger.LogError(ex, "An error occurred while accessing the plugin manifest: {Manifest}", manifest);
+ _logger.LogError(ex, "Failed to deserialize the plugin manifest retrieved from {Manifest}", manifest);
+ return Array.Empty<PackageInfo>();
+ }
+ catch (UriFormatException ex)
+ {
+ _logger.LogError(ex, "The URL configured for the plugin repository manifest URL is not valid: {Manifest}", manifest);
return Array.Empty<PackageInfo>();
}
catch (HttpRequestException ex)
@@ -161,7 +172,48 @@ namespace Emby.Server.Implementations.Updates
var result = new List<PackageInfo>();
foreach (RepositoryInfo repository in _config.Configuration.PluginRepositories)
{
- result.AddRange(await GetPackages(repository.Url, cancellationToken).ConfigureAwait(true));
+ if (repository.Enabled && repository.Url != null)
+ {
+ // Where repositories have the same content, the details from the first is taken.
+ foreach (var package in await GetPackages(repository.Name ?? "Unnamed Repo", repository.Url, true, cancellationToken).ConfigureAwait(true))
+ {
+ var existing = FilterPackages(result, package.Name, package.Id).FirstOrDefault();
+
+ // Remove invalid versions from the valid package.
+ for (var i = package.Versions.Count - 1; i >= 0; i--)
+ {
+ var version = package.Versions[i];
+
+ var plugin = _pluginManager.GetPlugin(package.Id, version.VersionNumber);
+ if (plugin != null)
+ {
+ await _pluginManager.GenerateManifest(package, version.VersionNumber, plugin.Path, plugin.Manifest.Status).ConfigureAwait(false);
+ }
+
+ // Remove versions with a target ABI greater then the current application version.
+ if (Version.TryParse(version.TargetAbi, out var targetAbi) && _applicationHost.ApplicationVersion < targetAbi)
+ {
+ package.Versions.RemoveAt(i);
+ }
+ }
+
+ // Don't add a package that doesn't have any compatible versions.
+ if (package.Versions.Count == 0)
+ {
+ continue;
+ }
+
+ if (existing != null)
+ {
+ // Assumption is both lists are ordered, so slot these into the correct place.
+ MergeSortedList(existing.Versions, package.Versions);
+ }
+ else
+ {
+ result.Add(package);
+ }
+ }
+ }
}
return result;
@@ -170,17 +222,23 @@ namespace Emby.Server.Implementations.Updates
/// <inheritdoc />
public IEnumerable<PackageInfo> FilterPackages(
IEnumerable<PackageInfo> availablePackages,
- string name = null,
- Guid guid = default)
+ string? name = null,
+ Guid id = default,
+ Version? specificVersion = null)
{
if (name != null)
{
- availablePackages = availablePackages.Where(x => x.name.Equals(name, StringComparison.OrdinalIgnoreCase));
+ availablePackages = availablePackages.Where(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
+ }
+
+ if (id != default)
+ {
+ availablePackages = availablePackages.Where(x => x.Id == id);
}
- if (guid != Guid.Empty)
+ if (specificVersion != null)
{
- availablePackages = availablePackages.Where(x => Guid.Parse(x.guid) == guid);
+ availablePackages = availablePackages.Where(x => x.Versions.Any(y => y.VersionNumber.Equals(specificVersion)));
}
return availablePackages;
@@ -189,11 +247,12 @@ namespace Emby.Server.Implementations.Updates
/// <inheritdoc />
public IEnumerable<InstallationInfo> GetCompatibleVersions(
IEnumerable<PackageInfo> availablePackages,
- string name = null,
- Guid guid = default,
- Version minVersion = null)
+ string? name = null,
+ Guid id = default,
+ Version? minVersion = null,
+ Version? specificVersion = null)
{
- var package = FilterPackages(availablePackages, name, guid).FirstOrDefault();
+ var package = FilterPackages(availablePackages, name, id, specificVersion).FirstOrDefault();
// Package not found in repository
if (package == null)
@@ -202,24 +261,29 @@ namespace Emby.Server.Implementations.Updates
}
var appVer = _applicationHost.ApplicationVersion;
- var availableVersions = package.versions
- .Where(x => Version.Parse(x.targetAbi) <= appVer);
+ var availableVersions = package.Versions
+ .Where(x => string.IsNullOrEmpty(x.TargetAbi) || Version.Parse(x.TargetAbi) <= appVer);
- if (minVersion != null)
+ if (specificVersion != null)
{
- availableVersions = availableVersions.Where(x => new Version(x.version) >= minVersion);
+ availableVersions = availableVersions.Where(x => x.VersionNumber.Equals(specificVersion));
+ }
+ else if (minVersion != null)
+ {
+ availableVersions = availableVersions.Where(x => x.VersionNumber >= minVersion);
}
- foreach (var v in availableVersions.OrderByDescending(x => x.version))
+ foreach (var v in availableVersions.OrderByDescending(x => x.VersionNumber))
{
yield return new InstallationInfo
{
- Changelog = v.changelog,
- Guid = new Guid(package.guid),
- Name = package.name,
- Version = new Version(v.version),
- SourceUrl = v.sourceUrl,
- Checksum = v.checksum
+ Changelog = v.Changelog,
+ Id = package.Id,
+ Name = package.Name,
+ Version = v.VersionNumber,
+ SourceUrl = v.SourceUrl,
+ Checksum = v.Checksum,
+ PackageInfo = package
};
}
}
@@ -231,19 +295,6 @@ namespace Emby.Server.Implementations.Updates
return GetAvailablePluginUpdates(catalog);
}
- private IEnumerable<InstallationInfo> GetAvailablePluginUpdates(IReadOnlyList<PackageInfo> pluginCatalog)
- {
- foreach (var plugin in _applicationHost.Plugins)
- {
- var compatibleversions = GetCompatibleVersions(pluginCatalog, plugin.Name, plugin.Id, plugin.Version);
- var version = compatibleversions.FirstOrDefault(y => y.Version > plugin.Version);
- if (version != null && CompletedInstallations.All(x => x.Guid != version.Guid))
- {
- yield return version;
- }
- }
- }
-
/// <inheritdoc />
public async Task InstallPackage(InstallationInfo package, CancellationToken cancellationToken)
{
@@ -262,13 +313,14 @@ namespace Emby.Server.Implementations.Updates
_currentInstallations.Add(tuple);
}
- var linkedToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, innerCancellationTokenSource.Token).Token;
+ using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, innerCancellationTokenSource.Token);
+ var linkedToken = linkedTokenSource.Token;
- PackageInstalling?.Invoke(this, package);
+ await _eventManager.PublishAsync(new PluginInstallingEventArgs(package)).ConfigureAwait(false);
try
{
- await InstallPackageInternal(package, linkedToken).ConfigureAwait(false);
+ var isUpdate = await InstallPackageInternal(package, linkedToken).ConfigureAwait(false);
lock (_currentInstallationsLock)
{
@@ -276,8 +328,11 @@ namespace Emby.Server.Implementations.Updates
}
_completedInstallationsInternal.Add(package);
+ await _eventManager.PublishAsync(isUpdate
+ ? (GenericEventArgs<InstallationInfo>)new PluginUpdatedEventArgs(package)
+ : new PluginInstalledEventArgs(package)).ConfigureAwait(false);
- PackageInstallationCompleted?.Invoke(this, package);
+ _applicationHost.NotifyPendingRestart();
}
catch (OperationCanceledException)
{
@@ -288,7 +343,7 @@ namespace Emby.Server.Implementations.Updates
_logger.LogInformation("Package installation cancelled: {0} {1}", package.Name, package.Version);
- PackageInstallationCancelled?.Invoke(this, package);
+ await _eventManager.PublishAsync(new PluginInstallationCancelledEventArgs(package)).ConfigureAwait(false);
throw;
}
@@ -301,11 +356,11 @@ namespace Emby.Server.Implementations.Updates
_currentInstallations.Remove(tuple);
}
- PackageInstallationFailed?.Invoke(this, new InstallationFailedEventArgs
+ await _eventManager.PublishAsync(new InstallationFailedEventArgs
{
InstallationInfo = package,
Exception = ex
- });
+ }).ConfigureAwait(false);
throw;
}
@@ -317,144 +372,28 @@ namespace Emby.Server.Implementations.Updates
}
/// <summary>
- /// Installs the package internal.
+ /// Uninstalls a plugin.
/// </summary>
- /// <param name="package">The package.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns><see cref="Task" />.</returns>
- private async Task InstallPackageInternal(InstallationInfo package, CancellationToken cancellationToken)
+ /// <param name="plugin">The <see cref="LocalPlugin"/> to uninstall.</param>
+ public void UninstallPlugin(LocalPlugin plugin)
{
- // Set last update time if we were installed before
- IPlugin plugin = _applicationHost.Plugins.FirstOrDefault(p => p.Id == package.Guid)
- ?? _applicationHost.Plugins.FirstOrDefault(p => p.Name.Equals(package.Name, StringComparison.OrdinalIgnoreCase));
-
- // Do the install
- await PerformPackageInstallation(package, cancellationToken).ConfigureAwait(false);
-
- // Do plugin-specific processing
if (plugin == null)
{
- _logger.LogInformation("New plugin installed: {0} {1}", package.Name, package.Version);
-
- PluginInstalled?.Invoke(this, package);
- }
- else
- {
- _logger.LogInformation("Plugin updated: {0} {1}", package.Name, package.Version);
-
- PluginUpdated?.Invoke(this, package);
- }
-
- _applicationHost.NotifyPendingRestart();
- }
-
- private async Task PerformPackageInstallation(InstallationInfo package, CancellationToken cancellationToken)
- {
- var extension = Path.GetExtension(package.SourceUrl);
- if (!string.Equals(extension, ".zip", StringComparison.OrdinalIgnoreCase))
- {
- _logger.LogError("Only zip packages are supported. {SourceUrl} is not a zip archive.", package.SourceUrl);
return;
}
- // Always override the passed-in target (which is a file) and figure it out again
- string targetDir = Path.Combine(_appPaths.PluginsPath, package.Name);
-
- // CA5351: Do Not Use Broken Cryptographic Algorithms
-#pragma warning disable CA5351
- using (var res = await _httpClient.SendAsync(
- new HttpRequestOptions
- {
- Url = package.SourceUrl,
- CancellationToken = cancellationToken,
- // We need it to be buffered for setting the position
- BufferContent = true
- },
- HttpMethod.Get).ConfigureAwait(false))
- using (var stream = res.Content)
- using (var md5 = MD5.Create())
- {
- cancellationToken.ThrowIfCancellationRequested();
-
- var hash = Hex.Encode(md5.ComputeHash(stream));
- if (!string.Equals(package.Checksum, hash, StringComparison.OrdinalIgnoreCase))
- {
- _logger.LogError(
- "The checksums didn't match while installing {Package}, expected: {Expected}, got: {Received}",
- package.Name,
- package.Checksum,
- hash);
- throw new InvalidDataException("The checksum of the received data doesn't match.");
- }
-
- if (Directory.Exists(targetDir))
- {
- Directory.Delete(targetDir, true);
- }
-
- stream.Position = 0;
- _zipClient.ExtractAllFromZip(stream, targetDir, true);
- }
-
-#pragma warning restore CA5351
- }
-
- /// <summary>
- /// Uninstalls a plugin.
- /// </summary>
- /// <param name="plugin">The plugin.</param>
- public void UninstallPlugin(IPlugin plugin)
- {
- if (!plugin.CanUninstall)
+ if (plugin.Instance?.CanUninstall == false)
{
- _logger.LogWarning("Attempt to delete non removable plugin {0}, ignoring request", plugin.Name);
+ _logger.LogWarning("Attempt to delete non removable plugin {PluginName}, ignoring request", plugin.Name);
return;
}
- plugin.OnUninstalling();
+ plugin.Instance?.OnUninstalling();
// Remove it the quick way for now
- _applicationHost.RemovePlugin(plugin);
-
- var path = plugin.AssemblyFilePath;
- bool isDirectory = false;
- // Check if we have a plugin directory we should remove too
- if (Path.GetDirectoryName(plugin.AssemblyFilePath) != _appPaths.PluginsPath)
- {
- path = Path.GetDirectoryName(plugin.AssemblyFilePath);
- isDirectory = true;
- }
+ _pluginManager.RemovePlugin(plugin);
- // Make this case-insensitive to account for possible incorrect assembly naming
- var file = _fileSystem.GetFilePaths(Path.GetDirectoryName(path))
- .FirstOrDefault(i => string.Equals(i, path, StringComparison.OrdinalIgnoreCase));
-
- if (!string.IsNullOrWhiteSpace(file))
- {
- path = file;
- }
-
- if (isDirectory)
- {
- _logger.LogInformation("Deleting plugin directory {0}", path);
- Directory.Delete(path, true);
- }
- else
- {
- _logger.LogInformation("Deleting plugin file {0}", path);
- _fileSystem.DeleteFile(path);
- }
-
- var list = _config.Configuration.UninstalledPlugins.ToList();
- var filename = Path.GetFileName(path);
- if (!list.Contains(filename, StringComparer.OrdinalIgnoreCase))
- {
- list.Add(filename);
- _config.Configuration.UninstalledPlugins = list.ToArray();
- _config.SaveConfiguration();
- }
-
- PluginUninstalled?.Invoke(this, plugin);
+ _eventManager.Publish(new PluginUninstalledEventArgs(plugin.GetPluginInfo()));
_applicationHost.NotifyPendingRestart();
}
@@ -464,7 +403,7 @@ namespace Emby.Server.Implementations.Updates
{
lock (_currentInstallationsLock)
{
- var install = _currentInstallations.Find(x => x.info.Guid == id);
+ var install = _currentInstallations.Find(x => x.info.Id == id);
if (install == default((InstallationInfo, CancellationTokenSource)))
{
return false;
@@ -493,14 +432,148 @@ namespace Emby.Server.Implementations.Updates
{
lock (_currentInstallationsLock)
{
- foreach (var tuple in _currentInstallations)
+ foreach (var (info, token) in _currentInstallations)
{
- tuple.token.Dispose();
+ token.Dispose();
}
_currentInstallations.Clear();
}
}
}
+
+ /// <summary>
+ /// Merges two sorted lists.
+ /// </summary>
+ /// <param name="source">The source <see cref="IList{VersionInfo}"/> instance to merge.</param>
+ /// <param name="dest">The destination <see cref="IList{VersionInfo}"/> instance to merge with.</param>
+ private static void MergeSortedList(IList<VersionInfo> source, IList<VersionInfo> dest)
+ {
+ int sLength = source.Count - 1;
+ int dLength = dest.Count;
+ int s = 0, d = 0;
+ var sourceVersion = source[0].VersionNumber;
+ var destVersion = dest[0].VersionNumber;
+
+ while (d < dLength)
+ {
+ if (sourceVersion.CompareTo(destVersion) >= 0)
+ {
+ if (s < sLength)
+ {
+ sourceVersion = source[++s].VersionNumber;
+ }
+ else
+ {
+ // Append all of destination to the end of source.
+ while (d < dLength)
+ {
+ source.Add(dest[d++]);
+ }
+
+ break;
+ }
+ }
+ else
+ {
+ source.Insert(s++, dest[d++]);
+ if (d >= dLength)
+ {
+ break;
+ }
+
+ sLength++;
+ destVersion = dest[d].VersionNumber;
+ }
+ }
+ }
+
+ private IEnumerable<InstallationInfo> GetAvailablePluginUpdates(IReadOnlyList<PackageInfo> pluginCatalog)
+ {
+ var plugins = _pluginManager.Plugins;
+ foreach (var plugin in plugins)
+ {
+ // Don't auto update when plugin marked not to, or when it's disabled.
+ if (plugin.Manifest?.AutoUpdate == false || plugin.Manifest?.Status == PluginStatus.Disabled)
+ {
+ continue;
+ }
+
+ var compatibleVersions = GetCompatibleVersions(pluginCatalog, plugin.Name, plugin.Id, minVersion: plugin.Version);
+ var version = compatibleVersions.FirstOrDefault(y => y.Version > plugin.Version);
+
+ if (version != null && CompletedInstallations.All(x => x.Id != version.Id))
+ {
+ yield return version;
+ }
+ }
+ }
+
+ private async Task PerformPackageInstallation(InstallationInfo package, PluginStatus status, CancellationToken cancellationToken)
+ {
+ var extension = Path.GetExtension(package.SourceUrl);
+ if (!string.Equals(extension, ".zip", StringComparison.OrdinalIgnoreCase))
+ {
+ _logger.LogError("Only zip packages are supported. {SourceUrl} is not a zip archive.", package.SourceUrl);
+ return;
+ }
+
+ // Always override the passed-in target (which is a file) and figure it out again
+ string targetDir = Path.Combine(_appPaths.PluginsPath, package.Name);
+
+ using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
+ .GetAsync(new Uri(package.SourceUrl), cancellationToken).ConfigureAwait(false);
+ response.EnsureSuccessStatusCode();
+ await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
+
+ // CA5351: Do Not Use Broken Cryptographic Algorithms
+#pragma warning disable CA5351
+ using var md5 = MD5.Create();
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var hash = Convert.ToHexString(md5.ComputeHash(stream));
+ if (!string.Equals(package.Checksum, hash, StringComparison.OrdinalIgnoreCase))
+ {
+ _logger.LogError(
+ "The checksums didn't match while installing {Package}, expected: {Expected}, got: {Received}",
+ package.Name,
+ package.Checksum,
+ hash);
+ throw new InvalidDataException("The checksum of the received data doesn't match.");
+ }
+
+ // Version folder as they cannot be overwritten in Windows.
+ targetDir += "_" + package.Version;
+
+ if (Directory.Exists(targetDir))
+ {
+ try
+ {
+ Directory.Delete(targetDir, true);
+ }
+#pragma warning disable CA1031 // Do not catch general exception types
+ catch
+#pragma warning restore CA1031 // Do not catch general exception types
+ {
+ // Ignore any exceptions.
+ }
+ }
+
+ stream.Position = 0;
+ _zipClient.ExtractAllFromZip(stream, targetDir, true);
+ await _pluginManager.GenerateManifest(package.PackageInfo, package.Version, targetDir, status).ConfigureAwait(false);
+ _pluginManager.ImportPluginFrom(targetDir);
+ }
+
+ private async Task<bool> InstallPackageInternal(InstallationInfo package, CancellationToken cancellationToken)
+ {
+ LocalPlugin? plugin = _pluginManager.Plugins.FirstOrDefault(p => p.Id.Equals(package.Id) && p.Version.Equals(package.Version))
+ ?? _pluginManager.Plugins.FirstOrDefault(p => p.Name.Equals(package.Name, StringComparison.OrdinalIgnoreCase) && p.Version.Equals(package.Version));
+
+ await PerformPackageInstallation(package, plugin?.Manifest.Status ?? PluginStatus.Active, cancellationToken).ConfigureAwait(false);
+ _logger.LogInformation(plugin == null ? "New plugin installed: {PluginName} {PluginVersion}" : "Plugin updated: {PluginName} {PluginVersion}", package.Name, package.Version);
+
+ return plugin != null;
+ }
}
}