diff --git a/src/Core/ExtensionsManager.cs b/src/Core/ExtensionsManager.cs index 7bbfde49a..1b585b910 100644 --- a/src/Core/ExtensionsManager.cs +++ b/src/Core/ExtensionsManager.cs @@ -1,4 +1,4 @@ -using FreneticUtilities.FreneticDataSyntax; +using FreneticUtilities.FreneticDataSyntax; using FreneticUtilities.FreneticExtensions; using Microsoft.AspNetCore.Html; using SwarmUI.Utils; @@ -62,6 +62,8 @@ public async Task PrepExtensions() string[] extras = Directory.Exists("./src/Extensions") ? [.. Directory.EnumerateDirectories("./src/Extensions/").Select(s => "src/" + s.Replace('\\', '/').AfterLast("/src/"))] : []; string[] deleteMe = [.. extras.Where(e => e.TrimEnd('/').EndsWith(".delete"))]; extras = [.. extras.Where(e => !e.TrimEnd('/').EndsWith(".delete") && !e.TrimEnd('/').EndsWith(".disable"))]; + HashSet disabledFolders = [.. Program.ServerSettings.Extensions.DisabledExtensions]; + extras = [.. extras.Where(e => !disabledFolders.Contains(e.AfterLast('/')))]; foreach (string deletable in deleteMe) { try @@ -267,4 +269,37 @@ public T GetExtension() where T : Extension { return Extensions.FirstOrDefault(e => e is T) as T; } + + /// Returns folder name from an extension path. + public static string GetFolderNameFromPath(string path) + { + return path?.Replace('\\', '/').TrimEnd('/').AfterLast('/') ?? ""; + } + + /// Returns disabled extensions for UI display. + public IEnumerable GetDisabledExtensionsForUi() + { + foreach (string folderName in Program.ServerSettings.Extensions.DisabledExtensions.OrderBy(e => e)) + { + ExtensionInfo info = KnownExtensions.FirstOrDefault(e => e.FolderName == folderName); + info ??= new ExtensionInfo(folderName, "(Unknown)", "(Unknown)", "(Disabled - restart to load)", "", ["none"], folderName); + yield return info; + } + } + + /// Removes an extension folder from the disabled list in settings. + public bool RemoveDisabledExtensionSetting(string folderName) + { + return Program.ServerSettings.Extensions.DisabledExtensions.Remove(folderName); + } + + /// Adds an extension folder to the disabled list in settings. + public bool AddDisabledExtensionSetting(string folderName) + { + if (!Program.ServerSettings.Extensions.DisabledExtensions.Contains(folderName)) + { + Program.ServerSettings.Extensions.DisabledExtensions.Add(folderName); + } + return true; + } } diff --git a/src/Core/Settings.cs b/src/Core/Settings.cs index 08559d3fb..a34225e56 100644 --- a/src/Core/Settings.cs +++ b/src/Core/Settings.cs @@ -1,4 +1,4 @@ -using FreneticUtilities.FreneticDataSyntax; +using FreneticUtilities.FreneticDataSyntax; using SwarmUI.Backends; using SwarmUI.Media; using SwarmUI.Utils; @@ -27,6 +27,9 @@ public class Settings : AutoConfiguration [ConfigComment("Settings related to backends.")] public BackendData Backends = new(); + [ConfigComment("Settings related to extensions.")] + public ExtensionsData Extensions = new(); + [ConfigComment("If this is set to 'true', hides the installer page. If 'false', the installer page will be shown.")] [SettingHidden] public bool IsInstalled = false; @@ -67,6 +70,14 @@ public class Settings : AutoConfiguration [ConfigComment("Settings related to server performance.")] public PerformanceData Performance = new(); + /// Settings related to extensions. + public class ExtensionsData : AutoConfiguration + { + [ConfigComment("List of disabled extension folder names.\nDisabled extensions remain installed on disk, but are not loaded at server startup.")] + [SettingHidden] + public List DisabledExtensions = []; + } + /// Settings related to Swarm server maintenance.. public class ServerMaintenanceData : AutoConfiguration { diff --git a/src/Pages/_Generate/ServerTab.cshtml b/src/Pages/_Generate/ServerTab.cshtml index 490ba29a0..062b23d03 100644 --- a/src/Pages/_Generate/ServerTab.cshtml +++ b/src/Pages/_Generate/ServerTab.cshtml @@ -233,6 +233,7 @@ @(ext.ReadmeURL == "" ? "(Missing)": new HtmlString($"Here")) @ext.License + @if (ext.CanUpdate) { @@ -241,6 +242,22 @@ } + @foreach (ExtensionsManager.ExtensionInfo ext in Program.Extensions.GetDisabledExtensionsForUi()) + { + + @ext.Name + (Disabled) + @ExtensionsManager.HtmlTags(ext.Tags) + @ext.Author + @ext.Description + @(ext.URL == "" ? "(Missing)" : new HtmlString($"Here")) + @ext.License + + + + + + }

Available Extensions

diff --git a/src/WebAPI/AdminAPI.cs b/src/WebAPI/AdminAPI.cs index 1074a72dc..1378ca156 100644 --- a/src/WebAPI/AdminAPI.cs +++ b/src/WebAPI/AdminAPI.cs @@ -36,6 +36,7 @@ public static void Register() API.RegisterAPICall(InstallExtension, true, Permissions.ManageExtensions); API.RegisterAPICall(UpdateExtension, true, Permissions.ManageExtensions); API.RegisterAPICall(UninstallExtension, true, Permissions.ManageExtensions); + API.RegisterAPICall(SetExtensionEnabled, true, Permissions.ManageExtensions); API.RegisterAPICall(AdminListUsers, false, Permissions.ManageUsers); API.RegisterAPICall(AdminAddUser, true, Permissions.ManageUsers); API.RegisterAPICall(AdminSetUserPassword, true, Permissions.ManageUsers); @@ -712,11 +713,50 @@ public static async Task InstallExtension(Session session, { return new JObject() { ["error"] = "Extension already installed." }; } + Program.Extensions.RemoveDisabledExtensionSetting(ext.FolderName); + Program.SaveSettingsFile(); await Utilities.RunGitProcess($"clone {ext.URL}", extensionsFolder); return new JObject() { ["success"] = true }; } - [API.APIDescription("Triggers an extension update for an installed extension. Does not trigger a restart.", + [API.APIDescription("Enables or disables an installed extension. Does not trigger a restart.", + """ + "success": true + """)] + public static async Task SetExtensionEnabled(Session session, + [API.APIParameter("The extension name (disable) or folder name (enable).")] string extensionName, + [API.APIParameter("True to enable the extension, false to disable it.")] bool enabled) + { + if (enabled) + { + if (!Program.Extensions.RemoveDisabledExtensionSetting(extensionName)) + { + return new JObject() { ["error"] = "Unknown extension." }; + } + } + else + { + Extension extension = Program.Extensions.Extensions.FirstOrDefault(e => string.Equals(e.ExtensionName, extensionName, StringComparison.OrdinalIgnoreCase)); + if (extension is null) + { + return new JObject() { ["error"] = "Unknown extension." }; + } + if (extension.IsCore) + { + return new JObject() { ["error"] = "Core extensions cannot be enabled/disabled." }; + } + if (!Program.Extensions.AddDisabledExtensionSetting(ExtensionsManager.GetFolderNameFromPath(extension.FilePath))) + { + return new JObject() { ["error"] = "Unknown extension." }; + } + } + Program.SaveSettingsFile(); + File.WriteAllText("src/bin/must_rebuild", "yes"); + Logs.Debug($"User {session.User.UserID} {(enabled ? "enabled" : "disabled")} extension '{extensionName}'. Restart required to apply."); + return new JObject() { ["success"] = true }; + } + + [API.APIDescription("Triggers an extension update for an installed extension. Does not trigger a restart. Does signal required rebuild.", """ "success": true // or false if no update available """)] @@ -745,19 +785,24 @@ public static async Task UpdateExtension(Session session, "success": true """)] public static async Task UninstallExtension(Session session, - [API.APIParameter("The name of the extension to uninstall.")] string extensionName) + [API.APIParameter("The name (if loaded) or folder name (if disabled) of the extension to uninstall.")] string extensionName) { Extension ext = Program.Extensions.Extensions.FirstOrDefault(e => e.ExtensionName == extensionName); - if (ext is null) + string folder = ext?.FilePath; + if (folder is null) { - return new JObject() { ["error"] = "Unknown extension." }; + folder = $"src/Extensions/{extensionName}/"; } - string path = Path.GetFullPath(Utilities.CombinePathWithAbsolute(Environment.CurrentDirectory, ext.FilePath)); - Logs.Debug($"Will clear out Extension path: {path}"); + string path = Path.GetFullPath(Utilities.CombinePathWithAbsolute(Environment.CurrentDirectory, folder)); if (!Directory.Exists(path)) { - return new JObject() { ["error"] = "Extension has invalid path, cannot delete." }; + return new JObject() { ["error"] = "Unknown extension." }; } + if (Program.Extensions.RemoveDisabledExtensionSetting(ExtensionsManager.GetFolderNameFromPath(folder))) + { + Program.SaveSettingsFile(); + } + Logs.Debug($"Will clear out Extension path: {path}"); try { FileSystem.DeleteDirectory(path, UIOption.OnlyErrorDialogs, RecycleOption.SendToRecycleBin, UICancelOption.ThrowException); diff --git a/src/wwwroot/js/genpage/server/servertab.js b/src/wwwroot/js/genpage/server/servertab.js index ce11a32c9..0817e71b0 100644 --- a/src/wwwroot/js/genpage/server/servertab.js +++ b/src/wwwroot/js/genpage/server/servertab.js @@ -57,6 +57,20 @@ class ExtensionsManager { button.disabled = false; }); } + + setExtensionEnabled(extensionName, enabled, button) { + button.disabled = true; + button.parentElement.querySelectorAll('.installing_info').forEach(e => e.remove()); + let infoDiv = createDiv(null, 'installing_info', (enabled ? 'Enabling' : 'Disabling') + ' (restart required)...'); + button.parentElement.appendChild(infoDiv); + genericRequest('SetExtensionEnabled', {'extensionName': extensionName, 'enabled': enabled}, data => { + button.parentElement.innerHTML = (enabled ? 'Enabled' : 'Disabled') + ', restart to apply'; + this.newInstallsCard.style.display = 'block'; + }, 0, e => { + infoDiv.innerText = (enabled ? 'Failed to enable: ' : 'Failed to disable: ') + e; + button.disabled = false; + }); + } } extensionsManager = new ExtensionsManager();