< Summary - Jellyfin

Information
Class: Emby.Server.Implementations.Updates.InstallationManager
Assembly: Emby.Server.Implementations
File(s): /srv/git/jellyfin/Emby.Server.Implementations/Updates/InstallationManager.cs
Line coverage
58%
Covered lines: 142
Uncovered lines: 100
Coverable lines: 242
Total lines: 627
Line coverage: 58.6%
Branch coverage
58%
Covered branches: 70
Total branches: 120
Branch coverage: 58.3%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 3/26/2026 - 12:14:14 AM Line coverage: 41.5% (27/65) Branch coverage: 25% (8/32) Total lines: 5844/19/2026 - 12:14:27 AM Line coverage: 57.4% (127/221) Branch coverage: 55.5% (60/108) Total lines: 5845/20/2026 - 12:15:44 AM Line coverage: 57.4% (127/221) Branch coverage: 54.6% (59/108) Total lines: 5845/30/2026 - 12:15:32 AM Line coverage: 57.6% (128/222) Branch coverage: 54.6% (59/108) Total lines: 5866/28/2026 - 12:15:35 AM Line coverage: 58.6% (142/242) Branch coverage: 58.3% (70/120) Total lines: 627 3/26/2026 - 12:14:14 AM Line coverage: 41.5% (27/65) Branch coverage: 25% (8/32) Total lines: 5844/19/2026 - 12:14:27 AM Line coverage: 57.4% (127/221) Branch coverage: 55.5% (60/108) Total lines: 5845/20/2026 - 12:15:44 AM Line coverage: 57.4% (127/221) Branch coverage: 54.6% (59/108) Total lines: 5845/30/2026 - 12:15:32 AM Line coverage: 57.6% (128/222) Branch coverage: 54.6% (59/108) Total lines: 5866/28/2026 - 12:15:35 AM Line coverage: 58.6% (142/242) Branch coverage: 58.3% (70/120) Total lines: 627

Coverage delta

Coverage delta 31 -31

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
.ctor(...)100%11100%
get_CompletedInstallations()100%210%
GetPackages()75%381243.75%
GetAvailablePackages()81.81%242285%
FilterPackages(...)66.66%6685.71%
GetCompatibleVersions()12.5%49813.63%
GetAvailablePluginUpdates()100%11100%
InstallPackage()75%4475.67%
UninstallPlugin(...)0%7280%
CancelInstallation(...)0%2040%
Dispose()100%11100%
Dispose(...)75%4485.71%
MergeSortedList(...)0%110100%
GetAvailablePluginUpdates()71.42%141487.5%
PerformPackageInstallation()71.42%191470.73%
IsValidPackageDirectoryName(...)100%88100%
InstallPackageInternal()66.66%66100%

File(s)

/srv/git/jellyfin/Emby.Server.Implementations/Updates/InstallationManager.cs

#LineLine coverage
 1using System;
 2using System.Buffers;
 3using System.Collections.Concurrent;
 4using System.Collections.Generic;
 5using System.IO;
 6using System.IO.Compression;
 7using System.Linq;
 8using System.Net.Http;
 9using System.Net.Http.Json;
 10using System.Security.Cryptography;
 11using System.Text.Json;
 12using System.Threading;
 13using System.Threading.Tasks;
 14using Jellyfin.Data.Events;
 15using Jellyfin.Extensions;
 16using Jellyfin.Extensions.Json;
 17using MediaBrowser.Common.Configuration;
 18using MediaBrowser.Common.Net;
 19using MediaBrowser.Common.Plugins;
 20using MediaBrowser.Common.Updates;
 21using MediaBrowser.Controller;
 22using MediaBrowser.Controller.Configuration;
 23using MediaBrowser.Controller.Events;
 24using MediaBrowser.Controller.Events.Updates;
 25using MediaBrowser.Model.Plugins;
 26using MediaBrowser.Model.Updates;
 27using Microsoft.Extensions.Logging;
 28
 29namespace Emby.Server.Implementations.Updates
 30{
 31    /// <summary>
 32    /// Manages all install, uninstall, and update operations for the system and individual plugins.
 33    /// </summary>
 34    public class InstallationManager : IInstallationManager
 35    {
 136        private static readonly SearchValues<char> InvalidPackageNameChars = SearchValues.Create([.. Path.GetInvalidFile
 37
 38        /// <summary>
 39        /// The logger.
 40        /// </summary>
 41        private readonly ILogger<InstallationManager> _logger;
 42        private readonly IApplicationPaths _appPaths;
 43        private readonly IEventManager _eventManager;
 44        private readonly IHttpClientFactory _httpClientFactory;
 45        private readonly IServerConfigurationManager _config;
 46        private readonly JsonSerializerOptions _jsonSerializerOptions;
 47        private readonly IPluginManager _pluginManager;
 48
 49        /// <summary>
 50        /// Gets the application host.
 51        /// </summary>
 52        /// <value>The application host.</value>
 53        private readonly IServerApplicationHost _applicationHost;
 3854        private readonly Lock _currentInstallationsLock = new();
 55
 56        /// <summary>
 57        /// The current installations.
 58        /// </summary>
 59        private readonly List<(InstallationInfo Info, CancellationTokenSource Token)> _currentInstallations;
 60
 61        /// <summary>
 62        /// The completed installations.
 63        /// </summary>
 64        private readonly ConcurrentBag<InstallationInfo> _completedInstallationsInternal;
 65
 66        /// <summary>
 67        /// Initializes a new instance of the <see cref="InstallationManager"/> class.
 68        /// </summary>
 69        /// <param name="logger">The <see cref="ILogger{InstallationManager}"/>.</param>
 70        /// <param name="appHost">The <see cref="IServerApplicationHost"/>.</param>
 71        /// <param name="appPaths">The <see cref="IApplicationPaths"/>.</param>
 72        /// <param name="eventManager">The <see cref="IEventManager"/>.</param>
 73        /// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/>.</param>
 74        /// <param name="config">The <see cref="IServerConfigurationManager"/>.</param>
 75        /// <param name="pluginManager">The <see cref="IPluginManager"/>.</param>
 76        public InstallationManager(
 77            ILogger<InstallationManager> logger,
 78            IServerApplicationHost appHost,
 79            IApplicationPaths appPaths,
 80            IEventManager eventManager,
 81            IHttpClientFactory httpClientFactory,
 82            IServerConfigurationManager config,
 83            IPluginManager pluginManager)
 84        {
 3885            _currentInstallations = new List<(InstallationInfo, CancellationTokenSource)>();
 3886            _completedInstallationsInternal = new ConcurrentBag<InstallationInfo>();
 87
 3888            _logger = logger;
 3889            _applicationHost = appHost;
 3890            _appPaths = appPaths;
 3891            _eventManager = eventManager;
 3892            _httpClientFactory = httpClientFactory;
 3893            _config = config;
 3894            _jsonSerializerOptions = JsonDefaults.Options;
 3895            _pluginManager = pluginManager;
 3896        }
 97
 98        /// <inheritdoc />
 099        public IEnumerable<InstallationInfo> CompletedInstallations => _completedInstallationsInternal;
 100
 101        /// <inheritdoc />
 102        public async Task<PackageInfo[]> GetPackages(string manifestName, string manifest, bool filterIncompatible, Canc
 103        {
 104            try
 105            {
 7106                PackageInfo[]? packages = await _httpClientFactory.CreateClient(NamedClient.Default)
 7107                        .GetFromJsonAsync<PackageInfo[]>(new Uri(manifest), _jsonSerializerOptions, cancellationToken).C
 108
 5109                if (packages is null)
 110                {
 0111                    return Array.Empty<PackageInfo>();
 112                }
 113
 5114                var minimumVersion = new Version(0, 0, 0, 1);
 115                // Store the repository and repository url with each version, as they may be spread apart.
 296116                foreach (var entry in packages)
 117                {
 1718118                    for (int a = entry.Versions.Count - 1; a >= 0; a--)
 119                    {
 716120                        var ver = entry.Versions[a];
 716121                        ver.RepositoryName = manifestName;
 716122                        ver.RepositoryUrl = manifest;
 123
 716124                        if (!filterIncompatible)
 125                        {
 126                            continue;
 127                        }
 128
 554129                        if (!Version.TryParse(ver.TargetAbi, out var targetAbi))
 130                        {
 0131                            targetAbi = minimumVersion;
 132                        }
 133
 134                        // Only show plugins that are greater than or equal to targetAbi.
 554135                        if (_applicationHost.ApplicationVersion >= targetAbi)
 136                        {
 137                            continue;
 138                        }
 139
 140                        // Not compatible with this version so remove it.
 0141                        entry.Versions.Remove(ver);
 142                    }
 143                }
 144
 5145                return packages;
 146            }
 0147            catch (IOException ex)
 148            {
 0149                _logger.LogError(ex, "Cannot locate the plugin manifest {Manifest}", manifest);
 0150                return Array.Empty<PackageInfo>();
 151            }
 0152            catch (JsonException ex)
 153            {
 0154                _logger.LogError(ex, "Failed to deserialize the plugin manifest retrieved from {Manifest}", manifest);
 0155                return Array.Empty<PackageInfo>();
 156            }
 0157            catch (UriFormatException ex)
 158            {
 0159                _logger.LogError(ex, "The URL configured for the plugin repository manifest URL is not valid: {Manifest}
 0160                return Array.Empty<PackageInfo>();
 161            }
 0162            catch (NotSupportedException ex)
 163            {
 0164                _logger.LogError(ex, "The URL scheme configured for the plugin repository is not supported: {Manifest}",
 0165                return Array.Empty<PackageInfo>();
 166            }
 0167            catch (HttpRequestException ex)
 168            {
 0169                _logger.LogError(ex, "An error occurred while accessing the plugin manifest: {Manifest}", manifest);
 0170                return Array.Empty<PackageInfo>();
 171            }
 5172        }
 173
 174        /// <inheritdoc />
 175        public async Task<IReadOnlyList<PackageInfo>> GetAvailablePackages(CancellationToken cancellationToken = default
 176        {
 4177            var result = new List<PackageInfo>();
 14178            foreach (RepositoryInfo repository in _config.Configuration.PluginRepositories)
 179            {
 4180                if (repository.Enabled && repository.Url is not null)
 181                {
 182                    // Where repositories have the same content, the details from the first is taken.
 142183                    foreach (var package in await GetPackages(repository.Name ?? "Unnamed Repo", repository.Url, true, c
 184                    {
 68185                        var existing = FilterPackages(result, package.Name, package.Id).FirstOrDefault();
 186
 187                        // Remove invalid versions from the valid package.
 1244188                        for (var i = package.Versions.Count - 1; i >= 0; i--)
 189                        {
 554190                            var version = package.Versions[i];
 191
 554192                            var plugin = _pluginManager.GetPlugin(package.Id, version.VersionNumber);
 554193                            if (plugin is not null)
 194                            {
 0195                                await _pluginManager.PopulateManifest(package, version.VersionNumber, plugin.Path, plugi
 196                            }
 197
 198                            // Remove versions with a target ABI greater than the current application version.
 554199                            if (Version.TryParse(version.TargetAbi, out var targetAbi) && _applicationHost.ApplicationVe
 200                            {
 0201                                package.Versions.RemoveAt(i);
 202                            }
 554203                        }
 204
 205                        // Don't add a package that doesn't have any compatible versions.
 68206                        if (package.Versions.Count == 0)
 207                        {
 208                            continue;
 209                        }
 210
 68211                        if (existing is not null)
 212                        {
 213                            // Assumption is both lists are ordered, so slot these into the correct place.
 0214                            MergeSortedList(existing.Versions, package.Versions);
 215                        }
 216                        else
 217                        {
 68218                            result.Add(package);
 219                        }
 68220                    }
 221                }
 222            }
 223
 2224            return result;
 2225        }
 226
 227        /// <inheritdoc />
 228        public IEnumerable<PackageInfo> FilterPackages(
 229            IEnumerable<PackageInfo> availablePackages,
 230            string? name = null,
 231            Guid id = default,
 232            Version? specificVersion = null)
 233        {
 86234            if (!id.IsEmpty())
 235            {
 85236                availablePackages = availablePackages.Where(x => x.Id.Equals(id));
 237            }
 1238            else if (name is not null)
 239            {
 1240                availablePackages = availablePackages.Where(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase)
 241            }
 242
 86243            if (specificVersion is not null)
 244            {
 0245                availablePackages = availablePackages.Where(x => x.Versions.Any(y => y.VersionNumber.Equals(specificVers
 246            }
 247
 86248            return availablePackages;
 249        }
 250
 251        /// <inheritdoc />
 252        public IEnumerable<InstallationInfo> GetCompatibleVersions(
 253            IEnumerable<PackageInfo> availablePackages,
 254            string? name = null,
 255            Guid id = default,
 256            Version? minVersion = null,
 257            Version? specificVersion = null)
 258        {
 16259            var package = FilterPackages(availablePackages, name, id, specificVersion).FirstOrDefault();
 260
 261            // Package not found in repository
 16262            if (package is null)
 263            {
 16264                yield break;
 265            }
 266
 0267            var appVer = _applicationHost.ApplicationVersion;
 0268            var availableVersions = package.Versions
 0269                .Where(x => string.IsNullOrEmpty(x.TargetAbi) || Version.Parse(x.TargetAbi) <= appVer);
 270
 0271            if (specificVersion is not null)
 272            {
 0273                availableVersions = availableVersions.Where(x => x.VersionNumber.Equals(specificVersion));
 274            }
 0275            else if (minVersion is not null)
 276            {
 0277                availableVersions = availableVersions.Where(x => x.VersionNumber >= minVersion);
 278            }
 279
 0280            foreach (var v in availableVersions.OrderByDescending(x => x.VersionNumber))
 281            {
 0282                yield return new InstallationInfo
 0283                {
 0284                    Changelog = v.Changelog,
 0285                    Id = package.Id,
 0286                    Name = package.Name,
 0287                    Version = v.VersionNumber,
 0288                    SourceUrl = v.SourceUrl,
 0289                    Checksum = v.Checksum,
 0290                    PackageInfo = package
 0291                };
 292            }
 0293        }
 294
 295        /// <inheritdoc />
 296        public async Task<IEnumerable<InstallationInfo>> GetAvailablePluginUpdates(CancellationToken cancellationToken =
 297        {
 4298            var catalog = await GetAvailablePackages(cancellationToken).ConfigureAwait(false);
 2299            return GetAvailablePluginUpdates(catalog);
 2300        }
 301
 302        /// <inheritdoc />
 303        public async Task InstallPackage(InstallationInfo package, CancellationToken cancellationToken)
 304        {
 13305            ArgumentNullException.ThrowIfNull(package);
 306
 13307            var innerCancellationTokenSource = new CancellationTokenSource();
 308
 13309            var tuple = (package, innerCancellationTokenSource);
 310
 311            // Add it to the in-progress list
 312            lock (_currentInstallationsLock)
 313            {
 13314                _currentInstallations.Add(tuple);
 13315            }
 316
 13317            using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, innerCancel
 13318            var linkedToken = linkedTokenSource.Token;
 319
 13320            await _eventManager.PublishAsync(new PluginInstallingEventArgs(package)).ConfigureAwait(false);
 321
 322            try
 323            {
 13324                var isUpdate = await InstallPackageInternal(package, linkedToken).ConfigureAwait(false);
 325
 326                lock (_currentInstallationsLock)
 327                {
 1328                    _currentInstallations.Remove(tuple);
 1329                }
 330
 1331                _completedInstallationsInternal.Add(package);
 332
 1333                if (isUpdate)
 334                {
 0335                    await _eventManager.PublishAsync(new PluginUpdatedEventArgs(package)).ConfigureAwait(false);
 336                }
 337                else
 338                {
 1339                    await _eventManager.PublishAsync(new PluginInstalledEventArgs(package)).ConfigureAwait(false);
 340                }
 341
 1342                _applicationHost.NotifyPendingRestart();
 1343            }
 0344            catch (OperationCanceledException)
 0345            {
 346                lock (_currentInstallationsLock)
 347                {
 0348                    _currentInstallations.Remove(tuple);
 0349                }
 350
 0351                _logger.LogInformation("Package installation cancelled: {0} {1}", package.Name, package.Version);
 352
 0353                await _eventManager.PublishAsync(new PluginInstallationCancelledEventArgs(package)).ConfigureAwait(false
 354
 0355                throw;
 0356            }
 12357            catch (Exception ex)
 358            {
 12359                _logger.LogError(ex, "Package installation failed");
 360
 361                lock (_currentInstallationsLock)
 362                {
 12363                    _currentInstallations.Remove(tuple);
 12364                }
 365
 12366                await _eventManager.PublishAsync(new InstallationFailedEventArgs
 12367                {
 12368                    InstallationInfo = package,
 12369                    Exception = ex
 12370                }).ConfigureAwait(false);
 371
 12372                throw;
 373            }
 374            finally
 375            {
 376                // Dispose the progress object and remove the installation from the in-progress list
 13377                tuple.innerCancellationTokenSource.Dispose();
 378            }
 1379        }
 380
 381        /// <summary>
 382        /// Uninstalls a plugin.
 383        /// </summary>
 384        /// <param name="plugin">The <see cref="LocalPlugin"/> to uninstall.</param>
 385        public void UninstallPlugin(LocalPlugin plugin)
 386        {
 0387            if (plugin is null)
 388            {
 0389                return;
 390            }
 391
 0392            if (plugin.Instance?.CanUninstall == false)
 393            {
 0394                _logger.LogWarning("Attempt to delete non removable plugin {PluginName}, ignoring request", plugin.Name)
 0395                return;
 396            }
 397
 0398            plugin.Instance?.OnUninstalling();
 399
 400            // Remove it the quick way for now
 0401            _pluginManager.RemovePlugin(plugin);
 402
 0403            _eventManager.Publish(new PluginUninstalledEventArgs(plugin.GetPluginInfo()));
 404
 0405            _applicationHost.NotifyPendingRestart();
 0406        }
 407
 408        /// <inheritdoc/>
 409        public bool CancelInstallation(Guid id)
 0410        {
 411            lock (_currentInstallationsLock)
 412            {
 0413                var install = _currentInstallations.Find(x => x.Info.Id.Equals(id));
 0414                if (install == default((InstallationInfo, CancellationTokenSource)))
 415                {
 0416                    return false;
 417                }
 418
 0419                install.Token.Cancel();
 0420                _currentInstallations.Remove(install);
 0421                return true;
 422            }
 0423        }
 424
 425        /// <inheritdoc />
 426        public void Dispose()
 427        {
 22428            Dispose(true);
 22429            GC.SuppressFinalize(this);
 22430        }
 431
 432        /// <summary>
 433        /// Releases unmanaged and optionally managed resources.
 434        /// </summary>
 435        /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources or <c>false</c> to release
 436        protected virtual void Dispose(bool dispose)
 437        {
 22438            if (dispose)
 22439            {
 440                lock (_currentInstallationsLock)
 441                {
 44442                    foreach (var (info, token) in _currentInstallations)
 443                    {
 0444                        token.Dispose();
 445                    }
 446
 22447                    _currentInstallations.Clear();
 22448                }
 449            }
 22450        }
 451
 452        /// <summary>
 453        /// Merges two sorted lists.
 454        /// </summary>
 455        /// <param name="source">The source <see cref="IList{VersionInfo}"/> instance to merge.</param>
 456        /// <param name="dest">The destination <see cref="IList{VersionInfo}"/> instance to merge with.</param>
 457        private static void MergeSortedList(IList<VersionInfo> source, IList<VersionInfo> dest)
 458        {
 0459            int sLength = source.Count - 1;
 0460            int dLength = dest.Count;
 0461            int s = 0, d = 0;
 0462            var sourceVersion = source[0].VersionNumber;
 0463            var destVersion = dest[0].VersionNumber;
 464
 0465            while (d < dLength)
 466            {
 0467                if (sourceVersion.CompareTo(destVersion) >= 0)
 468                {
 0469                    if (s < sLength)
 470                    {
 0471                        sourceVersion = source[++s].VersionNumber;
 472                    }
 473                    else
 474                    {
 475                        // Append all of destination to the end of source.
 0476                        while (d < dLength)
 477                        {
 0478                            source.Add(dest[d++]);
 479                        }
 480
 0481                        break;
 482                    }
 483                }
 484                else
 485                {
 0486                    source.Insert(s++, dest[d++]);
 0487                    if (d >= dLength)
 488                    {
 489                        break;
 490                    }
 491
 0492                    sLength++;
 0493                    destVersion = dest[d].VersionNumber;
 494                }
 495            }
 0496        }
 497
 498        private IEnumerable<InstallationInfo> GetAvailablePluginUpdates(IReadOnlyList<PackageInfo> pluginCatalog)
 499        {
 2500            var plugins = _pluginManager.Plugins;
 36501            foreach (var plugin in plugins)
 502            {
 503                // Don't auto update when plugin marked not to, or when it's disabled.
 16504                if (plugin.Manifest?.AutoUpdate == false || plugin.Manifest?.Status == PluginStatus.Disabled)
 505                {
 506                    continue;
 507                }
 508
 16509                var compatibleVersions = GetCompatibleVersions(pluginCatalog, plugin.Name, plugin.Id, minVersion: plugin
 16510                var version = compatibleVersions.FirstOrDefault(y => y.Version > plugin.Version);
 511
 16512                if (version is not null && CompletedInstallations.All(x => !x.Id.Equals(version.Id)))
 513                {
 0514                    yield return version;
 515                }
 516            }
 2517        }
 518
 519        private async Task PerformPackageInstallation(InstallationInfo package, PluginStatus status, CancellationToken c
 520        {
 13521            if (!Path.GetExtension(package.SourceUrl.AsSpan()).Equals(".zip", StringComparison.OrdinalIgnoreCase))
 522            {
 0523                _logger.LogError("Only zip packages are supported. {SourceUrl} is not a zip archive.", package.SourceUrl
 0524                return;
 525            }
 526
 13527            if (!IsValidPackageDirectoryName(package.Name))
 528            {
 11529                _logger.LogError("Refusing to install package with invalid name {PackageName}.", package.Name);
 11530                throw new InvalidDataException($"Plugin package name '{package.Name}' is not a valid directory name.");
 531            }
 532
 533            // Always override the passed-in target (which is a file) and figure it out again
 2534            string targetDir = Path.Combine(_appPaths.PluginsPath, package.Name);
 535
 2536            var pluginsRoot = Path.TrimEndingDirectorySeparator(Path.GetFullPath(_appPaths.PluginsPath));
 2537            var resolvedTarget = Path.GetFullPath(targetDir);
 2538            if (!resolvedTarget.StartsWith(pluginsRoot + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase
 539            {
 0540                _logger.LogError(
 0541                    "Refusing to install package {PackageName}: resolved target {Resolved} is outside plugins directory 
 0542                    package.Name,
 0543                    resolvedTarget,
 0544                    pluginsRoot);
 0545                throw new InvalidDataException($"Plugin package name '{package.Name}' resolves outside the plugins direc
 546            }
 547
 2548            using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
 2549                .GetAsync(new Uri(package.SourceUrl), cancellationToken).ConfigureAwait(false);
 2550            response.EnsureSuccessStatusCode();
 2551            Stream stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
 2552            await using (stream.ConfigureAwait(false))
 553            {
 554                // CA5351: Do Not Use Broken Cryptographic Algorithms
 555#pragma warning disable CA5351
 2556                cancellationToken.ThrowIfCancellationRequested();
 557
 2558                var hash = Convert.ToHexString(await MD5.HashDataAsync(stream, cancellationToken).ConfigureAwait(false))
 2559                if (!string.Equals(package.Checksum, hash, StringComparison.OrdinalIgnoreCase))
 560                {
 1561                    _logger.LogError(
 1562                        "The checksums didn't match while installing {Package}, expected: {Expected}, got: {Received}",
 1563                        package.Name,
 1564                        package.Checksum,
 1565                        hash);
 1566                    throw new InvalidDataException("The checksum of the received data doesn't match.");
 567                }
 568
 569                // Version folder as they cannot be overwritten in Windows.
 1570                targetDir += "_" + package.Version;
 571
 1572                if (Directory.Exists(targetDir))
 573                {
 574                    try
 575                    {
 0576                        Directory.Delete(targetDir, true);
 0577                    }
 578#pragma warning disable CA1031 // Do not catch general exception types
 0579                    catch
 580#pragma warning restore CA1031 // Do not catch general exception types
 581                    {
 582                        // Ignore any exceptions.
 0583                    }
 584                }
 585
 1586                stream.Position = 0;
 1587                await ZipFile.ExtractToDirectoryAsync(stream, targetDir, true, cancellationToken).ConfigureAwait(false);
 588            }
 589
 590            // Ensure we create one or populate existing ones with missing data.
 1591            await _pluginManager.PopulateManifest(package.PackageInfo, package.Version, targetDir, status).ConfigureAwai
 592
 1593            _pluginManager.ImportPluginFrom(targetDir);
 1594        }
 595
 596        private static bool IsValidPackageDirectoryName(string? name)
 597        {
 13598            if (string.IsNullOrWhiteSpace(name))
 599            {
 2600                return false;
 601            }
 602
 11603            if (name.Equals(".", StringComparison.Ordinal) || name.Equals("..", StringComparison.Ordinal))
 604            {
 2605                return false;
 606            }
 607
 9608            if (name.IndexOfAny(InvalidPackageNameChars) >= 0)
 609            {
 7610                return false;
 611            }
 612
 2613            return true;
 614        }
 615
 616        private async Task<bool> InstallPackageInternal(InstallationInfo package, CancellationToken cancellationToken)
 617        {
 13618            LocalPlugin? plugin = _pluginManager.Plugins.FirstOrDefault(p => p.Id.Equals(package.Id) && p.Version.Equals
 13619                  ?? _pluginManager.Plugins.FirstOrDefault(p => p.Name.Equals(package.Name, StringComparison.OrdinalIgno
 620
 13621            await PerformPackageInstallation(package, plugin?.Manifest.Status ?? PluginStatus.Active, cancellationToken)
 1622            _logger.LogInformation("Plugin {Action}: {PluginName} {PluginVersion}", plugin is null ? "installed" : "upda
 623
 1624            return plugin is not null;
 1625        }
 626    }
 627}