diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index 642e2fdbe9..276d0fe30a 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -4,6 +4,7 @@ using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
+using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
@@ -37,6 +38,7 @@ using Emby.Server.Implementations.LiveTv;
using Emby.Server.Implementations.Localization;
using Emby.Server.Implementations.Net;
using Emby.Server.Implementations.Playlists;
+using Emby.Server.Implementations.Plugins;
using Emby.Server.Implementations.QuickConnect;
using Emby.Server.Implementations.ScheduledTasks;
using Emby.Server.Implementations.Security;
@@ -119,6 +121,7 @@ namespace Emby.Server.Implementations
private readonly IFileSystem _fileSystemManager;
private readonly INetworkManager _networkManager;
private readonly IXmlSerializer _xmlSerializer;
+ private readonly IJsonSerializer _jsonSerializer;
private readonly IStartupOptions _startupOptions;
private IMediaEncoder _mediaEncoder;
@@ -255,6 +258,8 @@ namespace Emby.Server.Implementations
IServiceCollection serviceCollection)
{
_xmlSerializer = new MyXmlSerializer();
+ _jsonSerializer = new JsonSerializer();
+
ServiceCollection = serviceCollection;
_networkManager = networkManager;
@@ -1021,6 +1026,108 @@ namespace Emby.Server.Implementations
protected abstract void RestartInternal();
+ ///
+ /// Comparison function used in .
+ ///
+ /// Item to compare.
+ /// Item to compare with.
+ /// Boolean result of the operation.
+ private static int VersionCompare(
+ (Version PluginVersion, string Name, string Path) a,
+ (Version PluginVersion, string Name, string Path) b)
+ {
+ int compare = string.Compare(a.Name, b.Name, true, CultureInfo.InvariantCulture);
+
+ if (compare == 0)
+ {
+ return a.PluginVersion.CompareTo(b.PluginVersion);
+ }
+
+ return compare;
+ }
+
+ ///
+ /// Returns a list of plugins to install.
+ ///
+ /// Path to check.
+ /// True if an attempt should be made to delete old plugs.
+ /// Enumerable list of dlls to load.
+ private IEnumerable GetPlugins(string path, bool cleanup = true)
+ {
+ var dllList = new List();
+ var versions = new List<(Version PluginVersion, string Name, string Path)>();
+ var directories = Directory.EnumerateDirectories(path, "*.*", SearchOption.TopDirectoryOnly);
+ string metafile;
+
+ foreach (var dir in directories)
+ {
+ try
+ {
+ metafile = Path.Combine(dir, "meta.json");
+ if (File.Exists(metafile))
+ {
+ var manifest = _jsonSerializer.DeserializeFromFile(metafile);
+
+ if (!Version.TryParse(manifest.TargetAbi, out var targetAbi))
+ {
+ targetAbi = new Version(0, 0, 0, 1);
+ }
+
+ if (!Version.TryParse(manifest.Version, out var version))
+ {
+ version = new Version(0, 0, 0, 1);
+ }
+
+ if (ApplicationVersion >= targetAbi)
+ {
+ // Only load Plugins if the plugin is built for this version or below.
+ versions.Add((version, manifest.Name, dir));
+ }
+ }
+ else
+ {
+ metafile = dir.Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries)[^1];
+ // Add it under the path name and version 0.0.0.1.
+ versions.Add((new Version(0, 0, 0, 1), metafile, dir));
+ }
+ }
+ catch
+ {
+ continue;
+ }
+ }
+
+ string lastName = string.Empty;
+ versions.Sort(VersionCompare);
+ // Traverse backwards through the list.
+ // The first item will be the latest version.
+ for (int x = versions.Count - 1; x >= 0; x--)
+ {
+ if (!string.Equals(lastName, versions[x].Name, StringComparison.OrdinalIgnoreCase))
+ {
+ dllList.AddRange(Directory.EnumerateFiles(versions[x].Path, "*.dll", SearchOption.AllDirectories));
+ lastName = versions[x].Name;
+ continue;
+ }
+
+ if (!string.IsNullOrEmpty(lastName) && cleanup)
+ {
+ // Attempt a cleanup of old folders.
+ try
+ {
+ Logger.LogDebug("Deleting {Path}", versions[x].Path);
+ Directory.Delete(versions[x].Path, true);
+ }
+ catch (Exception e)
+ {
+ Logger.LogWarning(e, "Unable to delete {Path}", versions[x].Path);
+ }
+ }
+ }
+
+ return dllList;
+ }
+
///
/// Gets the composable part assemblies.
///
@@ -1029,7 +1136,7 @@ namespace Emby.Server.Implementations
{
if (Directory.Exists(ApplicationPaths.PluginsPath))
{
- foreach (var file in Directory.EnumerateFiles(ApplicationPaths.PluginsPath, "*.dll", SearchOption.AllDirectories))
+ foreach (var file in GetPlugins(ApplicationPaths.PluginsPath))
{
Assembly plugAss;
try
diff --git a/Emby.Server.Implementations/Plugins/PluginManifest.cs b/Emby.Server.Implementations/Plugins/PluginManifest.cs
new file mode 100644
index 0000000000..33762791bc
--- /dev/null
+++ b/Emby.Server.Implementations/Plugins/PluginManifest.cs
@@ -0,0 +1,60 @@
+using System;
+
+namespace Emby.Server.Implementations.Plugins
+{
+ ///
+ /// Defines a Plugin manifest file.
+ ///
+ public class PluginManifest
+ {
+ ///
+ /// Gets or sets the category of the plugin.
+ ///
+ public string Category { get; set; }
+
+ ///
+ /// Gets or sets the changelog information.
+ ///
+ public string Changelog { get; set; }
+
+ ///
+ /// Gets or sets the description of the plugin.
+ ///
+ public string Description { get; set; }
+
+ ///
+ /// Gets or sets the Global Unique Identifier for the plugin.
+ ///
+ public Guid Guid { get; set; }
+
+ ///
+ /// Gets or sets the Name of the plugin.
+ ///
+ public string Name { get; set; }
+
+ ///
+ /// Gets or sets an overview of the plugin.
+ ///
+ public string Overview { get; set; }
+
+ ///
+ /// Gets or sets the owner of the plugin.
+ ///
+ public string Owner { get; set; }
+
+ ///
+ /// Gets or sets the compatibility version for the plugin.
+ ///
+ public string TargetAbi { get; set; }
+
+ ///
+ /// Gets or sets the timestamp of the plugin.
+ ///
+ public DateTime Timestamp { get; set; }
+
+ ///
+ /// Gets or sets the Version number of the plugin.
+ ///
+ public string Version { get; set; }
+ }
+}
diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs
index e19158f8e9..8e24bf55ce 100644
--- a/Emby.Server.Implementations/Updates/InstallationManager.cs
+++ b/Emby.Server.Implementations/Updates/InstallationManager.cs
@@ -15,12 +15,14 @@ using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Common.Updates;
+using MediaBrowser.Common.System;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Updates;
using Microsoft.Extensions.Logging;
+using MediaBrowser.Model.System;
namespace Emby.Server.Implementations.Updates
{
@@ -377,11 +379,20 @@ namespace Emby.Server.Implementations.Updates
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))
{
- Directory.Delete(targetDir, true);
+ try
+ {
+ Directory.Delete(targetDir, true);
+ }
+ catch
+ {
+ // Ignore any exceptions.
+ }
}
-
stream.Position = 0;
_zipClient.ExtractAllFromZip(stream, targetDir, true);
@@ -423,15 +434,22 @@ namespace Emby.Server.Implementations.Updates
path = file;
}
- if (isDirectory)
+ try
{
- _logger.LogInformation("Deleting plugin directory {0}", path);
- Directory.Delete(path, true);
+ if (isDirectory)
+ {
+ _logger.LogInformation("Deleting plugin directory {0}", path);
+ Directory.Delete(path, true);
+ }
+ else
+ {
+ _logger.LogInformation("Deleting plugin file {0}", path);
+ _fileSystem.DeleteFile(path);
+ }
}
- else
+ catch
{
- _logger.LogInformation("Deleting plugin file {0}", path);
- _fileSystem.DeleteFile(path);
+ // Ignore file errors.
}
var list = _config.Configuration.UninstalledPlugins.ToList();