mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-17 22:14: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 Identifier { get; set; }
|
||||||
public string Condition { 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()
|
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 wid = new WalletObjectId(new WalletId("AAA", "ddd"), "a", "b");
|
||||||
var all = Enumerable.Range(0, 10)
|
var all = Enumerable.Range(0, 10)
|
||||||
|
#pragma warning disable CS0618 // Type or member is obsolete
|
||||||
.Select(i => walletRepo.ModifyWalletObjectData(wid, (o) => { o["idx"] = i; }))
|
.Select(i => walletRepo.ModifyWalletObjectData(wid, (o) => { o["idx"] = i; }))
|
||||||
|
#pragma warning restore CS0618 // Type or member is obsolete
|
||||||
.ToArray();
|
.ToArray();
|
||||||
foreach (var task in all)
|
foreach (var task in all)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ using System.Text;
|
|||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using BTCPayServer.Abstractions.Contracts;
|
||||||
using BTCPayServer.Abstractions.Extensions;
|
using BTCPayServer.Abstractions.Extensions;
|
||||||
using BTCPayServer.Client;
|
using BTCPayServer.Client;
|
||||||
using BTCPayServer.Client.Models;
|
using BTCPayServer.Client.Models;
|
||||||
@@ -488,6 +489,62 @@ namespace BTCPayServer.Tests
|
|||||||
Assert.Equal(Money.Satoshis(200), bump.NewTxFee);
|
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]
|
[Fact]
|
||||||
public void CanCalculateDust()
|
public void CanCalculateDust()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -40,7 +40,8 @@ namespace BTCPayServer.Controllers
|
|||||||
availablePluginsByIdentifier.TryAdd(p.Identifier, p);
|
availablePluginsByIdentifier.TryAdd(p.Identifier, p);
|
||||||
var res = new ListPluginsViewModel()
|
var res = new ListPluginsViewModel()
|
||||||
{
|
{
|
||||||
Installed = pluginService.LoadedPlugins,
|
Plugins = pluginService.LoadedPlugins,
|
||||||
|
Installed = pluginService.Installed,
|
||||||
Available = availablePlugins,
|
Available = availablePlugins,
|
||||||
Commands = pluginService.GetPendingCommands(),
|
Commands = pluginService.GetPendingCommands(),
|
||||||
Disabled = pluginService.GetDisabledPlugins(),
|
Disabled = pluginService.GetDisabledPlugins(),
|
||||||
@@ -52,12 +53,13 @@ namespace BTCPayServer.Controllers
|
|||||||
|
|
||||||
public class ListPluginsViewModel
|
public class ListPluginsViewModel
|
||||||
{
|
{
|
||||||
public IEnumerable<IBTCPayServerPlugin> Installed { get; set; }
|
public IEnumerable<IBTCPayServerPlugin> Plugins { get; set; }
|
||||||
public IEnumerable<PluginService.AvailablePlugin> Available { get; set; }
|
public IEnumerable<PluginService.AvailablePlugin> Available { get; set; }
|
||||||
public (string command, string plugin)[] Commands { get; set; }
|
public (string command, string plugin)[] Commands { get; set; }
|
||||||
public bool CanShowRestart { get; set; }
|
public bool CanShowRestart { get; set; }
|
||||||
public Dictionary<string, Version> Disabled { get; set; }
|
public Dictionary<string, Version> Disabled { get; set; }
|
||||||
public Dictionary<string, AvailablePlugin> DownloadedPluginsByIdentifier { get; set; } = new Dictionary<string, AvailablePlugin>();
|
public Dictionary<string, AvailablePlugin> DownloadedPluginsByIdentifier { get; set; } = new Dictionary<string, AvailablePlugin>();
|
||||||
|
public Dictionary<string, Version> Installed { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("server/plugins/uninstall-all")]
|
[HttpPost("server/plugins/uninstall-all")]
|
||||||
@@ -102,28 +104,24 @@ namespace BTCPayServer.Controllers
|
|||||||
public async Task<IActionResult> InstallPlugin(
|
public async Task<IActionResult> InstallPlugin(
|
||||||
[FromServices] PluginService pluginService, string plugin, bool update = false, string version = null)
|
[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
|
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||||
{
|
{
|
||||||
Message = StringLocalizer["Plugin scheduled to be installed."].Value,
|
Message = StringLocalizer["Plugin scheduled to be installed."].Value,
|
||||||
Severity = StatusMessageModel.StatusSeverity.Success
|
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
|
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
|
Severity = StatusMessageModel.StatusSeverity.Error
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -131,6 +129,60 @@ namespace BTCPayServer.Controllers
|
|||||||
return RedirectToAction("ListPlugins");
|
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")]
|
[HttpPost("server/plugins/upload")]
|
||||||
public async Task<IActionResult> UploadPlugin([FromServices] PluginService pluginService,
|
public async Task<IActionResult> UploadPlugin([FromServices] PluginService pluginService,
|
||||||
List<IFormFile> files)
|
List<IFormFile> files)
|
||||||
|
|||||||
@@ -83,8 +83,7 @@ namespace BTCPayServer.HostedServices
|
|||||||
dh.LastVersions ??= new Dictionary<string, Version>();
|
dh.LastVersions ??= new Dictionary<string, Version>();
|
||||||
var disabledPlugins = pluginService.GetDisabledPlugins();
|
var disabledPlugins = pluginService.GetDisabledPlugins();
|
||||||
|
|
||||||
var installedPlugins =
|
var installedPlugins = pluginService.Installed;
|
||||||
pluginService.LoadedPlugins.ToDictionary(plugin => plugin.Identifier, plugin => plugin.Version);
|
|
||||||
var remotePlugins = await pluginService.GetRemotePlugins(null);
|
var remotePlugins = await pluginService.GetRemotePlugins(null);
|
||||||
//take the latest version of each plugin
|
//take the latest version of each plugin
|
||||||
var remotePluginsList = remotePlugins
|
var remotePluginsList = remotePlugins
|
||||||
|
|||||||
@@ -55,13 +55,17 @@ namespace BTCPayServer.Plugins
|
|||||||
this.httpClient = httpClient;
|
this.httpClient = httpClient;
|
||||||
}
|
}
|
||||||
static JsonSerializerSettings serializerSettings = new() { ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver() };
|
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}";
|
var queryString = $"?includePreRelease={includePreRelease}";
|
||||||
if (btcpayVersion is not null)
|
if (btcpayVersion is not null)
|
||||||
queryString += $"&btcpayVersion={btcpayVersion}";
|
queryString += $"&btcpayVersion={btcpayVersion}";
|
||||||
if (searchPluginName is not null)
|
if (searchPluginName is not null)
|
||||||
queryString += $"&searchPluginName={searchPluginName}";
|
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}");
|
var result = await httpClient.GetStringAsync($"api/v1/plugins{queryString}");
|
||||||
return JsonConvert.DeserializeObject<PublishedVersion[]>(result, serializerSettings) ?? throw new InvalidOperationException();
|
return JsonConvert.DeserializeObject<PublishedVersion[]>(result, serializerSettings) ?? throw new InvalidOperationException();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ namespace BTCPayServer.Plugins
|
|||||||
});
|
});
|
||||||
logger.LogInformation($"Loading plugins from {pluginsFolder}");
|
logger.LogInformation($"Loading plugins from {pluginsFolder}");
|
||||||
Directory.CreateDirectory(pluginsFolder);
|
Directory.CreateDirectory(pluginsFolder);
|
||||||
ExecuteCommands(pluginsFolder);
|
ExecuteCommands(pluginsFolder, new());
|
||||||
|
|
||||||
var disabledPluginIdentifiers = GetDisabledPluginIdentifiers(pluginsFolder);
|
var disabledPluginIdentifiers = GetDisabledPluginIdentifiers(pluginsFolder);
|
||||||
var systemAssembly = typeof(Program).Assembly;
|
var systemAssembly = typeof(Program).Assembly;
|
||||||
@@ -348,7 +348,7 @@ namespace BTCPayServer.Plugins
|
|||||||
return GetPluginInstancesFromAssembly(assembly, silentlyFails).FirstOrDefault(plugin => plugin.Identifier == pluginIdentifier);
|
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);
|
var pendingCommands = GetPendingCommands(pluginsFolder);
|
||||||
if (!pendingCommands.Any())
|
if (!pendingCommands.Any())
|
||||||
@@ -411,13 +411,6 @@ namespace BTCPayServer.Plugins
|
|||||||
var dirName = Path.Combine(pluginsFolder, command.extension);
|
var dirName = Path.Combine(pluginsFolder, command.extension);
|
||||||
switch (command.command)
|
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":
|
case "delete":
|
||||||
ExecuteCommand(("enable", command.extension), pluginsFolder, installed);
|
ExecuteCommand(("enable", command.extension), pluginsFolder, installed);
|
||||||
if (File.Exists(dirName))
|
if (File.Exists(dirName))
|
||||||
@@ -433,12 +426,12 @@ namespace BTCPayServer.Plugins
|
|||||||
case "install":
|
case "install":
|
||||||
var fileName = dirName + BTCPayPluginSuffix;
|
var fileName = dirName + BTCPayPluginSuffix;
|
||||||
var manifestFileName = dirName + ".json";
|
var manifestFileName = dirName + ".json";
|
||||||
if (!DependenciesMet(pluginsFolder, command.extension, installed))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
ExecuteCommand(("enable", command.extension), pluginsFolder, installed);
|
ExecuteCommand(("enable", command.extension), pluginsFolder, installed);
|
||||||
|
|
||||||
if (File.Exists(fileName))
|
if (File.Exists(fileName))
|
||||||
{
|
{
|
||||||
|
if (File.Exists(dirName) || Directory.Exists(dirName))
|
||||||
|
ExecuteCommand(("delete", dirName), pluginsFolder, installed);
|
||||||
ZipFile.ExtractToDirectory(fileName, dirName, true);
|
ZipFile.ExtractToDirectory(fileName, dirName, true);
|
||||||
File.Delete(fileName);
|
File.Delete(fileName);
|
||||||
if (File.Exists(manifestFileName))
|
if (File.Exists(manifestFileName))
|
||||||
@@ -538,58 +531,15 @@ namespace BTCPayServer.Plugins
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static bool DependencyMet(IBTCPayServerPlugin.PluginDependency dependency,
|
public static bool DependencyMet(IBTCPayServerPlugin.PluginDependency dependency,
|
||||||
Dictionary<string, Version>? installed = null)
|
Dictionary<string, Version> installed)
|
||||||
{
|
{
|
||||||
var plugin = dependency.Identifier.ToLowerInvariant();
|
var condition = dependency.ParseCondition();
|
||||||
var versionReq = dependency.Condition;
|
if (!installed.TryGetValue(dependency.Identifier, out var v))
|
||||||
// ensure installed is not null and has lowercased keys for comparison
|
return condition is VersionCondition.Not;
|
||||||
installed = installed == null
|
return condition.IsFulfilled(v);
|
||||||
? 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;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool DependenciesMet(IEnumerable<IBTCPayServerPlugin.PluginDependency> dependencies,
|
public static bool DependenciesMet(IEnumerable<IBTCPayServerPlugin.PluginDependency> dependencies,
|
||||||
Dictionary<string, Version>? installed)
|
Dictionary<string, Version> installed) => dependencies.All(dependency => DependencyMet(dependency, installed));
|
||||||
{
|
|
||||||
return dependencies.All(dependency => DependencyMet(dependency, installed));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,12 +28,15 @@ namespace BTCPayServer.Plugins
|
|||||||
BTCPayServerEnvironment env)
|
BTCPayServerEnvironment env)
|
||||||
{
|
{
|
||||||
LoadedPlugins = btcPayServerPlugins;
|
LoadedPlugins = btcPayServerPlugins;
|
||||||
|
Installed = btcPayServerPlugins.ToDictionary(p => p.Identifier, p => p.Version, StringComparer.OrdinalIgnoreCase);
|
||||||
_pluginBuilderClient = pluginBuilderClient;
|
_pluginBuilderClient = pluginBuilderClient;
|
||||||
_dataDirectories = dataDirectories;
|
_dataDirectories = dataDirectories;
|
||||||
_policiesSettings = policiesSettings;
|
_policiesSettings = policiesSettings;
|
||||||
Env = env;
|
Env = env;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Dictionary<string, Version> Installed { get; set; }
|
||||||
|
|
||||||
public IEnumerable<IBTCPayServerPlugin> LoadedPlugins { get; }
|
public IEnumerable<IBTCPayServerPlugin> LoadedPlugins { get; }
|
||||||
public BTCPayServerEnvironment Env { get; }
|
public BTCPayServerEnvironment Env { get; }
|
||||||
|
|
||||||
@@ -67,8 +70,36 @@ namespace BTCPayServer.Plugins
|
|||||||
}).ToArray();
|
}).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 dest = _dataDirectories.Value.PluginDir;
|
||||||
var filedest = Path.Join(dest, pluginIdentifier + ".btcpay");
|
var filedest = Path.Join(dest, pluginIdentifier + ".btcpay");
|
||||||
var filemanifestdest = Path.Join(dest, pluginIdentifier + ".json");
|
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 using var fs = new FileStream(filedest, FileMode.Create, FileAccess.ReadWrite);
|
||||||
await resp2.Content.CopyToAsync(fs);
|
await resp2.Content.CopyToAsync(fs);
|
||||||
await fs.FlushAsync();
|
await fs.FlushAsync();
|
||||||
|
return manifest;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void InstallPlugin(string plugin)
|
public void InstallPlugin(string plugin)
|
||||||
{
|
{
|
||||||
var dest = _dataDirectories.Value.PluginDir;
|
PluginManager.QueueCommands(_dataDirectories.Value.PluginDir, ("install", plugin));
|
||||||
UninstallPlugin(plugin);
|
|
||||||
PluginManager.QueueCommands(dest, ("install", plugin));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UpdatePlugin(string plugin)
|
|
||||||
{
|
|
||||||
var dest = _dataDirectories.Value.PluginDir;
|
|
||||||
PluginManager.QueueCommands(dest, ("update", plugin));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task UploadPlugin(IFormFile plugin)
|
public async Task UploadPlugin(IFormFile plugin)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
@{
|
@{
|
||||||
Layout = "_Layout";
|
Layout = "_Layout";
|
||||||
ViewData.SetActivePage(ServerNavPages.Plugins);
|
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 availableAndNotInstalled = new List<PluginService.AvailablePlugin>();
|
||||||
var availableAndNotInstalledx = Model.Available
|
var availableAndNotInstalledx = Model.Available
|
||||||
.Where(plugin => !installed.ContainsKey(plugin.Identifier))
|
.Where(plugin => !installed.ContainsKey(plugin.Identifier))
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
bool DependentOn(string plugin)
|
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)))
|
if (installedPlugin.Dependencies.Any(dep => dep.Identifier.Equals(plugin, StringComparison.InvariantCultureIgnoreCase)))
|
||||||
{
|
{
|
||||||
@@ -121,11 +121,11 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (Model.Installed.Any())
|
@if (Model.Plugins.Any())
|
||||||
{
|
{
|
||||||
<h3 class="mb-4">Installed Plugins</h3>
|
<h3 class="mb-4">Installed Plugins</h3>
|
||||||
<div class="row mb-4">
|
<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);
|
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();
|
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))
|
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>
|
<button type="submit" class="btn btn-secondary" text-translate="true">Update</button>
|
||||||
</form>
|
</form>
|
||||||
}
|
}
|
||||||
else
|
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>
|
<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>
|
</form>
|
||||||
}
|
}
|
||||||
@@ -473,7 +473,7 @@ else
|
|||||||
}
|
}
|
||||||
else if (disabled != plugin.Version)
|
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>
|
<button type="submit" class="btn btn-primary" text-translate="true">Update</button>
|
||||||
</form>
|
</form>
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user