implement plugins section
This commit is contained in:
8
TorchRemote.Models/Responses/PluginInfo.cs
Normal file
8
TorchRemote.Models/Responses/PluginInfo.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace TorchRemote.Models.Responses;
|
||||
|
||||
public record PluginInfo(Guid Id, string Name, string Version);
|
||||
|
||||
public record PluginItemInfo(Guid Id, string Name, string Version, string Author) : PluginInfo(Id, Name, Version);
|
||||
|
||||
public record FullPluginItemInfo(Guid Id, string Name, string Description, string Version, string Author) : PluginItemInfo(Id, Name, Version, Author);
|
||||
public record InstalledPluginInfo(Guid Id, string Name, string Version, string? SettingId) : PluginInfo(Id, Name, Version);
|
49
TorchRemote.Plugin/Controllers/PluginDownloadsController.cs
Normal file
49
TorchRemote.Plugin/Controllers/PluginDownloadsController.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using EmbedIO;
|
||||
using EmbedIO.Routing;
|
||||
using EmbedIO.WebApi;
|
||||
using Torch.API.WebAPI;
|
||||
using TorchRemote.Models.Responses;
|
||||
using TorchRemote.Plugin.Utils;
|
||||
|
||||
namespace TorchRemote.Plugin.Controllers;
|
||||
|
||||
public class PluginDownloadsController : WebApiController
|
||||
{
|
||||
private const string RootPath = "/plugins/downloads";
|
||||
|
||||
[Route(HttpVerbs.Get, RootPath)]
|
||||
public async Task<IEnumerable<PluginInfo>> GetAsync()
|
||||
{
|
||||
var response = await PluginQuery.Instance.QueryAll();
|
||||
return response.Plugins.Select(b => new PluginItemInfo(Guid.Parse(b.ID), b.Name, b.LatestVersion, b.Author));
|
||||
}
|
||||
|
||||
[Route(HttpVerbs.Get, $"{RootPath}/{{id}}")]
|
||||
public async Task<FullPluginItemInfo> GetFullAsync(Guid id)
|
||||
{
|
||||
var response = await PluginQuery.Instance.QueryOne(id);
|
||||
|
||||
if (response is null)
|
||||
throw HttpException.NotFound("Plugin not found", id);
|
||||
|
||||
return new(Guid.Parse(response.ID), response.Name, response.Description, response.LatestVersion,
|
||||
response.Author);
|
||||
}
|
||||
|
||||
[Route(HttpVerbs.Post, $"{RootPath}/{{id}}/install")]
|
||||
public async Task InstallAsync(Guid id)
|
||||
{
|
||||
if (Statics.PluginManager.Plugins.ContainsKey(id))
|
||||
throw HttpException.BadRequest("Plugin with given id already exists", id);
|
||||
|
||||
var response = await PluginQuery.Instance.QueryOne(id);
|
||||
|
||||
if (response is null)
|
||||
throw HttpException.NotFound("Plugin not found", id);
|
||||
|
||||
if (!await PluginQuery.Instance.DownloadPlugin(response))
|
||||
throw HttpException.InternalServerError();
|
||||
|
||||
Statics.Torch.Config.Plugins.Add(id);
|
||||
}
|
||||
}
|
66
TorchRemote.Plugin/Controllers/PluginsController.cs
Normal file
66
TorchRemote.Plugin/Controllers/PluginsController.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
using System.IO;
|
||||
using EmbedIO;
|
||||
using EmbedIO.Routing;
|
||||
using EmbedIO.WebApi;
|
||||
using HttpMultipartParser;
|
||||
using Swan;
|
||||
using Torch.API.Managers;
|
||||
using Torch.Managers;
|
||||
using TorchRemote.Models.Responses;
|
||||
using TorchRemote.Plugin.Utils;
|
||||
|
||||
namespace TorchRemote.Plugin.Controllers;
|
||||
|
||||
public class PluginsController : WebApiController
|
||||
{
|
||||
private const string RootPath = "/plugins";
|
||||
|
||||
[Route(HttpVerbs.Get, RootPath)]
|
||||
public IEnumerable<InstalledPluginInfo> Get()
|
||||
{
|
||||
return Statics.PluginManager.Select(plugin => new InstalledPluginInfo(plugin.Id, plugin.Name, plugin.Version,
|
||||
Statics.SettingManager.PluginSettings!
|
||||
.GetValueOrDefault(plugin.Id)));
|
||||
}
|
||||
|
||||
[Route(HttpVerbs.Delete, $"{RootPath}/{{id}}")]
|
||||
public void Uninstall(Guid id)
|
||||
{
|
||||
foreach (var zip in Directory.EnumerateFiles(Statics.PluginManager.PluginDir, "*.zip"))
|
||||
{
|
||||
var manifest = PluginManifestUtils.ReadFromZip(zip);
|
||||
if (manifest.Guid != id)
|
||||
continue;
|
||||
|
||||
File.Delete(zip);
|
||||
return;
|
||||
}
|
||||
|
||||
throw HttpException.NotFound("Plugin zip with given id not found", id);
|
||||
}
|
||||
|
||||
[Route(HttpVerbs.Put, RootPath)]
|
||||
public async Task<IEnumerable<PluginInfo>> InstallAsync()
|
||||
{
|
||||
var payload = await MultipartFormDataParser.ParseAsync(Request.InputStream);
|
||||
|
||||
var pluginsToInstall = payload.Files.ToDictionary(f => PluginManifestUtils.ReadFromZip(f.Data));
|
||||
|
||||
if (pluginsToInstall.Keys.FirstOrDefault(m => Statics.PluginManager.Plugins.ContainsKey(m.Guid)) is { } m)
|
||||
throw HttpException.BadRequest("Plugin with given id already exists", m.Guid);
|
||||
|
||||
return pluginsToInstall.Select(b =>
|
||||
{
|
||||
var (manifest, file) = b;
|
||||
|
||||
var path = Path.Combine(Statics.PluginManager.PluginDir, $"{manifest.Name}.zip");
|
||||
|
||||
using var zipStream = File.Create(path);
|
||||
|
||||
file.Data.Position = 0;
|
||||
file.Data.CopyTo(zipStream);
|
||||
|
||||
return new PluginInfo(manifest.Guid, manifest.Name, manifest.Version);
|
||||
});
|
||||
}
|
||||
}
|
@@ -58,7 +58,9 @@ public class ApiServerManager : Manager
|
||||
.WithController<ServerController>()
|
||||
.WithController<SettingsController>()
|
||||
.WithController<WorldsController>()
|
||||
.WithController<ChatController>())
|
||||
.WithController<ChatController>()
|
||||
.WithController<PluginsController>()
|
||||
.WithController<PluginDownloadsController>())
|
||||
.WithModule(new LogsModule("/api/live/logs", true))
|
||||
.WithModule(chatModule)
|
||||
.WithBearerToken("/api", new SymmetricSecurityKey(Convert.FromBase64String(_config.SecurityKey)), new BasicAuthorizationServerProvider());
|
||||
|
@@ -71,7 +71,10 @@ public class SettingManager : Manager
|
||||
var persistentType = persistentInstance.GetType();
|
||||
var getter = persistentType.GetProperty("Data")!;
|
||||
|
||||
RegisterSetting(plugin.Name, getter.GetValue(persistentInstance), persistentType.GenericTypeArguments[0]);
|
||||
var settingType = persistentType.GenericTypeArguments[0];
|
||||
|
||||
RegisterSetting(plugin.Name, getter.GetValue(persistentInstance), settingType);
|
||||
PluginSettings.Add(plugin.Id, settingType.FullName!);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,6 +95,7 @@ public class SettingManager : Manager
|
||||
}
|
||||
|
||||
public IDictionary<string, Setting> Settings { get; } = new ConcurrentDictionary<string, Setting>();
|
||||
public IDictionary<Guid, string> PluginSettings { get; } = new ConcurrentDictionary<Guid, string>();
|
||||
}
|
||||
|
||||
public record Setting(string Name, Type Type, JsonSchema Schema, object Value);
|
@@ -35,6 +35,7 @@
|
||||
<HintPath>$(TorchDir)DedicatedServer64\System.Memory.dll</HintPath>
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
<Reference Include="System.IO.Compression" />
|
||||
<Reference Include="System.Net.Http" />
|
||||
<Reference Include="Torch">
|
||||
<HintPath>$(TorchDir)Torch.dll</HintPath>
|
||||
@@ -77,6 +78,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="EmbedIO" Version="3.4.3" />
|
||||
<PackageReference Include="EmbedIO.BearerToken" Version="3.4.2" />
|
||||
<PackageReference Include="HttpMultipartParser" Version="7.0.0" />
|
||||
<PackageReference Include="Json.More.Net" Version="1.7.1" />
|
||||
<PackageReference Include="JsonPointer.Net" Version="2.2.2" />
|
||||
<PackageReference Include="JsonSchema.Net" Version="3.2.2" />
|
||||
|
29
TorchRemote.Plugin/Utils/PluginManifestUtils.cs
Normal file
29
TorchRemote.Plugin/Utils/PluginManifestUtils.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Xml.Serialization;
|
||||
using Torch;
|
||||
|
||||
namespace TorchRemote.Plugin.Utils;
|
||||
|
||||
public static class PluginManifestUtils
|
||||
{
|
||||
private static readonly XmlSerializer Serializer = new(typeof(PluginManifest));
|
||||
|
||||
public static PluginManifest Read(Stream stream)
|
||||
{
|
||||
return (PluginManifest)Serializer.Deserialize(stream);
|
||||
}
|
||||
|
||||
public static PluginManifest ReadFromZip(Stream stream)
|
||||
{
|
||||
using var archive = new ZipArchive(stream);
|
||||
using var entryStream = archive.GetEntry("manifest.xml")!.Open();
|
||||
return Read(entryStream);
|
||||
}
|
||||
|
||||
public static PluginManifest ReadFromZip(string archivePath)
|
||||
{
|
||||
using var stream = File.OpenRead(archivePath);
|
||||
return ReadFromZip(stream);
|
||||
}
|
||||
}
|
@@ -3,6 +3,7 @@ using Torch;
|
||||
using Torch.API;
|
||||
using Torch.API.Managers;
|
||||
using Torch.Commands;
|
||||
using Torch.Managers;
|
||||
using Torch.Server;
|
||||
using Torch.Server.Managers;
|
||||
using TorchRemote.Plugin.Managers;
|
||||
@@ -18,6 +19,7 @@ internal static class Statics
|
||||
public static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web);
|
||||
public static SettingManager SettingManager => Torch.Managers.GetManager<SettingManager>();
|
||||
public static InstanceManager InstanceManager => Torch.Managers.GetManager<InstanceManager>();
|
||||
public static PluginManager PluginManager => Torch.Managers.GetManager<PluginManager>();
|
||||
public static CommandManager? CommandManager => Torch.CurrentSession?.Managers.GetManager<CommandManager>();
|
||||
|
||||
public static ChatModule ChatModule = null!;
|
||||
|
@@ -2,5 +2,5 @@
|
||||
<PluginManifest xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<Name>Torch Remote</Name>
|
||||
<Guid>284017F3-9682-4841-A544-EB04DB8CB9BA</Guid>
|
||||
<Version>v1.0.2</Version>
|
||||
<Version>v1.0.3</Version>
|
||||
</PluginManifest>
|
Reference in New Issue
Block a user