From 1e297191259c228c08efa7995db385a01322e29a Mon Sep 17 00:00:00 2001 From: zznty <94796179+zznty@users.noreply.github.com> Date: Tue, 4 Oct 2022 19:31:59 +0700 Subject: [PATCH] implement plugins section --- TorchRemote.Models/Responses/PluginInfo.cs | 8 +++ .../Controllers/PluginDownloadsController.cs | 49 ++++++++++++++ .../Controllers/PluginsController.cs | 66 +++++++++++++++++++ .../Managers/ApiServerManager.cs | 4 +- TorchRemote.Plugin/Managers/SettingManager.cs | 6 +- TorchRemote.Plugin/TorchRemote.Plugin.csproj | 2 + .../Utils/PluginManifestUtils.cs | 29 ++++++++ TorchRemote.Plugin/Utils/Statics.cs | 2 + TorchRemote.Plugin/manifest.xml | 2 +- 9 files changed, 165 insertions(+), 3 deletions(-) create mode 100644 TorchRemote.Models/Responses/PluginInfo.cs create mode 100644 TorchRemote.Plugin/Controllers/PluginDownloadsController.cs create mode 100644 TorchRemote.Plugin/Controllers/PluginsController.cs create mode 100644 TorchRemote.Plugin/Utils/PluginManifestUtils.cs diff --git a/TorchRemote.Models/Responses/PluginInfo.cs b/TorchRemote.Models/Responses/PluginInfo.cs new file mode 100644 index 0000000..3355692 --- /dev/null +++ b/TorchRemote.Models/Responses/PluginInfo.cs @@ -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); \ No newline at end of file diff --git a/TorchRemote.Plugin/Controllers/PluginDownloadsController.cs b/TorchRemote.Plugin/Controllers/PluginDownloadsController.cs new file mode 100644 index 0000000..fa42b36 --- /dev/null +++ b/TorchRemote.Plugin/Controllers/PluginDownloadsController.cs @@ -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> 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 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); + } +} \ No newline at end of file diff --git a/TorchRemote.Plugin/Controllers/PluginsController.cs b/TorchRemote.Plugin/Controllers/PluginsController.cs new file mode 100644 index 0000000..4ddb63b --- /dev/null +++ b/TorchRemote.Plugin/Controllers/PluginsController.cs @@ -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 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> 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); + }); + } +} \ No newline at end of file diff --git a/TorchRemote.Plugin/Managers/ApiServerManager.cs b/TorchRemote.Plugin/Managers/ApiServerManager.cs index ac36417..e7a9a77 100644 --- a/TorchRemote.Plugin/Managers/ApiServerManager.cs +++ b/TorchRemote.Plugin/Managers/ApiServerManager.cs @@ -58,7 +58,9 @@ public class ApiServerManager : Manager .WithController() .WithController() .WithController() - .WithController()) + .WithController() + .WithController() + .WithController()) .WithModule(new LogsModule("/api/live/logs", true)) .WithModule(chatModule) .WithBearerToken("/api", new SymmetricSecurityKey(Convert.FromBase64String(_config.SecurityKey)), new BasicAuthorizationServerProvider()); diff --git a/TorchRemote.Plugin/Managers/SettingManager.cs b/TorchRemote.Plugin/Managers/SettingManager.cs index 269078e..f3ee387 100644 --- a/TorchRemote.Plugin/Managers/SettingManager.cs +++ b/TorchRemote.Plugin/Managers/SettingManager.cs @@ -70,8 +70,11 @@ public class SettingManager : Manager var persistentType = persistentInstance.GetType(); var getter = persistentType.GetProperty("Data")!; + + var settingType = persistentType.GenericTypeArguments[0]; - RegisterSetting(plugin.Name, getter.GetValue(persistentInstance), 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 Settings { get; } = new ConcurrentDictionary(); + public IDictionary PluginSettings { get; } = new ConcurrentDictionary(); } public record Setting(string Name, Type Type, JsonSchema Schema, object Value); \ No newline at end of file diff --git a/TorchRemote.Plugin/TorchRemote.Plugin.csproj b/TorchRemote.Plugin/TorchRemote.Plugin.csproj index d6a4e47..c076571 100644 --- a/TorchRemote.Plugin/TorchRemote.Plugin.csproj +++ b/TorchRemote.Plugin/TorchRemote.Plugin.csproj @@ -35,6 +35,7 @@ $(TorchDir)DedicatedServer64\System.Memory.dll False + $(TorchDir)Torch.dll @@ -77,6 +78,7 @@ + diff --git a/TorchRemote.Plugin/Utils/PluginManifestUtils.cs b/TorchRemote.Plugin/Utils/PluginManifestUtils.cs new file mode 100644 index 0000000..0979f81 --- /dev/null +++ b/TorchRemote.Plugin/Utils/PluginManifestUtils.cs @@ -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); + } +} \ No newline at end of file diff --git a/TorchRemote.Plugin/Utils/Statics.cs b/TorchRemote.Plugin/Utils/Statics.cs index 30a436e..dd27a03 100644 --- a/TorchRemote.Plugin/Utils/Statics.cs +++ b/TorchRemote.Plugin/Utils/Statics.cs @@ -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(); public static InstanceManager InstanceManager => Torch.Managers.GetManager(); + public static PluginManager PluginManager => Torch.Managers.GetManager(); public static CommandManager? CommandManager => Torch.CurrentSession?.Managers.GetManager(); public static ChatModule ChatModule = null!; diff --git a/TorchRemote.Plugin/manifest.xml b/TorchRemote.Plugin/manifest.xml index afbc52c..4efcad2 100644 --- a/TorchRemote.Plugin/manifest.xml +++ b/TorchRemote.Plugin/manifest.xml @@ -2,5 +2,5 @@ Torch Remote 284017F3-9682-4841-A544-EB04DB8CB9BA - v1.0.2 + v1.0.3 \ No newline at end of file