< 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
59%
Covered lines: 152
Uncovered lines: 104
Coverable lines: 256
Total lines: 655
Line coverage: 59.3%
Branch coverage
58%
Covered branches: 72
Total branches: 124
Branch coverage: 58%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 5/16/2026 - 12:15:55 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: 6278/9/2026 - 12:16:58 AM Line coverage: 58.8% (143/243) Branch coverage: 58.7% (74/126) Total lines: 62810/29/2026 - 12:17:11 AM Line coverage: 59.3% (152/256) Branch coverage: 58% (72/124) Total lines: 655 5/16/2026 - 12:15:55 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: 6278/9/2026 - 12:16:58 AM Line coverage: 58.8% (143/243) Branch coverage: 58.7% (74/126) Total lines: 62810/29/2026 - 12:17:11 AM Line coverage: 59.3% (152/256) Branch coverage: 58% (72/124) Total lines: 655

Coverage delta

Coverage delta 4 -4

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()70%212088.88%
PerformPackageInstallation()66.66%161269.81%
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.Extensions;
 15using Jellyfin.Extensions.Json;
 16using MediaBrowser.Common.Configuration;
 17using MediaBrowser.Common.Net;
 18using MediaBrowser.Common.Plugins;
 19using MediaBrowser.Common.Updates;
 20using MediaBrowser.Controller;
 21using MediaBrowser.Controller.Configuration;
 22using MediaBrowser.Controller.Events;
 23using MediaBrowser.Controller.Events.Updates;
 24using MediaBrowser.Model.Plugins;
 25using MediaBrowser.Model.Updates;
 26using Microsoft.Extensions.Logging;
 27
 28namespace Emby.Server.Implementations.Updates
 29{
 30    /// <summary>
 31    /// Manages all install, uninstall, and update operations for the system and individual plugins.
 32    /// </summary>
 33    public class InstallationManager : IInstallationManager
 34    {
 135        private static readonly SearchValues<char> InvalidPackageNameChars = SearchValues.Create([.. Path.GetInvalidFile
 36        // Budget for the whole package download. The response headers are already bounded by the
 37        // HttpClient timeout; this covers reading the package body, which can be large and slow.
 138        private static readonly TimeSpan PackageDownloadTimeout = TimeSpan.FromMinutes(10);
 39
 40        /// <summary>
 41        /// The logger.
 42        /// </summary>
 43        private readonly ILogger<InstallationManager> _logger;
 44        private readonly IApplicationPaths _appPaths;
 45        private readonly IEventManager _eventManager;
 46        private readonly IHttpClientFactory _httpClientFactory;
 47        private readonly IServerConfigurationManager _config;
 48        private readonly JsonSerializerOptions _jsonSerializerOptions;
 49        private readonly IPluginManager _pluginManager;
 50
 51        /// <summary>
 52        /// Gets the application host.
 53        /// </summary>
 54        /// <value>The application host.</value>
 55        private readonly IServerApplicationHost _applicationHost;
 4356        private readonly Lock _currentInstallationsLock = new();
 57
 58        /// <summary>
 59        /// The current installations.
 60        /// </summary>
 61        private readonly List<(InstallationInfo Info, CancellationTokenSource Token)> _currentInstallations;
 62
 63        /// <summary>
 64        /// The completed installations.
 65        /// </summary>
 66        private readonly ConcurrentBag<InstallationInfo> _completedInstallationsInternal;
 67
 68        /// <summary>
 69        /// Initializes a new instance of the <see cref="InstallationManager"/> class.
 70        /// </summary>
 71        /// <param name="logger">The <see cref="ILogger{InstallationManager}"/>.</param>
 72        /// <param name="appHost">The <see cref="IServerApplicationHost"/>.</param>
 73        /// <param name="appPaths">The <see cref="IApplicationPaths"/>.</param>
 74        /// <param name="eventManager">The <see cref="IEventManager"/>.</param>
 75        /// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/>.</param>
 76        /// <param name="config">The <see cref="IServerConfigurationManager"/>.</param>
 77        /// <param name="pluginManager">The <see cref="IPluginManager"/>.</param>
 78        public InstallationManager(
 79            ILogger<InstallationManager> logger,
 80            IServerApplicationHost appHost,
 81            IApplicationPaths appPaths,
 82            IEventManager eventManager,
 83            IHttpClientFactory httpClientFactory,
 84            IServerConfigurationManager config,
 85            IPluginManager pluginManager)
 86        {
 4387            _currentInstallations = [];
 4388            _completedInstallationsInternal = [];
 89
 4390            _logger = logger;
 4391            _applicationHost = appHost;
 4392            _appPaths = appPaths;
 4393            _eventManager = eventManager;
 4394            _httpClientFactory = httpClientFactory;
 4395            _config = config;
 4396            _jsonSerializerOptions = JsonDefaults.Options;
 4397            _pluginManager = pluginManager;
 4398        }
 99
 100        /// <inheritdoc />
 0101        public IEnumerable<InstallationInfo> CompletedInstallations => _completedInstallationsInternal;
 102
 103        /// <inheritdoc />
 104        public async Task<PackageInfo[]> GetPackages(string manifestName, string manifest, bool filterIncompatible, Canc
 105        {
 106            try
 107            {
 6108                PackageInfo[]? packages = await _httpClientFactory.CreateClient(NamedClient.Default)
 6109                        .GetFromJsonAsync<PackageInfo[]>(new Uri(manifest), _jsonSerializerOptions, cancellationToken).C
 110
 6111                if (packages is null)
 112                {
 0113                    return Array.Empty<PackageInfo>();
 114                }
 115
 6116                var minimumVersion = new Version(0, 0, 0, 1);
 117                // Store the repository and repository url with each version, as they may be spread apart.
 378118                foreach (var entry in packages)
 119                {
 2646120                    for (int a = entry.Versions.Count - 1; a >= 0; a--)
 121                    {
 1140122                        var ver = entry.Versions[a];
 1140123                        ver.RepositoryName = manifestName;
 1140124                        ver.RepositoryUrl = manifest;
 125
 1140126                        if (!filterIncompatible)
 127                        {
 128                            continue;
 129                        }
 130
 978131                        if (!Version.TryParse(ver.TargetAbi, out var targetAbi))
 132                        {
 0133                            targetAbi = minimumVersion;
 134                        }
 135
 136                        // Only show plugins that are greater than or equal to targetAbi.
 978137                        if (_applicationHost.ApplicationVersion >= targetAbi)
 138                        {
 139                            continue;
 140                        }
 141
 142                        // Not compatible with this version so remove it.
 0143                        entry.Versions.Remove(ver);
 144                    }
 145                }
 146
 6147                return packages;
 148            }
 0149            catch (IOException ex)
 150            {
 0151                _logger.LogError(ex, "Cannot locate the plugin manifest {Manifest}", manifest);
 0152                return Array.Empty<PackageInfo>();
 153            }
 0154            catch (JsonException ex)
 155            {
 0156                _logger.LogError(ex, "Failed to deserialize the plugin manifest retrieved from {Manifest}", manifest);
 0157                return Array.Empty<PackageInfo>();
 158            }
 0159            catch (UriFormatException ex)
 160            {
 0161                _logger.LogError(ex, "The URL configured for the plugin repository manifest URL is not valid: {Manifest}
 0162                return Array.Empty<PackageInfo>();
 163            }
 0164            catch (NotSupportedException ex)
 165            {
 0166                _logger.LogError(ex, "The URL scheme configured for the plugin repository is not supported: {Manifest}",
 0167                return Array.Empty<PackageInfo>();
 168            }
 0169            catch (HttpRequestException ex)
 170            {
 0171                _logger.LogError(ex, "An error occurred while accessing the plugin manifest: {Manifest}", manifest);
 0172                return Array.Empty<PackageInfo>();
 173            }
 6174        }
 175
 176        /// <inheritdoc />
 177        public async Task<IReadOnlyList<PackageInfo>> GetAvailablePackages(CancellationToken cancellationToken = default
 178        {
 3179            var result = new List<PackageInfo>();
 12180            foreach (RepositoryInfo repository in _config.Configuration.PluginRepositories)
 181            {
 3182                if (repository.Enabled && repository.Url is not null)
 183                {
 184                    // Where repositories have the same content, the details from the first is taken.
 222185                    foreach (var package in await GetPackages(repository.Name ?? "Unnamed Repo", repository.Url, true, c
 186                    {
 108187                        var existing = FilterPackages(result, package.Name, package.Id).FirstOrDefault();
 188
 189                        // Remove invalid versions from the valid package.
 2172190                        for (var i = package.Versions.Count - 1; i >= 0; i--)
 191                        {
 978192                            var version = package.Versions[i];
 193
 978194                            var plugin = _pluginManager.GetPlugin(package.Id, version.VersionNumber);
 978195                            if (plugin is not null)
 196                            {
 0197                                await _pluginManager.PopulateManifest(package, version.VersionNumber, plugin.Path, plugi
 198                            }
 199
 200                            // Remove versions with a target ABI greater than the current application version.
 978201                            if (Version.TryParse(version.TargetAbi, out var targetAbi) && _applicationHost.ApplicationVe
 202                            {
 0203                                package.Versions.RemoveAt(i);
 204                            }
 978205                        }
 206
 207                        // Don't add a package that doesn't have any compatible versions.
 108208                        if (package.Versions.Count == 0)
 209                        {
 210                            continue;
 211                        }
 212
 108213                        if (existing is not null)
 214                        {
 215                            // Assumption is both lists are ordered, so slot these into the correct place.
 0216                            MergeSortedList(existing.Versions, package.Versions);
 217                        }
 218                        else
 219                        {
 108220                            result.Add(package);
 221                        }
 108222                    }
 223                }
 224            }
 225
 3226            return result;
 3227        }
 228
 229        /// <inheritdoc />
 230        public IEnumerable<PackageInfo> FilterPackages(
 231            IEnumerable<PackageInfo> availablePackages,
 232            string? name = null,
 233            Guid id = default,
 234            Version? specificVersion = null)
 235        {
 134236            if (!id.IsEmpty())
 237            {
 133238                availablePackages = availablePackages.Where(x => x.Id.Equals(id));
 239            }
 1240            else if (name is not null)
 241            {
 1242                availablePackages = availablePackages.Where(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase)
 243            }
 244
 134245            if (specificVersion is not null)
 246            {
 0247                availablePackages = availablePackages.Where(x => x.Versions.Any(y => y.VersionNumber.Equals(specificVers
 248            }
 249
 134250            return availablePackages;
 251        }
 252
 253        /// <inheritdoc />
 254        public IEnumerable<InstallationInfo> GetCompatibleVersions(
 255            IEnumerable<PackageInfo> availablePackages,
 256            string? name = null,
 257            Guid id = default,
 258            Version? minVersion = null,
 259            Version? specificVersion = null)
 260        {
 24261            var package = FilterPackages(availablePackages, name, id, specificVersion).FirstOrDefault();
 262
 263            // Package not found in repository
 24264            if (package is null)
 265            {
 24266                yield break;
 267            }
 268
 0269            var appVer = _applicationHost.ApplicationVersion;
 0270            var availableVersions = package.Versions
 0271                .Where(x => string.IsNullOrEmpty(x.TargetAbi) || Version.Parse(x.TargetAbi) <= appVer);
 272
 0273            if (specificVersion is not null)
 274            {
 0275                availableVersions = availableVersions.Where(x => x.VersionNumber.Equals(specificVersion));
 276            }
 0277            else if (minVersion is not null)
 278            {
 0279                availableVersions = availableVersions.Where(x => x.VersionNumber >= minVersion);
 280            }
 281
 0282            foreach (var v in availableVersions.OrderByDescending(x => x.VersionNumber))
 283            {
 0284                yield return new InstallationInfo
 0285                {
 0286                    Changelog = v.Changelog,
 0287                    Id = package.Id,
 0288                    Name = package.Name,
 0289                    Version = v.VersionNumber,
 0290                    SourceUrl = v.SourceUrl,
 0291                    Checksum = v.Checksum,
 0292                    PackageInfo = package
 0293                };
 294            }
 0295        }
 296
 297        /// <inheritdoc />
 298        public async Task<IEnumerable<InstallationInfo>> GetAvailablePluginUpdates(CancellationToken cancellationToken =
 299        {
 3300            var catalog = await GetAvailablePackages(cancellationToken).ConfigureAwait(false);
 3301            return GetAvailablePluginUpdates(catalog);
 3302        }
 303
 304        /// <inheritdoc />
 305        public async Task InstallPackage(InstallationInfo package, CancellationToken cancellationToken)
 306        {
 13307            ArgumentNullException.ThrowIfNull(package);
 308
 13309            var innerCancellationTokenSource = new CancellationTokenSource();
 310
 13311            var tuple = (package, innerCancellationTokenSource);
 312
 313            // Add it to the in-progress list
 314            lock (_currentInstallationsLock)
 315            {
 13316                _currentInstallations.Add(tuple);
 13317            }
 318
 13319            using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, innerCancel
 13320            var linkedToken = linkedTokenSource.Token;
 321
 13322            await _eventManager.PublishAsync(new PluginInstallingEventArgs(package)).ConfigureAwait(false);
 323
 324            try
 325            {
 13326                var isUpdate = await InstallPackageInternal(package, linkedToken).ConfigureAwait(false);
 327
 328                lock (_currentInstallationsLock)
 329                {
 1330                    _currentInstallations.Remove(tuple);
 1331                }
 332
 1333                _completedInstallationsInternal.Add(package);
 334
 1335                if (isUpdate)
 336                {
 0337                    await _eventManager.PublishAsync(new PluginUpdatedEventArgs(package)).ConfigureAwait(false);
 338                }
 339                else
 340                {
 1341                    await _eventManager.PublishAsync(new PluginInstalledEventArgs(package)).ConfigureAwait(false);
 342                }
 343
 1344                _applicationHost.NotifyPendingRestart();
 1345            }
 0346            catch (OperationCanceledException) when (linkedToken.IsCancellationRequested)
 0347            {
 348                // Only an actually cancelled token is a cancellation.
 349                lock (_currentInstallationsLock)
 350                {
 0351                    _currentInstallations.Remove(tuple);
 0352                }
 353
 0354                _logger.LogInformation("Package installation cancelled: {0} {1}", package.Name, package.Version);
 355
 0356                await _eventManager.PublishAsync(new PluginInstallationCancelledEventArgs(package)).ConfigureAwait(false
 357
 0358                throw;
 0359            }
 12360            catch (Exception ex)
 361            {
 12362                _logger.LogError(ex, "Package installation failed: {Name} {Version}", package.Name, package.Version);
 363
 364                lock (_currentInstallationsLock)
 365                {
 12366                    _currentInstallations.Remove(tuple);
 12367                }
 368
 12369                await _eventManager.PublishAsync(new InstallationFailedEventArgs
 12370                {
 12371                    InstallationInfo = package,
 12372                    Exception = ex
 12373                }).ConfigureAwait(false);
 374
 12375                throw;
 376            }
 377            finally
 378            {
 379                // Dispose the progress object and remove the installation from the in-progress list
 13380                tuple.innerCancellationTokenSource.Dispose();
 381            }
 1382        }
 383
 384        /// <summary>
 385        /// Uninstalls a plugin.
 386        /// </summary>
 387        /// <param name="plugin">The <see cref="LocalPlugin"/> to uninstall.</param>
 388        public void UninstallPlugin(LocalPlugin plugin)
 389        {
 0390            if (plugin is null)
 391            {
 0392                return;
 393            }
 394
 0395            if (plugin.Instance?.CanUninstall == false)
 396            {
 0397                _logger.LogWarning("Attempt to delete non removable plugin {PluginName}, ignoring request", plugin.Name)
 0398                return;
 399            }
 400
 0401            plugin.Instance?.OnUninstalling();
 402
 403            // Remove it the quick way for now
 0404            _pluginManager.RemovePlugin(plugin);
 405
 0406            _eventManager.Publish(new PluginUninstalledEventArgs(plugin.GetPluginInfo()));
 407
 0408            _applicationHost.NotifyPendingRestart();
 0409        }
 410
 411        /// <inheritdoc/>
 412        public bool CancelInstallation(Guid id)
 0413        {
 414            lock (_currentInstallationsLock)
 415            {
 0416                var install = _currentInstallations.Find(x => x.Info.Id.Equals(id));
 0417                if (install == default((InstallationInfo, CancellationTokenSource)))
 418                {
 0419                    return false;
 420                }
 421
 0422                install.Token.Cancel();
 0423                _currentInstallations.Remove(install);
 0424                return true;
 425            }
 0426        }
 427
 428        /// <inheritdoc />
 429        public void Dispose()
 430        {
 27431            Dispose(true);
 27432            GC.SuppressFinalize(this);
 27433        }
 434
 435        /// <summary>
 436        /// Releases unmanaged and optionally managed resources.
 437        /// </summary>
 438        /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources or <c>false</c> to release
 439        protected virtual void Dispose(bool dispose)
 440        {
 27441            if (dispose)
 27442            {
 443                lock (_currentInstallationsLock)
 444                {
 54445                    foreach (var (info, token) in _currentInstallations)
 446                    {
 0447                        token.Dispose();
 448                    }
 449
 27450                    _currentInstallations.Clear();
 27451                }
 452            }
 27453        }
 454
 455        /// <summary>
 456        /// Merges two sorted lists.
 457        /// </summary>
 458        /// <param name="source">The source <see cref="IList{VersionInfo}"/> instance to merge.</param>
 459        /// <param name="dest">The destination <see cref="IList{VersionInfo}"/> instance to merge with.</param>
 460        private static void MergeSortedList(IList<VersionInfo> source, IList<VersionInfo> dest)
 461        {
 0462            int sLength = source.Count - 1;
 0463            int dLength = dest.Count;
 0464            int s = 0, d = 0;
 0465            var sourceVersion = source[0].VersionNumber;
 0466            var destVersion = dest[0].VersionNumber;
 467
 0468            while (d < dLength)
 469            {
 0470                if (sourceVersion.CompareTo(destVersion) >= 0)
 471                {
 0472                    if (s < sLength)
 473                    {
 0474                        sourceVersion = source[++s].VersionNumber;
 475                    }
 476                    else
 477                    {
 478                        // Append all of destination to the end of source.
 0479                        while (d < dLength)
 480                        {
 0481                            source.Add(dest[d++]);
 482                        }
 483
 0484                        break;
 485                    }
 486                }
 487                else
 488                {
 0489                    source.Insert(s++, dest[d++]);
 0490                    if (d >= dLength)
 491                    {
 492                        break;
 493                    }
 494
 0495                    sLength++;
 0496                    destVersion = dest[d].VersionNumber;
 497                }
 498            }
 0499        }
 500
 501        private IEnumerable<InstallationInfo> GetAvailablePluginUpdates(IReadOnlyList<PackageInfo> pluginCatalog)
 502        {
 3503            var plugins = _pluginManager.Plugins;
 54504            foreach (var plugin in plugins)
 505            {
 506                // Don't auto update when plugin marked not to, or when it's disabled or pending removal.
 24507                if (plugin.Manifest?.AutoUpdate == false
 24508                    || plugin.Manifest?.Status is PluginStatus.Disabled or PluginStatus.Deleted)
 509                {
 510                    continue;
 511                }
 512
 24513                var compatibleVersions = GetCompatibleVersions(pluginCatalog, plugin.Name, plugin.Id, minVersion: plugin
 24514                var version = compatibleVersions.FirstOrDefault(y => y.Version > plugin.Version);
 515
 24516                if (version is not null && CompletedInstallations.All(x => !x.Id.Equals(version.Id)))
 517                {
 0518                    yield return version;
 519                }
 520            }
 3521        }
 522
 523        private async Task PerformPackageInstallation(InstallationInfo package, PluginStatus status, CancellationToken c
 524        {
 13525            if (!Path.GetExtension(package.SourceUrl.AsSpan()).Equals(".zip", StringComparison.OrdinalIgnoreCase))
 526            {
 0527                _logger.LogError("Only zip packages are supported. {SourceUrl} is not a zip archive.", package.SourceUrl
 0528                return;
 529            }
 530
 13531            if (!IsValidPackageDirectoryName(package.Name))
 532            {
 11533                _logger.LogError("Refusing to install package with invalid name {PackageName}.", package.Name);
 11534                throw new InvalidDataException($"Plugin package name '{package.Name}' is not a valid directory name.");
 535            }
 536
 537            // Always override the passed-in target (which is a file) and figure it out again
 2538            string targetDir = Path.Combine(_appPaths.PluginsPath, package.Name);
 539
 2540            var pluginsRoot = Path.TrimEndingDirectorySeparator(Path.GetFullPath(_appPaths.PluginsPath));
 2541            var resolvedTarget = Path.GetFullPath(targetDir);
 2542            if (!resolvedTarget.StartsWith(pluginsRoot + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase
 543            {
 0544                _logger.LogError(
 0545                    "Refusing to install package {PackageName}: resolved target {Resolved} is outside plugins directory 
 0546                    package.Name,
 0547                    resolvedTarget,
 0548                    pluginsRoot);
 0549                throw new InvalidDataException($"Plugin package name '{package.Name}' resolves outside the plugins direc
 550            }
 551
 552            // ResponseHeadersRead keeps the body out of the HttpClient timeout, which otherwise covers
 553            // the whole download; the package gets the longer budget below instead.
 2554            using var downloadTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
 2555            downloadTokenSource.CancelAfter(PackageDownloadTimeout);
 2556            var downloadToken = downloadTokenSource.Token;
 557
 2558            var buffer = new MemoryStream();
 2559            await using (buffer.ConfigureAwait(false))
 560            {
 561                try
 562                {
 2563                    using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
 2564                        .GetAsync(new Uri(package.SourceUrl), HttpCompletionOption.ResponseHeadersRead, downloadToken).C
 2565                    response.EnsureSuccessStatusCode();
 566
 567                    // The package is read twice, for the checksum and for the extraction, so it has
 568                    // to be buffered: the response stream is not seekable.
 2569                    await response.Content.CopyToAsync(buffer, downloadToken).ConfigureAwait(false);
 2570                }
 0571                catch (OperationCanceledException ex) when (!cancellationToken.IsCancellationRequested)
 572                {
 573                    // Either our budget above or the HttpClient timeout ran out.
 0574                    throw new TimeoutException(
 0575                        $"Downloading the package {package.Name} {package.Version} from {package.SourceUrl} timed out.",
 0576                        ex);
 577                }
 578
 2579                buffer.Position = 0;
 2580                Stream stream = buffer;
 581
 582                // CA5351: Do Not Use Broken Cryptographic Algorithms
 583#pragma warning disable CA5351
 2584                cancellationToken.ThrowIfCancellationRequested();
 585
 2586                var hash = Convert.ToHexString(await MD5.HashDataAsync(stream, cancellationToken).ConfigureAwait(false))
 2587                if (!string.Equals(package.Checksum, hash, StringComparison.OrdinalIgnoreCase))
 588                {
 1589                    _logger.LogError(
 1590                        "The checksums didn't match while installing {Package}, expected: {Expected}, got: {Received}",
 1591                        package.Name,
 1592                        package.Checksum,
 1593                        hash);
 1594                    throw new InvalidDataException("The checksum of the received data doesn't match.");
 595                }
 596
 597                // Version folder as they cannot be overwritten in Windows.
 1598                targetDir += "_" + package.Version;
 599
 1600                if (Directory.Exists(targetDir))
 601                {
 602                    try
 603                    {
 0604                        Directory.Delete(targetDir, true);
 0605                    }
 606#pragma warning disable CA1031 // Do not catch general exception types
 0607                    catch
 608#pragma warning restore CA1031 // Do not catch general exception types
 609                    {
 610                        // Ignore any exceptions.
 0611                    }
 612                }
 613
 1614                stream.Position = 0;
 1615                await ZipFile.ExtractToDirectoryAsync(stream, targetDir, true, cancellationToken).ConfigureAwait(false);
 1616            }
 617
 618            // Ensure we create one or populate existing ones with missing data.
 1619            await _pluginManager.PopulateManifest(package.PackageInfo, package.Version, targetDir, status).ConfigureAwai
 620
 1621            _pluginManager.ImportPluginFrom(targetDir);
 1622        }
 623
 624        private static bool IsValidPackageDirectoryName(string? name)
 625        {
 13626            if (string.IsNullOrWhiteSpace(name))
 627            {
 2628                return false;
 629            }
 630
 11631            if (name.Equals(".", StringComparison.Ordinal) || name.Equals("..", StringComparison.Ordinal))
 632            {
 2633                return false;
 634            }
 635
 9636            if (name.IndexOfAny(InvalidPackageNameChars) >= 0)
 637            {
 7638                return false;
 639            }
 640
 2641            return true;
 642        }
 643
 644        private async Task<bool> InstallPackageInternal(InstallationInfo package, CancellationToken cancellationToken)
 645        {
 13646            LocalPlugin? plugin = _pluginManager.Plugins.FirstOrDefault(p => p.Id.Equals(package.Id) && p.Version.Equals
 13647                  ?? _pluginManager.Plugins.FirstOrDefault(p => p.Name.Equals(package.Name, StringComparison.OrdinalIgno
 648
 13649            await PerformPackageInstallation(package, plugin?.Manifest.Status ?? PluginStatus.Active, cancellationToken)
 1650            _logger.LogInformation("Plugin {Action}: {PluginName} {PluginVersion}", plugin is null ? "installed" : "upda
 651
 1652            return plugin is not null;
 1653        }
 654    }
 655}