Installing a plugin should install all plugin dependencies

This commit is contained in:
nicolas.dorier
2025-07-18 19:26:30 +09:00
parent e77d785358
commit 7273e9953f
10 changed files with 342 additions and 101 deletions

View File

@@ -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}";
} }
} }
} }

View 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);
}

View File

@@ -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)
{ {

View File

@@ -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()
{ {

View File

@@ -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)

View File

@@ -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

View File

@@ -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();
} }

View File

@@ -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));
}
} }
} }

View File

@@ -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)

View File

@@ -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>
} }