mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-17 05:54:26 +01:00
Installing a plugin should install all plugin dependencies
This commit is contained in:
@@ -22,9 +22,20 @@ namespace BTCPayServer.Abstractions.Contracts
|
||||
{
|
||||
public string Identifier { get; set; }
|
||||
public string Condition { get; set; }
|
||||
|
||||
public VersionCondition ParseCondition()
|
||||
{
|
||||
VersionCondition.TryParse(Condition ?? "", out var condition);
|
||||
return condition ?? new VersionCondition.Yes();
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Identifier}: {Condition}";
|
||||
// Format it
|
||||
var cond = Condition ?? "";
|
||||
if (VersionCondition.TryParse(cond, out var condition))
|
||||
cond = condition.ToString();
|
||||
return $"{Identifier}: {cond}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
142
BTCPayServer.Abstractions/Contracts/VersionCondition.cs
Normal file
142
BTCPayServer.Abstractions/Contracts/VersionCondition.cs
Normal file
@@ -0,0 +1,142 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace BTCPayServer.Abstractions.Contracts;
|
||||
|
||||
public abstract class VersionCondition
|
||||
{
|
||||
public class Any(VersionCondition[] conditions) : VersionCondition
|
||||
{
|
||||
public static bool TryParse(string str, [MaybeNullWhen(false)] out Any condition)
|
||||
{
|
||||
condition = null;
|
||||
var any = str.Trim().Split("||", StringSplitOptions.RemoveEmptyEntries);
|
||||
if (any.Length is 0 or 1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var conditions = new VersionCondition[any.Length];
|
||||
var i = 0;
|
||||
foreach (var item in any)
|
||||
{
|
||||
if (!VersionCondition.TryParse(item, out var subCondition))
|
||||
return false;
|
||||
conditions[i++] = subCondition;
|
||||
}
|
||||
condition = new Any(conditions);
|
||||
return true;
|
||||
}
|
||||
|
||||
public VersionCondition[] Conditions { get; set; } = conditions;
|
||||
public override string ToString() => string.Join(" || ", Conditions.Select(c => c.ToString()));
|
||||
public override bool IsFulfilled(Version version) => Conditions.Any(c => c.IsFulfilled(version));
|
||||
}
|
||||
public class All(VersionCondition[] conditions) : VersionCondition
|
||||
{
|
||||
public static bool TryParse(string str, [MaybeNullWhen(false)] out All condition)
|
||||
{
|
||||
condition = null;
|
||||
var any = str.Trim().Split("&&", StringSplitOptions.RemoveEmptyEntries);
|
||||
if (any.Length is 0 or 1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var conditions = new VersionCondition[any.Length];
|
||||
var i = 0;
|
||||
foreach (var item in any)
|
||||
{
|
||||
if (!VersionCondition.TryParse(item, out var subCondition))
|
||||
return false;
|
||||
conditions[i++] = subCondition;
|
||||
}
|
||||
condition = new All(conditions);
|
||||
return true;
|
||||
}
|
||||
public VersionCondition[] Conditions { get; set; } = conditions;
|
||||
public override string ToString() => string.Join(" && ", Conditions.Select(c => c.ToString()));
|
||||
public override bool IsFulfilled(Version version) => Conditions.All(c => c.IsFulfilled(version));
|
||||
}
|
||||
|
||||
public class Not : VersionCondition
|
||||
{
|
||||
public override bool IsFulfilled(Version version) => false;
|
||||
public override string ToString() => "!";
|
||||
}
|
||||
public class Yes : VersionCondition
|
||||
{
|
||||
public override bool IsFulfilled(Version version) => true;
|
||||
public override string ToString() => "";
|
||||
}
|
||||
|
||||
public class Op(string op, Version ver) : VersionCondition
|
||||
{
|
||||
public string Operation { get; set; } = op;
|
||||
public Version Version { get; set; } = ver;
|
||||
|
||||
public override bool IsFulfilled(Version version)
|
||||
=> Operation switch
|
||||
{
|
||||
">=" => version >= Version,
|
||||
"<=" => version <= Version,
|
||||
">" => version > Version,
|
||||
"<" => version < Version,
|
||||
"^" => version >= Version && version.Major == Version.Major,
|
||||
"~" => version >= Version && version.Major == Version.Major &&
|
||||
version.Minor == Version.Minor,
|
||||
"!=" => version != Version,
|
||||
_ => version == Version, // "==" is the default
|
||||
};
|
||||
|
||||
public override string ToString() => $"{Operation} {Version}";
|
||||
}
|
||||
|
||||
public static bool TryParse(string str, [MaybeNullWhen(false)] out VersionCondition condition)
|
||||
{
|
||||
condition = null;
|
||||
if (Any.TryParse(str, out var anyCond))
|
||||
{
|
||||
condition = anyCond;
|
||||
return true;
|
||||
}
|
||||
if (All.TryParse(str, out var allCond))
|
||||
{
|
||||
condition = allCond;
|
||||
return true;
|
||||
}
|
||||
|
||||
str = str.Trim();
|
||||
if (str == "!")
|
||||
{
|
||||
condition = new Not();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (str.Length == 0)
|
||||
{
|
||||
condition = new Yes();
|
||||
return true;
|
||||
}
|
||||
|
||||
var opLen = str switch
|
||||
{
|
||||
{ Length: >= 2 } when str.Substring(0, 2) is ">=" or "<=" or "!=" or "==" => 2,
|
||||
{ Length: >= 1 } when str.Substring(0, 1) is ">" or "<" or "^" or "~" => 1,
|
||||
_ => 0
|
||||
};
|
||||
if (opLen == 0)
|
||||
return false;
|
||||
var op = str.Substring(0, opLen);
|
||||
var ver = str.Substring(opLen).Trim();
|
||||
if (Version.TryParse(ver, out var v))
|
||||
{
|
||||
condition = new Op(op, v);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public abstract bool IsFulfilled(Version version);
|
||||
}
|
||||
@@ -30,7 +30,9 @@ namespace BTCPayServer.Tests
|
||||
|
||||
var wid = new WalletObjectId(new WalletId("AAA", "ddd"), "a", "b");
|
||||
var all = Enumerable.Range(0, 10)
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
.Select(i => walletRepo.ModifyWalletObjectData(wid, (o) => { o["idx"] = i; }))
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
.ToArray();
|
||||
foreach (var task in all)
|
||||
{
|
||||
|
||||
@@ -9,6 +9,7 @@ using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
@@ -488,6 +489,62 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(Money.Satoshis(200), bump.NewTxFee);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(">= 1.0.0.0", "1.0.0.0", true)]
|
||||
[InlineData("> 1.0.0.0", "1.0.0.0", false)]
|
||||
[InlineData("> 1.0.0.0", "1.0.0.1", true)]
|
||||
[InlineData(">= 1.0.0.0", "1.0.0.1", true)]
|
||||
[InlineData("<= 1.0.0.0", "1.0.0.0", true)]
|
||||
[InlineData("< 1.0.0.0", "1.0.0.0", false)]
|
||||
[InlineData("< 1.0.0.0", "1.0.0.1", false)]
|
||||
[InlineData("<= 1.0.0.0", "1.0.0.1", false)]
|
||||
[InlineData("!= 1.0.0.0", "1.0.0.0", false)]
|
||||
[InlineData("!= 1.0.0.0", "1.0.0.1", true)]
|
||||
[InlineData("== 1.0.0.0 || == 1.0.0.1", "1.0.0.1", true)]
|
||||
[InlineData("== 1.0.0.0 || == 1.0.0.1", "1.0.0.0", true)]
|
||||
[InlineData("== 1.0.0.0 || == 1.0.0.1", "1.0.0.3", false)]
|
||||
[InlineData("== 1.0.0.0 || == 1.0.0.1", "0.0.0.9", false)]
|
||||
// All
|
||||
[InlineData(">= 1.2.1.0 && < 1.2.2.0", "1.2.1.0", true)]
|
||||
[InlineData(">= 1.2.1.0 && < 1.2.2.0", "1.2.2.0", false)]
|
||||
[InlineData(">= 1.2.1.0 && < 1.2.2.0", "1.2.1.5", true)]
|
||||
// Above, same major
|
||||
[InlineData("^ 1.0.0.5", "1.2.3.4", true)]
|
||||
[InlineData("^ 1.0.0.5", "1.0.0.4", false)]
|
||||
[InlineData("^ 1.0.0.5", "1.0.0.6", true)]
|
||||
[InlineData("^ 1.0.0.5", "2.0.0.4", false)]
|
||||
// Above, same major + minor
|
||||
[InlineData("~ 1.0.0.5", "1.2.3.4", false)]
|
||||
[InlineData("~ 1.0.0.5", "1.0.0.4", false)]
|
||||
[InlineData("~ 1.0.0.5", "1.0.0.6", true)]
|
||||
[InlineData("~ 1.0.2.5", "2.0.0.4", false)]
|
||||
[InlineData("~ 1.0.2.5", "1.0.2.6", true)]
|
||||
[InlineData("~ 1.0.2.5", "1.0.2.4", false)]
|
||||
[InlineData("", "0.0.0.9", true)]
|
||||
public void CanParseVersionConditions(string condition, string version, bool expected)
|
||||
{
|
||||
Assert.True(VersionCondition.TryParse(condition, out var cond));
|
||||
Assert.Equal(expected, cond.IsFulfilled(Version.Parse(version)));
|
||||
Assert.Equal(condition, cond.ToString());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(" ~ 1.0.2.5 ", "~ 1.0.2.5")]
|
||||
[InlineData(" == 1.0.2.5||>1.2.3", "== 1.0.2.5 || > 1.2.3")]
|
||||
public void CanParseBadSpaceVersionCondition(string parsed, string formatted)
|
||||
{
|
||||
Assert.True(VersionCondition.TryParse(parsed, out var cond));
|
||||
Assert.Equal(formatted, cond.ToString());
|
||||
Assert.True(VersionCondition.TryParse(formatted, out cond));
|
||||
Assert.Equal(formatted, cond.ToString());
|
||||
|
||||
Assert.Equal("Test: " + formatted, new IBTCPayServerPlugin.PluginDependency()
|
||||
{
|
||||
Identifier = "Test",
|
||||
Condition = parsed
|
||||
}.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanCalculateDust()
|
||||
{
|
||||
|
||||
@@ -40,7 +40,8 @@ namespace BTCPayServer.Controllers
|
||||
availablePluginsByIdentifier.TryAdd(p.Identifier, p);
|
||||
var res = new ListPluginsViewModel()
|
||||
{
|
||||
Installed = pluginService.LoadedPlugins,
|
||||
Plugins = pluginService.LoadedPlugins,
|
||||
Installed = pluginService.Installed,
|
||||
Available = availablePlugins,
|
||||
Commands = pluginService.GetPendingCommands(),
|
||||
Disabled = pluginService.GetDisabledPlugins(),
|
||||
@@ -52,12 +53,13 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
public class ListPluginsViewModel
|
||||
{
|
||||
public IEnumerable<IBTCPayServerPlugin> Installed { get; set; }
|
||||
public IEnumerable<IBTCPayServerPlugin> Plugins { get; set; }
|
||||
public IEnumerable<PluginService.AvailablePlugin> Available { get; set; }
|
||||
public (string command, string plugin)[] Commands { get; set; }
|
||||
public bool CanShowRestart { get; set; }
|
||||
public Dictionary<string, Version> Disabled { get; set; }
|
||||
public Dictionary<string, AvailablePlugin> DownloadedPluginsByIdentifier { get; set; } = new Dictionary<string, AvailablePlugin>();
|
||||
public Dictionary<string, Version> Installed { get; set; }
|
||||
}
|
||||
|
||||
[HttpPost("server/plugins/uninstall-all")]
|
||||
@@ -102,28 +104,24 @@ namespace BTCPayServer.Controllers
|
||||
public async Task<IActionResult> InstallPlugin(
|
||||
[FromServices] PluginService pluginService, string plugin, bool update = false, string version = null)
|
||||
{
|
||||
try
|
||||
var ctx = new DownloadPluginContext(pluginService, plugin, version, new(), new(), null);
|
||||
await DownloadPluginAndDependencies(ctx);
|
||||
if (ctx.DependencyFailed.Count == 0)
|
||||
{
|
||||
await pluginService.DownloadRemotePlugin(plugin, version);
|
||||
if (update)
|
||||
{
|
||||
pluginService.UpdatePlugin(plugin);
|
||||
}
|
||||
else
|
||||
{
|
||||
pluginService.InstallPlugin(plugin);
|
||||
}
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Message = StringLocalizer["Plugin scheduled to be installed."].Value,
|
||||
Severity = StatusMessageModel.StatusSeverity.Success
|
||||
});
|
||||
}
|
||||
catch (Exception)
|
||||
else
|
||||
{
|
||||
var error = String.Join(" \n", ctx.DependencyFailed
|
||||
.Select(d => $"{d.Key}: {d.Value}")
|
||||
.ToArray());
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Message = StringLocalizer["The plugin could not be downloaded. Try again later."].Value,
|
||||
Message = StringLocalizer["The plugin could not be downloaded. Try again later."].Value + " \n" + error,
|
||||
Severity = StatusMessageModel.StatusSeverity.Error
|
||||
});
|
||||
}
|
||||
@@ -131,6 +129,60 @@ namespace BTCPayServer.Controllers
|
||||
return RedirectToAction("ListPlugins");
|
||||
}
|
||||
|
||||
public record DownloadPluginContext(PluginService PluginService, string Plugin, string Version, Dictionary<string, AvailablePlugin> Downloaded, Dictionary<string, string> DependencyFailed, VersionCondition VersionCondition);
|
||||
private async Task DownloadPluginAndDependencies(DownloadPluginContext ctx)
|
||||
{
|
||||
if (ctx.Downloaded.ContainsKey(ctx.Plugin)
|
||||
||
|
||||
ctx.DependencyFailed.ContainsKey(ctx.Plugin))
|
||||
return;
|
||||
AvailablePlugin manifest;
|
||||
try
|
||||
{
|
||||
manifest = await ctx.PluginService.DownloadRemotePlugin(ctx.Plugin, ctx.Version, ctx.VersionCondition);
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
ctx.DependencyFailed.Add(ctx.Plugin, ex.Message);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var dep in manifest.Dependencies)
|
||||
{
|
||||
if (!PluginManager.DependencyMet(dep, ctx.PluginService.Installed))
|
||||
{
|
||||
if (dep.Identifier.Equals("BTCPayServer", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
ctx.DependencyFailed.Add(ctx.Plugin, $"This condition can't be satisfied {dep}");
|
||||
return;
|
||||
}
|
||||
|
||||
var cond = dep.ParseCondition();
|
||||
var childCtx = ctx with
|
||||
{
|
||||
Plugin = dep.Identifier,
|
||||
Version = null,
|
||||
VersionCondition = cond
|
||||
};
|
||||
if (childCtx.VersionCondition is VersionCondition.Not)
|
||||
{
|
||||
ctx.DependencyFailed.Add(ctx.Plugin, $"The currently installed plugin {dep.Identifier} is incompatible with this plugin.");
|
||||
return;
|
||||
}
|
||||
|
||||
await DownloadPluginAndDependencies(childCtx);
|
||||
if (childCtx.DependencyFailed.ContainsKey(childCtx.Plugin))
|
||||
{
|
||||
ctx.DependencyFailed.Add(ctx.Plugin, $"Failed to download dependency {dep.Identifier}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.PluginService.InstallPlugin(ctx.Plugin);
|
||||
ctx.Downloaded.Add(ctx.Plugin, manifest);
|
||||
}
|
||||
|
||||
[HttpPost("server/plugins/upload")]
|
||||
public async Task<IActionResult> UploadPlugin([FromServices] PluginService pluginService,
|
||||
List<IFormFile> files)
|
||||
|
||||
@@ -83,8 +83,7 @@ namespace BTCPayServer.HostedServices
|
||||
dh.LastVersions ??= new Dictionary<string, Version>();
|
||||
var disabledPlugins = pluginService.GetDisabledPlugins();
|
||||
|
||||
var installedPlugins =
|
||||
pluginService.LoadedPlugins.ToDictionary(plugin => plugin.Identifier, plugin => plugin.Version);
|
||||
var installedPlugins = pluginService.Installed;
|
||||
var remotePlugins = await pluginService.GetRemotePlugins(null);
|
||||
//take the latest version of each plugin
|
||||
var remotePluginsList = remotePlugins
|
||||
|
||||
@@ -55,13 +55,17 @@ namespace BTCPayServer.Plugins
|
||||
this.httpClient = httpClient;
|
||||
}
|
||||
static JsonSerializerSettings serializerSettings = new() { ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver() };
|
||||
public async Task<PublishedVersion[]> GetPublishedVersions(string btcpayVersion, bool includePreRelease, string searchPluginName = null)
|
||||
public async Task<PublishedVersion[]> GetPublishedVersions(string btcpayVersion, bool includePreRelease, string searchPluginName = null, string searchPluginIdentifier = null, bool? includeAllVersions = null)
|
||||
{
|
||||
var queryString = $"?includePreRelease={includePreRelease}";
|
||||
if (btcpayVersion is not null)
|
||||
queryString += $"&btcpayVersion={btcpayVersion}";
|
||||
if (searchPluginName is not null)
|
||||
queryString += $"&searchPluginName={searchPluginName}";
|
||||
if (searchPluginIdentifier is not null)
|
||||
queryString += $"&searchPluginIdentifier={searchPluginIdentifier}";
|
||||
if (includeAllVersions is not null)
|
||||
queryString += $"&includeAllVersions={includeAllVersions}";
|
||||
var result = await httpClient.GetStringAsync($"api/v1/plugins{queryString}");
|
||||
return JsonConvert.DeserializeObject<PublishedVersion[]>(result, serializerSettings) ?? throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
@@ -148,7 +148,7 @@ namespace BTCPayServer.Plugins
|
||||
});
|
||||
logger.LogInformation($"Loading plugins from {pluginsFolder}");
|
||||
Directory.CreateDirectory(pluginsFolder);
|
||||
ExecuteCommands(pluginsFolder);
|
||||
ExecuteCommands(pluginsFolder, new());
|
||||
|
||||
var disabledPluginIdentifiers = GetDisabledPluginIdentifiers(pluginsFolder);
|
||||
var systemAssembly = typeof(Program).Assembly;
|
||||
@@ -348,7 +348,7 @@ namespace BTCPayServer.Plugins
|
||||
return GetPluginInstancesFromAssembly(assembly, silentlyFails).FirstOrDefault(plugin => plugin.Identifier == pluginIdentifier);
|
||||
}
|
||||
|
||||
private static bool ExecuteCommands(string pluginsFolder, Dictionary<string, Version>? installed = null)
|
||||
private static bool ExecuteCommands(string pluginsFolder, Dictionary<string, Version> installed)
|
||||
{
|
||||
var pendingCommands = GetPendingCommands(pluginsFolder);
|
||||
if (!pendingCommands.Any())
|
||||
@@ -411,13 +411,6 @@ namespace BTCPayServer.Plugins
|
||||
var dirName = Path.Combine(pluginsFolder, command.extension);
|
||||
switch (command.command)
|
||||
{
|
||||
case "update":
|
||||
if (!DependenciesMet(pluginsFolder, command.extension, installed))
|
||||
return false;
|
||||
ExecuteCommand(("delete", command.extension), pluginsFolder, installed);
|
||||
ExecuteCommand(("install", command.extension), pluginsFolder, installed);
|
||||
break;
|
||||
|
||||
case "delete":
|
||||
ExecuteCommand(("enable", command.extension), pluginsFolder, installed);
|
||||
if (File.Exists(dirName))
|
||||
@@ -433,12 +426,12 @@ namespace BTCPayServer.Plugins
|
||||
case "install":
|
||||
var fileName = dirName + BTCPayPluginSuffix;
|
||||
var manifestFileName = dirName + ".json";
|
||||
if (!DependenciesMet(pluginsFolder, command.extension, installed))
|
||||
return false;
|
||||
|
||||
ExecuteCommand(("enable", command.extension), pluginsFolder, installed);
|
||||
|
||||
if (File.Exists(fileName))
|
||||
{
|
||||
if (File.Exists(dirName) || Directory.Exists(dirName))
|
||||
ExecuteCommand(("delete", dirName), pluginsFolder, installed);
|
||||
ZipFile.ExtractToDirectory(fileName, dirName, true);
|
||||
File.Delete(fileName);
|
||||
if (File.Exists(manifestFileName))
|
||||
@@ -538,58 +531,15 @@ namespace BTCPayServer.Plugins
|
||||
}
|
||||
|
||||
public static bool DependencyMet(IBTCPayServerPlugin.PluginDependency dependency,
|
||||
Dictionary<string, Version>? installed = null)
|
||||
Dictionary<string, Version> installed)
|
||||
{
|
||||
var plugin = dependency.Identifier.ToLowerInvariant();
|
||||
var versionReq = dependency.Condition;
|
||||
// ensure installed is not null and has lowercased keys for comparison
|
||||
installed = installed == null
|
||||
? new Dictionary<string, Version>()
|
||||
: installed.ToDictionary(x => x.Key.ToLowerInvariant(), x => x.Value);
|
||||
if (!installed.ContainsKey(plugin) && !versionReq.Equals("!"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var versionConditions = versionReq.Split("||", StringSplitOptions.RemoveEmptyEntries);
|
||||
return versionConditions.Any(s =>
|
||||
{
|
||||
s = s.Trim();
|
||||
var v = s.Substring(1);
|
||||
if (s[1] == '=')
|
||||
{
|
||||
v = s.Substring(2);
|
||||
}
|
||||
|
||||
var parsedV = Version.Parse(v);
|
||||
switch (s)
|
||||
{
|
||||
case { } xx when xx.StartsWith(">="):
|
||||
return installed[plugin] >= parsedV;
|
||||
case { } xx when xx.StartsWith("<="):
|
||||
return installed[plugin] <= parsedV;
|
||||
case { } xx when xx.StartsWith(">"):
|
||||
return installed[plugin] > parsedV;
|
||||
case { } xx when xx.StartsWith("<"):
|
||||
return installed[plugin] < parsedV;
|
||||
case { } xx when xx.StartsWith("^"):
|
||||
return installed[plugin] >= parsedV && installed[plugin].Major == parsedV.Major;
|
||||
case { } xx when xx.StartsWith("~"):
|
||||
return installed[plugin] >= parsedV && installed[plugin].Major == parsedV.Major &&
|
||||
installed[plugin].Minor == parsedV.Minor;
|
||||
case { } xx when xx.StartsWith("!="):
|
||||
return installed[plugin] != parsedV;
|
||||
case { } xx when xx.StartsWith("=="):
|
||||
default:
|
||||
return installed[plugin] == parsedV;
|
||||
}
|
||||
});
|
||||
var condition = dependency.ParseCondition();
|
||||
if (!installed.TryGetValue(dependency.Identifier, out var v))
|
||||
return condition is VersionCondition.Not;
|
||||
return condition.IsFulfilled(v);
|
||||
}
|
||||
|
||||
public static bool DependenciesMet(IEnumerable<IBTCPayServerPlugin.PluginDependency> dependencies,
|
||||
Dictionary<string, Version>? installed)
|
||||
{
|
||||
return dependencies.All(dependency => DependencyMet(dependency, installed));
|
||||
}
|
||||
Dictionary<string, Version> installed) => dependencies.All(dependency => DependencyMet(dependency, installed));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,12 +28,15 @@ namespace BTCPayServer.Plugins
|
||||
BTCPayServerEnvironment env)
|
||||
{
|
||||
LoadedPlugins = btcPayServerPlugins;
|
||||
Installed = btcPayServerPlugins.ToDictionary(p => p.Identifier, p => p.Version, StringComparer.OrdinalIgnoreCase);
|
||||
_pluginBuilderClient = pluginBuilderClient;
|
||||
_dataDirectories = dataDirectories;
|
||||
_policiesSettings = policiesSettings;
|
||||
Env = env;
|
||||
}
|
||||
|
||||
public Dictionary<string, Version> Installed { get; set; }
|
||||
|
||||
public IEnumerable<IBTCPayServerPlugin> LoadedPlugins { get; }
|
||||
public BTCPayServerEnvironment Env { get; }
|
||||
|
||||
@@ -67,8 +70,36 @@ namespace BTCPayServer.Plugins
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
public async Task DownloadRemotePlugin(string pluginIdentifier, string version)
|
||||
public async Task<AvailablePlugin> DownloadRemotePlugin(string pluginIdentifier, string version, VersionCondition condition = null)
|
||||
{
|
||||
if (version is null)
|
||||
{
|
||||
string btcpayVersion = Env.Version.TrimStart('v').Split('+')[0];
|
||||
var versions = await _pluginBuilderClient.GetPublishedVersions(
|
||||
btcpayVersion, _policiesSettings.PluginPreReleases, searchPluginIdentifier: pluginIdentifier, includeAllVersions: true);
|
||||
var potentialVersions = versions
|
||||
.Select(v => v.ManifestInfo?.ToObject<AvailablePlugin>())
|
||||
.Where(v => v is not null)
|
||||
.Where(v => v.Identifier == pluginIdentifier)
|
||||
.Select(v => v.Version)
|
||||
.ToList();
|
||||
if (potentialVersions.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException($"Plugin {pluginIdentifier} not found");
|
||||
}
|
||||
|
||||
if (condition is not null)
|
||||
{
|
||||
version = potentialVersions
|
||||
.OrderDescending()
|
||||
.FirstOrDefault(condition.IsFulfilled)?.ToString();
|
||||
if (version is null)
|
||||
{
|
||||
throw new InvalidOperationException($"No version of plugin {pluginIdentifier} can satisfy condition {condition}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var dest = _dataDirectories.Value.PluginDir;
|
||||
var filedest = Path.Join(dest, pluginIdentifier + ".btcpay");
|
||||
var filemanifestdest = Path.Join(dest, pluginIdentifier + ".json");
|
||||
@@ -82,19 +113,12 @@ namespace BTCPayServer.Plugins
|
||||
await using var fs = new FileStream(filedest, FileMode.Create, FileAccess.ReadWrite);
|
||||
await resp2.Content.CopyToAsync(fs);
|
||||
await fs.FlushAsync();
|
||||
return manifest;
|
||||
}
|
||||
|
||||
public void InstallPlugin(string plugin)
|
||||
{
|
||||
var dest = _dataDirectories.Value.PluginDir;
|
||||
UninstallPlugin(plugin);
|
||||
PluginManager.QueueCommands(dest, ("install", plugin));
|
||||
}
|
||||
|
||||
public void UpdatePlugin(string plugin)
|
||||
{
|
||||
var dest = _dataDirectories.Value.PluginDir;
|
||||
PluginManager.QueueCommands(dest, ("update", plugin));
|
||||
PluginManager.QueueCommands(_dataDirectories.Value.PluginDir, ("install", plugin));
|
||||
}
|
||||
|
||||
public async Task UploadPlugin(IFormFile plugin)
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
@{
|
||||
Layout = "_Layout";
|
||||
ViewData.SetActivePage(ServerNavPages.Plugins);
|
||||
var installed = Model.Installed.ToDictionary(plugin => plugin.Identifier, plugin => plugin.Version);
|
||||
var installed = Model.Installed;
|
||||
var availableAndNotInstalled = new List<PluginService.AvailablePlugin>();
|
||||
var availableAndNotInstalledx = Model.Available
|
||||
.Where(plugin => !installed.ContainsKey(plugin.Identifier))
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
bool DependentOn(string plugin)
|
||||
{
|
||||
foreach (var installedPlugin in Model.Installed)
|
||||
foreach (var installedPlugin in Model.Plugins)
|
||||
{
|
||||
if (installedPlugin.Dependencies.Any(dep => dep.Identifier.Equals(plugin, StringComparison.InvariantCultureIgnoreCase)))
|
||||
{
|
||||
@@ -121,11 +121,11 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (Model.Installed.Any())
|
||||
@if (Model.Plugins.Any())
|
||||
{
|
||||
<h3 class="mb-4">Installed Plugins</h3>
|
||||
<div class="row mb-4">
|
||||
@foreach (var plugin in Model.Installed.Where(i => !i.SystemPlugin))
|
||||
@foreach (var plugin in Model.Plugins.Where(i => !i.SystemPlugin))
|
||||
{
|
||||
Model.DownloadedPluginsByIdentifier.TryGetValue(plugin.Identifier, out var downloadInfo);
|
||||
var matchedAvailable = Model.Available.Where(availablePlugin => availablePlugin.Identifier == plugin.Identifier && availablePlugin.Version > plugin.Version).OrderByDescending(availablePlugin => availablePlugin.Version).ToArray();
|
||||
@@ -276,13 +276,13 @@
|
||||
{
|
||||
if (PluginManager.DependenciesMet(x.Dependencies, installed))
|
||||
{
|
||||
<form asp-action="InstallPlugin" asp-route-plugin="@plugin.Identifier" asp-route-version="@x.Version" asp-route-update="true" class="me-3">
|
||||
<form asp-action="InstallPlugin" asp-route-plugin="@plugin.Identifier" asp-route-version="@x.Version" class="me-3">
|
||||
<button type="submit" class="btn btn-secondary" text-translate="true">Update</button>
|
||||
</form>
|
||||
}
|
||||
else
|
||||
{
|
||||
<form asp-action="InstallPlugin" asp-route-plugin="@plugin.Identifier" asp-route-version="@x.Version" asp-route-update="true" class="me-3">
|
||||
<form asp-action="InstallPlugin" asp-route-plugin="@plugin.Identifier" asp-route-version="@x.Version" class="me-3">
|
||||
<button title="Schedule upgrade for when the dependencies have been met to ensure a smooth update" data-bs-toggle="tooltip" type="submit" class="btn btn-secondary" text-translate="true">Schedule update</button>
|
||||
</form>
|
||||
}
|
||||
@@ -473,7 +473,7 @@ else
|
||||
}
|
||||
else if (disabled != plugin.Version)
|
||||
{
|
||||
<form asp-action="InstallPlugin" asp-route-plugin="@plugin.Identifier" asp-route-version="@plugin.Version" asp-route-update="true">
|
||||
<form asp-action="InstallPlugin" asp-route-plugin="@plugin.Identifier" asp-route-version="@plugin.Version">
|
||||
<button type="submit" class="btn btn-primary" text-translate="true">Update</button>
|
||||
</form>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user