From 61f16d1711c8d65c4f426373e06f72401b2bd261 Mon Sep 17 00:00:00 2001 From: Juan Treminio Date: Thu, 5 Feb 2026 18:40:15 -0600 Subject: [PATCH 1/5] Allows enabling/disabling installed extensions --- src/Core/ExtensionsManager.cs | 87 +++++++++++++++++++++- src/Core/Settings.cs | 13 +++- src/Pages/_Generate/ServerTab.cshtml | 19 ++++- src/WebAPI/AdminAPI.cs | 40 +++++++++- src/wwwroot/js/genpage/server/servertab.js | 14 ++++ 5 files changed, 167 insertions(+), 6 deletions(-) diff --git a/src/Core/ExtensionsManager.cs b/src/Core/ExtensionsManager.cs index 7bbfde49a..5abd07cb5 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; @@ -15,6 +15,12 @@ public class ExtensionsManager /// Hashset of folder names of all extensions currently loaded. public HashSet LoadedExtensionFolders = []; + /// Hashset of folder names of all extensions currently installed (loaded or disabled). + public HashSet InstalledExtensionFolders = new(StringComparer.OrdinalIgnoreCase); + + /// Dictionary of disabled extension name to its folder name. + public Dictionary DisabledExtensions = new(StringComparer.OrdinalIgnoreCase); + /// Simple holder of information about extensions available online. public record class ExtensionInfo(string Name, string Author, string License, string Description, string URL, string[] Tags, string FolderName) { @@ -62,6 +68,9 @@ 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"))]; + InstalledExtensionFolders = new HashSet(extras.Select(e => e.AfterLast('/')), StringComparer.OrdinalIgnoreCase); + List disabledExtras = BuildDisabledExtensionsAndGetDisabledExtras(extras); + extras = [.. extras.Where(e => !disabledExtras.Contains(e))]; foreach (string deletable in deleteMe) { try @@ -267,4 +276,80 @@ public T GetExtension() where T : Extension { return Extensions.FirstOrDefault(e => e is T) as T; } + + /// Returns disabled extensions for UI display. + public IEnumerable GetDisabledExtensionsForUi() + { + foreach (string name in DisabledExtensions.Keys.OrderBy(n => n)) + { + DisabledExtensions.TryGetValue(name, out string folderName); + ExtensionInfo info = folderName is null ? null : KnownExtensions.FirstOrDefault(e => string.Equals(e.FolderName, folderName, StringComparison.OrdinalIgnoreCase)); + info ??= new ExtensionInfo(name, "(Unknown)", "(Unknown)", "(Disabled - restart to load)", "", ["none"], folderName ?? ""); + yield return info; + } + } + + /// + /// Builds as a map of extension name -> folder name, + /// and returns a list of disabled entries in the same format as . + /// + public List BuildDisabledExtensionsAndGetDisabledExtras(string[] extras) + { + static string DetectNameFromFolder(string extDir) + { + try + { + if (!Directory.Exists(extDir)) + { + return null; + } + string[] csFiles = Directory.EnumerateFiles(extDir, "*.cs", SearchOption.TopDirectoryOnly) + .Where(f => !f.EndsWith($"{Path.DirectorySeparatorChar}AssemblyInfo.cs") && !f.EndsWith("/AssemblyInfo.cs")) + .ToArray(); + if (csFiles.IsEmpty()) + { + return null; + } + string preferred = csFiles + .OrderBy(f => f.EndsWith("Extension.cs", StringComparison.OrdinalIgnoreCase) ? 0 : 1) + .ThenBy(f => Path.GetFileNameWithoutExtension(f).Length) + .Select(Path.GetFileNameWithoutExtension) + .FirstOrDefault(); + return string.IsNullOrWhiteSpace(preferred) ? null : preferred; + } + catch (Exception) + { + return null; + } + } + + DisabledExtensions = new(StringComparer.OrdinalIgnoreCase); + HashSet disabledNames = new((Program.ServerSettings?.Extensions?.DisabledExtensions ?? []) + .Where(s => !string.IsNullOrWhiteSpace(s)) + .Select(s => s.Trim()), + StringComparer.OrdinalIgnoreCase); + if (disabledNames.IsEmpty()) + { + return []; + } + List disabledExtras = []; + foreach (string extDir in extras) + { + string detectedName = DetectNameFromFolder(extDir)?.Trim(); + if (string.IsNullOrWhiteSpace(detectedName) || !disabledNames.Contains(detectedName)) + { + continue; + } + DisabledExtensions[detectedName] = extDir.TrimEnd('/').AfterLast('/'); + disabledExtras.Add(extDir); + } + return disabledExtras; + } + + /// Removes an extension name from the disabled list in settings and saves. + public void RemoveDisabledExtensionSetting(string extensionName) + { + Program.ServerSettings.Extensions.DisabledExtensions.RemoveAll(n => string.Equals(n, extensionName, StringComparison.OrdinalIgnoreCase)); + Program.SaveSettingsFile(); + } } diff --git a/src/Core/Settings.cs b/src/Core/Settings.cs index 08559d3fb..381f7db7e 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 extension names that are disabled.\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..05ccfb554 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

@@ -254,7 +271,7 @@ License Actions - @foreach (ExtensionsManager.ExtensionInfo ext in Program.Extensions.KnownExtensions.Where(e => !Program.Extensions.LoadedExtensionFolders.Contains(e.FolderName) && !e.Tags.Contains("hidden"))) + @foreach (ExtensionsManager.ExtensionInfo ext in Program.Extensions.KnownExtensions.Where(e => !Program.Extensions.InstalledExtensionFolders.Contains(e.FolderName) && !e.Tags.Contains("hidden"))) { @ext.Name diff --git a/src/WebAPI/AdminAPI.cs b/src/WebAPI/AdminAPI.cs index 1074a72dc..90b8ef33b 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); @@ -706,6 +707,7 @@ public static async Task InstallExtension(Session session, { return new JObject() { ["error"] = "Unknown extension." }; } + Program.Extensions.RemoveDisabledExtensionSetting(extensionName); string extensionsFolder = Utilities.CombinePathWithAbsolute(Environment.CurrentDirectory, "src/Extensions"); string folder = Utilities.CombinePathWithAbsolute(extensionsFolder, ext.FolderName); if (Directory.Exists(folder)) @@ -716,7 +718,30 @@ public static async Task InstallExtension(Session session, 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 by name. Does not trigger a restart. Does signal required rebuild.", + """ + "success": true + """)] + public static async Task SetExtensionEnabled(Session session, + [API.APIParameter("The name of the extension to enable/disable.")] string extensionName, + [API.APIParameter("True to enable the extension, false to disable it.")] bool enabled) + { + if (Program.Extensions.Extensions.Any(e => e.IsCore && string.Equals(e.ExtensionName, extensionName, StringComparison.OrdinalIgnoreCase))) + { + return new JObject() { ["error"] = "Core extensions cannot be enabled/disabled." }; + } + Program.ServerSettings.Extensions.DisabledExtensions.RemoveAll(n => string.Equals(n, extensionName, StringComparison.OrdinalIgnoreCase)); + if (!enabled) + { + Program.ServerSettings.Extensions.DisabledExtensions.Add(extensionName); + } + 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 """)] @@ -748,11 +773,20 @@ public static async Task UninstallExtension(Session session, [API.APIParameter("The name 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) + { + if (Program.Extensions.DisabledExtensions.TryGetValue(extensionName, out string folderName)) + { + folder = $"src/Extensions/{folderName}/"; + } + } + if (folder is null) { return new JObject() { ["error"] = "Unknown extension." }; } - string path = Path.GetFullPath(Utilities.CombinePathWithAbsolute(Environment.CurrentDirectory, ext.FilePath)); + Program.Extensions.RemoveDisabledExtensionSetting(extensionName); + string path = Path.GetFullPath(Utilities.CombinePathWithAbsolute(Environment.CurrentDirectory, folder)); Logs.Debug($"Will clear out Extension path: {path}"); if (!Directory.Exists(path)) { diff --git a/src/wwwroot/js/genpage/server/servertab.js b/src/wwwroot/js/genpage/server/servertab.js index ce11a32c9..2fb235804 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(name, 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': name, '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(); From e4d49efc6d21fd43a0d6a32037aa32c255b06213 Mon Sep 17 00:00:00 2001 From: Juan Treminio Date: Fri, 6 Feb 2026 19:40:36 -0600 Subject: [PATCH 2/5] Refactored to simplified version --- src/Core/ExtensionsManager.cs | 141 +++++++++++++++++++--------------- src/Core/Settings.cs | 4 +- src/WebAPI/AdminAPI.cs | 16 ++-- 3 files changed, 92 insertions(+), 69 deletions(-) diff --git a/src/Core/ExtensionsManager.cs b/src/Core/ExtensionsManager.cs index 5abd07cb5..be51e8038 100644 --- a/src/Core/ExtensionsManager.cs +++ b/src/Core/ExtensionsManager.cs @@ -16,10 +16,10 @@ public class ExtensionsManager public HashSet LoadedExtensionFolders = []; /// Hashset of folder names of all extensions currently installed (loaded or disabled). - public HashSet InstalledExtensionFolders = new(StringComparer.OrdinalIgnoreCase); + public HashSet InstalledExtensionFolders = []; - /// Dictionary of disabled extension name to its folder name. - public Dictionary DisabledExtensions = new(StringComparer.OrdinalIgnoreCase); + /// Dictionary of disabled extension folder name to extension name. + public Dictionary DisabledExtensions = []; /// Simple holder of information about extensions available online. public record class ExtensionInfo(string Name, string Author, string License, string Description, string URL, string[] Tags, string FolderName) @@ -69,8 +69,9 @@ public async Task PrepExtensions() string[] deleteMe = [.. extras.Where(e => e.TrimEnd('/').EndsWith(".delete"))]; extras = [.. extras.Where(e => !e.TrimEnd('/').EndsWith(".delete") && !e.TrimEnd('/').EndsWith(".disable"))]; InstalledExtensionFolders = new HashSet(extras.Select(e => e.AfterLast('/')), StringComparer.OrdinalIgnoreCase); - List disabledExtras = BuildDisabledExtensionsAndGetDisabledExtras(extras); - extras = [.. extras.Where(e => !disabledExtras.Contains(e))]; + LoadKnownExtensions(); + HashSet disabledFolders = BuildDisabledExtensionsAndGetDisabledFolders(); + extras = [.. extras.Where(e => !disabledFolders.Contains(e.AfterLast('/')))]; foreach (string deletable in deleteMe) { try @@ -140,6 +141,13 @@ public async Task PrepExtensions() catch (Exception) { } } RunOnAllExtensions(e => e.OnFirstInit()); + RunOnAllExtensions(e => e.PopulateMetadata()); + } + + /// Loads known extension metadata from the extension list file. + public void LoadKnownExtensions() + { + KnownExtensions.Clear(); try { FDSSection extensionsOutThere = FDSUtility.ReadFile("./launchtools/extension_list.fds"); @@ -154,7 +162,6 @@ public async Task PrepExtensions() { Logs.Error($"Failed to read known extensions list: {ex.ReadableString()}"); } - RunOnAllExtensions(e => e.PopulateMetadata()); } public async Task BuildExtension(string folder, string projFile) @@ -277,79 +284,91 @@ public T GetExtension() where T : Extension return Extensions.FirstOrDefault(e => e is T) as T; } + /// Builds and returns disabled extension folders. + public HashSet BuildDisabledExtensionsAndGetDisabledFolders() + { + if (Program.ServerSettings?.Extensions.DisabledExtensions is null) + { + Program.ServerSettings.Extensions.DisabledExtensions = []; + } + DisabledExtensions = []; + HashSet disabledFolders = []; + foreach ((string folderName, string extensionName) in Program.ServerSettings.Extensions.DisabledExtensions) + { + if (string.IsNullOrWhiteSpace(extensionName) || string.IsNullOrWhiteSpace(folderName) + || !InstalledExtensionFolders.Contains(folderName) || !disabledFolders.Add(folderName)) + { + continue; + } + DisabledExtensions[folderName] = extensionName; + } + return disabledFolders; + } + /// Returns disabled extensions for UI display. public IEnumerable GetDisabledExtensionsForUi() { - foreach (string name in DisabledExtensions.Keys.OrderBy(n => n)) + foreach ((string folderName, string name) in DisabledExtensions.OrderBy(e => e.Value, StringComparer.OrdinalIgnoreCase)) { - DisabledExtensions.TryGetValue(name, out string folderName); - ExtensionInfo info = folderName is null ? null : KnownExtensions.FirstOrDefault(e => string.Equals(e.FolderName, folderName, StringComparison.OrdinalIgnoreCase)); + ExtensionInfo info = KnownExtensions.FirstOrDefault(e => string.Equals(e.Name, name, StringComparison.OrdinalIgnoreCase)); info ??= new ExtensionInfo(name, "(Unknown)", "(Unknown)", "(Disabled - restart to load)", "", ["none"], folderName ?? ""); yield return info; } } - /// - /// Builds as a map of extension name -> folder name, - /// and returns a list of disabled entries in the same format as . - /// - public List BuildDisabledExtensionsAndGetDisabledExtras(string[] extras) + /// Removes an extension name from the disabled list in settings. + public bool RemoveDisabledExtensionSetting(string extensionName) { - static string DetectNameFromFolder(string extDir) + extensionName = extensionName?.Trim(); + if (string.IsNullOrWhiteSpace(extensionName)) { - try - { - if (!Directory.Exists(extDir)) - { - return null; - } - string[] csFiles = Directory.EnumerateFiles(extDir, "*.cs", SearchOption.TopDirectoryOnly) - .Where(f => !f.EndsWith($"{Path.DirectorySeparatorChar}AssemblyInfo.cs") && !f.EndsWith("/AssemblyInfo.cs")) - .ToArray(); - if (csFiles.IsEmpty()) - { - return null; - } - string preferred = csFiles - .OrderBy(f => f.EndsWith("Extension.cs", StringComparison.OrdinalIgnoreCase) ? 0 : 1) - .ThenBy(f => Path.GetFileNameWithoutExtension(f).Length) - .Select(Path.GetFileNameWithoutExtension) - .FirstOrDefault(); - return string.IsNullOrWhiteSpace(preferred) ? null : preferred; - } - catch (Exception) - { - return null; - } + return false; } - - DisabledExtensions = new(StringComparer.OrdinalIgnoreCase); - HashSet disabledNames = new((Program.ServerSettings?.Extensions?.DisabledExtensions ?? []) - .Where(s => !string.IsNullOrWhiteSpace(s)) - .Select(s => s.Trim()), - StringComparer.OrdinalIgnoreCase); - if (disabledNames.IsEmpty()) + List keysToRemove = [.. Program.ServerSettings.Extensions.DisabledExtensions + .Where(e => + string.Equals(e.Key, extensionName, StringComparison.OrdinalIgnoreCase) || + string.Equals(e.Value, extensionName, StringComparison.OrdinalIgnoreCase)) + .Select(e => e.Key)]; + ExtensionInfo known = KnownExtensions.FirstOrDefault(e => + string.Equals(e.Name, extensionName, StringComparison.OrdinalIgnoreCase) || + string.Equals($"{e.Name}Extension", extensionName, StringComparison.OrdinalIgnoreCase)); + if (known is not null) { - return []; + keysToRemove.Add(known.FolderName); } - List disabledExtras = []; - foreach (string extDir in extras) + Extension loaded = Extensions.FirstOrDefault(e => string.Equals(e.ExtensionName, extensionName, StringComparison.OrdinalIgnoreCase)); + string loadedFolder = loaded?.FilePath?.Replace('\\', '/').TrimEnd('/').AfterLast('/'); + if (!string.IsNullOrWhiteSpace(loadedFolder)) { - string detectedName = DetectNameFromFolder(extDir)?.Trim(); - if (string.IsNullOrWhiteSpace(detectedName) || !disabledNames.Contains(detectedName)) - { - continue; - } - DisabledExtensions[detectedName] = extDir.TrimEnd('/').AfterLast('/'); - disabledExtras.Add(extDir); + keysToRemove.Add(loadedFolder); + } + foreach (string key in keysToRemove.Distinct(StringComparer.OrdinalIgnoreCase)) + { + Program.ServerSettings.Extensions.DisabledExtensions.Remove(key); } - return disabledExtras; + return true; } - /// Removes an extension name from the disabled list in settings and saves. - public void RemoveDisabledExtensionSetting(string extensionName) + public bool AddDisabledExtensionSetting(string extensionName) { - Program.ServerSettings.Extensions.DisabledExtensions.RemoveAll(n => string.Equals(n, extensionName, StringComparison.OrdinalIgnoreCase)); - Program.SaveSettingsFile(); + extensionName = extensionName?.Trim(); + if (string.IsNullOrWhiteSpace(extensionName)) + { + return false; + } + Extension extension = Extensions.FirstOrDefault(e => string.Equals(e.ExtensionName, extensionName, StringComparison.OrdinalIgnoreCase)); + if (extension is null) + { + return false; + } + string folder = extension.FilePath?.Replace('\\', '/').TrimEnd('/').AfterLast('/'); + if (string.IsNullOrWhiteSpace(folder)) + { + return false; + } + ExtensionInfo known = KnownExtensions.FirstOrDefault(e => string.Equals(e.FolderName, folder, StringComparison.OrdinalIgnoreCase)); + string storedName = known?.Name ?? extension.ExtensionName; + Program.ServerSettings.Extensions.DisabledExtensions[folder] = storedName; + return true; } } diff --git a/src/Core/Settings.cs b/src/Core/Settings.cs index 381f7db7e..5309293b4 100644 --- a/src/Core/Settings.cs +++ b/src/Core/Settings.cs @@ -73,9 +73,9 @@ public class Settings : AutoConfiguration /// Settings related to extensions. public class ExtensionsData : AutoConfiguration { - [ConfigComment("List of extension names that are disabled.\nDisabled extensions remain installed on disk, but are not loaded at server startup.")] + [ConfigComment("Map of disabled extension folder name to extension name.\nDisabled extensions remain installed on disk, but are not loaded at server startup.")] [SettingHidden] - public List DisabledExtensions = []; + public Dictionary DisabledExtensions = []; } /// Settings related to Swarm server maintenance.. diff --git a/src/WebAPI/AdminAPI.cs b/src/WebAPI/AdminAPI.cs index 90b8ef33b..303b51585 100644 --- a/src/WebAPI/AdminAPI.cs +++ b/src/WebAPI/AdminAPI.cs @@ -707,13 +707,14 @@ public static async Task InstallExtension(Session session, { return new JObject() { ["error"] = "Unknown extension." }; } - Program.Extensions.RemoveDisabledExtensionSetting(extensionName); string extensionsFolder = Utilities.CombinePathWithAbsolute(Environment.CurrentDirectory, "src/Extensions"); string folder = Utilities.CombinePathWithAbsolute(extensionsFolder, ext.FolderName); if (Directory.Exists(folder)) { return new JObject() { ["error"] = "Extension already installed." }; } + Program.Extensions.RemoveDisabledExtensionSetting(extensionName); + Program.SaveSettingsFile(); await Utilities.RunGitProcess($"clone {ext.URL}", extensionsFolder); return new JObject() { ["success"] = true }; } @@ -730,10 +731,10 @@ public static async Task SetExtensionEnabled(Session session, { return new JObject() { ["error"] = "Core extensions cannot be enabled/disabled." }; } - Program.ServerSettings.Extensions.DisabledExtensions.RemoveAll(n => string.Equals(n, extensionName, StringComparison.OrdinalIgnoreCase)); - if (!enabled) + if ((enabled && !Program.Extensions.RemoveDisabledExtensionSetting(extensionName)) || + (!enabled && !Program.Extensions.AddDisabledExtensionSetting(extensionName))) { - Program.ServerSettings.Extensions.DisabledExtensions.Add(extensionName); + return new JObject() { ["error"] = "Unknown extension." }; } Program.SaveSettingsFile(); File.WriteAllText("src/bin/must_rebuild", "yes"); @@ -776,7 +777,8 @@ public static async Task UninstallExtension(Session session, string folder = ext?.FilePath; if (folder is null) { - if (Program.Extensions.DisabledExtensions.TryGetValue(extensionName, out string folderName)) + string folderName = Program.Extensions.DisabledExtensions.FirstOrDefault(e => string.Equals(e.Value, extensionName, StringComparison.OrdinalIgnoreCase)).Key; + if (!string.IsNullOrWhiteSpace(folderName)) { folder = $"src/Extensions/{folderName}/"; } @@ -785,7 +787,9 @@ public static async Task UninstallExtension(Session session, { return new JObject() { ["error"] = "Unknown extension." }; } - Program.Extensions.RemoveDisabledExtensionSetting(extensionName); + if (Program.Extensions.RemoveDisabledExtensionSetting(extensionName)) { + Program.SaveSettingsFile(); + } string path = Path.GetFullPath(Utilities.CombinePathWithAbsolute(Environment.CurrentDirectory, folder)); Logs.Debug($"Will clear out Extension path: {path}"); if (!Directory.Exists(path)) From d29ef336379b282299a37dedea5f0e9cad1a18cd Mon Sep 17 00:00:00 2001 From: Juan Treminio Date: Fri, 6 Feb 2026 21:38:59 -0600 Subject: [PATCH 3/5] simplify simplify simplify --- src/Core/ExtensionsManager.cs | 90 ++++++++-------------- src/Core/Settings.cs | 4 +- src/Pages/_Generate/ServerTab.cshtml | 4 +- src/WebAPI/AdminAPI.cs | 37 ++++++--- src/wwwroot/js/genpage/server/servertab.js | 4 +- 5 files changed, 63 insertions(+), 76 deletions(-) diff --git a/src/Core/ExtensionsManager.cs b/src/Core/ExtensionsManager.cs index be51e8038..a3ba02bfb 100644 --- a/src/Core/ExtensionsManager.cs +++ b/src/Core/ExtensionsManager.cs @@ -18,8 +18,8 @@ public class ExtensionsManager /// Hashset of folder names of all extensions currently installed (loaded or disabled). public HashSet InstalledExtensionFolders = []; - /// Dictionary of disabled extension folder name to extension name. - public Dictionary DisabledExtensions = []; + /// Folder names of disabled extensions. + public HashSet DisabledExtensions = []; /// Simple holder of information about extensions available online. public record class ExtensionInfo(string Name, string Author, string License, string Description, string URL, string[] Tags, string FolderName) @@ -68,8 +68,7 @@ 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"))]; - InstalledExtensionFolders = new HashSet(extras.Select(e => e.AfterLast('/')), StringComparer.OrdinalIgnoreCase); - LoadKnownExtensions(); + InstalledExtensionFolders = [.. extras.Select(e => e.AfterLast('/'))]; HashSet disabledFolders = BuildDisabledExtensionsAndGetDisabledFolders(); extras = [.. extras.Where(e => !disabledFolders.Contains(e.AfterLast('/')))]; foreach (string deletable in deleteMe) @@ -141,13 +140,6 @@ public async Task PrepExtensions() catch (Exception) { } } RunOnAllExtensions(e => e.OnFirstInit()); - RunOnAllExtensions(e => e.PopulateMetadata()); - } - - /// Loads known extension metadata from the extension list file. - public void LoadKnownExtensions() - { - KnownExtensions.Clear(); try { FDSSection extensionsOutThere = FDSUtility.ReadFile("./launchtools/extension_list.fds"); @@ -162,6 +154,7 @@ public void LoadKnownExtensions() { Logs.Error($"Failed to read known extensions list: {ex.ReadableString()}"); } + RunOnAllExtensions(e => e.PopulateMetadata()); } public async Task BuildExtension(string folder, string projFile) @@ -284,6 +277,12 @@ 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('/') ?? ""; + } + /// Builds and returns disabled extension folders. public HashSet BuildDisabledExtensionsAndGetDisabledFolders() { @@ -293,14 +292,14 @@ public HashSet BuildDisabledExtensionsAndGetDisabledFolders() } DisabledExtensions = []; HashSet disabledFolders = []; - foreach ((string folderName, string extensionName) in Program.ServerSettings.Extensions.DisabledExtensions) + foreach (string rawFolderName in Program.ServerSettings.Extensions.DisabledExtensions) { - if (string.IsNullOrWhiteSpace(extensionName) || string.IsNullOrWhiteSpace(folderName) - || !InstalledExtensionFolders.Contains(folderName) || !disabledFolders.Add(folderName)) + string folderName = rawFolderName?.Trim(); + if (string.IsNullOrWhiteSpace(folderName) || !InstalledExtensionFolders.Contains(folderName) || !disabledFolders.Add(folderName)) { continue; } - DisabledExtensions[folderName] = extensionName; + DisabledExtensions.Add(folderName); } return disabledFolders; } @@ -308,67 +307,40 @@ public HashSet BuildDisabledExtensionsAndGetDisabledFolders() /// Returns disabled extensions for UI display. public IEnumerable GetDisabledExtensionsForUi() { - foreach ((string folderName, string name) in DisabledExtensions.OrderBy(e => e.Value, StringComparer.OrdinalIgnoreCase)) + foreach (string folderName in DisabledExtensions.OrderBy(e => e, StringComparer.OrdinalIgnoreCase)) { - ExtensionInfo info = KnownExtensions.FirstOrDefault(e => string.Equals(e.Name, name, StringComparison.OrdinalIgnoreCase)); - info ??= new ExtensionInfo(name, "(Unknown)", "(Unknown)", "(Disabled - restart to load)", "", ["none"], folderName ?? ""); + ExtensionInfo info = KnownExtensions.FirstOrDefault(e => string.Equals(e.FolderName, folderName, StringComparison.OrdinalIgnoreCase)); + info ??= new ExtensionInfo(folderName, "(Unknown)", "(Unknown)", "(Disabled - restart to load)", "", ["none"], folderName); yield return info; } } - /// Removes an extension name from the disabled list in settings. - public bool RemoveDisabledExtensionSetting(string extensionName) + /// Removes an extension folder from the disabled list in settings. + public bool RemoveDisabledExtensionSetting(string folderName) { - extensionName = extensionName?.Trim(); - if (string.IsNullOrWhiteSpace(extensionName)) + folderName = folderName?.Trim(); + if (string.IsNullOrWhiteSpace(folderName)) { return false; } - List keysToRemove = [.. Program.ServerSettings.Extensions.DisabledExtensions - .Where(e => - string.Equals(e.Key, extensionName, StringComparison.OrdinalIgnoreCase) || - string.Equals(e.Value, extensionName, StringComparison.OrdinalIgnoreCase)) - .Select(e => e.Key)]; - ExtensionInfo known = KnownExtensions.FirstOrDefault(e => - string.Equals(e.Name, extensionName, StringComparison.OrdinalIgnoreCase) || - string.Equals($"{e.Name}Extension", extensionName, StringComparison.OrdinalIgnoreCase)); - if (known is not null) - { - keysToRemove.Add(known.FolderName); - } - Extension loaded = Extensions.FirstOrDefault(e => string.Equals(e.ExtensionName, extensionName, StringComparison.OrdinalIgnoreCase)); - string loadedFolder = loaded?.FilePath?.Replace('\\', '/').TrimEnd('/').AfterLast('/'); - if (!string.IsNullOrWhiteSpace(loadedFolder)) - { - keysToRemove.Add(loadedFolder); - } - foreach (string key in keysToRemove.Distinct(StringComparer.OrdinalIgnoreCase)) - { - Program.ServerSettings.Extensions.DisabledExtensions.Remove(key); - } - return true; + int removed = Program.ServerSettings.Extensions.DisabledExtensions.RemoveAll(f => string.Equals(f, folderName, StringComparison.OrdinalIgnoreCase)); + DisabledExtensions.RemoveWhere(f => string.Equals(f, folderName, StringComparison.OrdinalIgnoreCase)); + return removed > 0; } - public bool AddDisabledExtensionSetting(string extensionName) + /// Adds an extension folder to the disabled list in settings. + public bool AddDisabledExtensionSetting(string folderName) { - extensionName = extensionName?.Trim(); - if (string.IsNullOrWhiteSpace(extensionName)) + folderName = folderName?.Trim(); + if (string.IsNullOrWhiteSpace(folderName) || !InstalledExtensionFolders.Contains(folderName)) { return false; } - Extension extension = Extensions.FirstOrDefault(e => string.Equals(e.ExtensionName, extensionName, StringComparison.OrdinalIgnoreCase)); - if (extension is null) + if (!Program.ServerSettings.Extensions.DisabledExtensions.Any(f => string.Equals(f, folderName, StringComparison.OrdinalIgnoreCase))) { - return false; - } - string folder = extension.FilePath?.Replace('\\', '/').TrimEnd('/').AfterLast('/'); - if (string.IsNullOrWhiteSpace(folder)) - { - return false; + Program.ServerSettings.Extensions.DisabledExtensions.Add(folderName); } - ExtensionInfo known = KnownExtensions.FirstOrDefault(e => string.Equals(e.FolderName, folder, StringComparison.OrdinalIgnoreCase)); - string storedName = known?.Name ?? extension.ExtensionName; - Program.ServerSettings.Extensions.DisabledExtensions[folder] = storedName; + DisabledExtensions.Add(folderName); return true; } } diff --git a/src/Core/Settings.cs b/src/Core/Settings.cs index 5309293b4..a34225e56 100644 --- a/src/Core/Settings.cs +++ b/src/Core/Settings.cs @@ -73,9 +73,9 @@ public class Settings : AutoConfiguration /// Settings related to extensions. public class ExtensionsData : AutoConfiguration { - [ConfigComment("Map of disabled extension folder name to extension name.\nDisabled extensions remain installed on disk, but are not loaded at server startup.")] + [ConfigComment("List of disabled extension folder names.\nDisabled extensions remain installed on disk, but are not loaded at server startup.")] [SettingHidden] - public Dictionary DisabledExtensions = []; + public List DisabledExtensions = []; } /// Settings related to Swarm server maintenance.. diff --git a/src/Pages/_Generate/ServerTab.cshtml b/src/Pages/_Generate/ServerTab.cshtml index 05ccfb554..4a3a74f1e 100644 --- a/src/Pages/_Generate/ServerTab.cshtml +++ b/src/Pages/_Generate/ServerTab.cshtml @@ -253,8 +253,8 @@ @(ext.URL == "" ? "(Missing)" : new HtmlString($"Here")) @ext.License - - + + } diff --git a/src/WebAPI/AdminAPI.cs b/src/WebAPI/AdminAPI.cs index 303b51585..83dc93417 100644 --- a/src/WebAPI/AdminAPI.cs +++ b/src/WebAPI/AdminAPI.cs @@ -713,28 +713,42 @@ public static async Task InstallExtension(Session session, { return new JObject() { ["error"] = "Extension already installed." }; } - Program.Extensions.RemoveDisabledExtensionSetting(extensionName); + Program.Extensions.RemoveDisabledExtensionSetting(ext.FolderName); Program.SaveSettingsFile(); await Utilities.RunGitProcess($"clone {ext.URL}", extensionsFolder); return new JObject() { ["success"] = true }; } - [API.APIDescription("Enables or disables an installed extension by name. Does not trigger a restart. Does signal required rebuild.", + [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 name of the extension to enable/disable.")] string extensionName, + [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 (Program.Extensions.Extensions.Any(e => e.IsCore && string.Equals(e.ExtensionName, extensionName, StringComparison.OrdinalIgnoreCase))) + if (enabled) { - return new JObject() { ["error"] = "Core extensions cannot be enabled/disabled." }; + if (!Program.Extensions.RemoveDisabledExtensionSetting(extensionName)) + { + return new JObject() { ["error"] = "Unknown extension." }; + } } - if ((enabled && !Program.Extensions.RemoveDisabledExtensionSetting(extensionName)) || - (!enabled && !Program.Extensions.AddDisabledExtensionSetting(extensionName))) + else { - return new JObject() { ["error"] = "Unknown extension." }; + 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"); @@ -771,13 +785,13 @@ 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); string folder = ext?.FilePath; if (folder is null) { - string folderName = Program.Extensions.DisabledExtensions.FirstOrDefault(e => string.Equals(e.Value, extensionName, StringComparison.OrdinalIgnoreCase)).Key; + string folderName = extensionName?.Trim(); if (!string.IsNullOrWhiteSpace(folderName)) { folder = $"src/Extensions/{folderName}/"; @@ -787,7 +801,8 @@ public static async Task UninstallExtension(Session session, { return new JObject() { ["error"] = "Unknown extension." }; } - if (Program.Extensions.RemoveDisabledExtensionSetting(extensionName)) { + if (Program.Extensions.RemoveDisabledExtensionSetting(ExtensionsManager.GetFolderNameFromPath(folder))) + { Program.SaveSettingsFile(); } string path = Path.GetFullPath(Utilities.CombinePathWithAbsolute(Environment.CurrentDirectory, folder)); diff --git a/src/wwwroot/js/genpage/server/servertab.js b/src/wwwroot/js/genpage/server/servertab.js index 2fb235804..0817e71b0 100644 --- a/src/wwwroot/js/genpage/server/servertab.js +++ b/src/wwwroot/js/genpage/server/servertab.js @@ -58,12 +58,12 @@ class ExtensionsManager { }); } - setExtensionEnabled(name, enabled, button) { + 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': name, 'enabled': enabled}, data => { + genericRequest('SetExtensionEnabled', {'extensionName': extensionName, 'enabled': enabled}, data => { button.parentElement.innerHTML = (enabled ? 'Enabled' : 'Disabled') + ', restart to apply'; this.newInstallsCard.style.display = 'block'; }, 0, e => { From a83c734e2ce70ace3dea80ef0e0fe959c8a08a82 Mon Sep 17 00:00:00 2001 From: Juan Treminio Date: Mon, 9 Feb 2026 10:03:53 -0600 Subject: [PATCH 4/5] On delete of inactive extension, validate folder exists --- src/Core/ExtensionsManager.cs | 23 +++++++++++++++++++++++ src/WebAPI/AdminAPI.cs | 10 +--------- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/src/Core/ExtensionsManager.cs b/src/Core/ExtensionsManager.cs index a3ba02bfb..de0c0fa46 100644 --- a/src/Core/ExtensionsManager.cs +++ b/src/Core/ExtensionsManager.cs @@ -283,6 +283,29 @@ public static string GetFolderNameFromPath(string path) return path?.Replace('\\', '/').TrimEnd('/').AfterLast('/') ?? ""; } + /// Returns normalized "src/Extensions/{folderName}/" for a direct child folder match, or null if not found. + public static string GetNormalizedExtensionFolderPath(string folderName) + { + folderName = folderName?.Trim(); + if (string.IsNullOrWhiteSpace(folderName)) + { + return null; + } + string extensionsRoot = Path.GetFullPath(Utilities.CombinePathWithAbsolute(Environment.CurrentDirectory, "src/Extensions")); + if (!Directory.Exists(extensionsRoot)) + { + return null; + } + string matchingFolder = Directory.EnumerateDirectories(extensionsRoot) + .Select(Path.GetFileName) + .FirstOrDefault(name => string.Equals(name, folderName, StringComparison.OrdinalIgnoreCase)); + if (string.IsNullOrWhiteSpace(matchingFolder)) + { + return null; + } + return $"src/Extensions/{matchingFolder}/"; + } + /// Builds and returns disabled extension folders. public HashSet BuildDisabledExtensionsAndGetDisabledFolders() { diff --git a/src/WebAPI/AdminAPI.cs b/src/WebAPI/AdminAPI.cs index 83dc93417..d9c22d5c6 100644 --- a/src/WebAPI/AdminAPI.cs +++ b/src/WebAPI/AdminAPI.cs @@ -788,15 +788,7 @@ public static async Task UninstallExtension(Session session, [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); - string folder = ext?.FilePath; - if (folder is null) - { - string folderName = extensionName?.Trim(); - if (!string.IsNullOrWhiteSpace(folderName)) - { - folder = $"src/Extensions/{folderName}/"; - } - } + string folder = ext?.FilePath ?? ExtensionsManager.GetNormalizedExtensionFolderPath(extensionName); if (folder is null) { return new JObject() { ["error"] = "Unknown extension." }; From 984f634826312b6e20707db9ad107e50db00d911 Mon Sep 17 00:00:00 2001 From: Juan Treminio Date: Mon, 23 Feb 2026 21:53:44 -0600 Subject: [PATCH 5/5] Feedback --- src/Core/ExtensionsManager.cs | 74 ++-------------------------- src/Pages/_Generate/ServerTab.cshtml | 2 +- src/WebAPI/AdminAPI.cs | 12 ++--- 3 files changed, 12 insertions(+), 76 deletions(-) diff --git a/src/Core/ExtensionsManager.cs b/src/Core/ExtensionsManager.cs index de0c0fa46..1b585b910 100644 --- a/src/Core/ExtensionsManager.cs +++ b/src/Core/ExtensionsManager.cs @@ -15,12 +15,6 @@ public class ExtensionsManager /// Hashset of folder names of all extensions currently loaded. public HashSet LoadedExtensionFolders = []; - /// Hashset of folder names of all extensions currently installed (loaded or disabled). - public HashSet InstalledExtensionFolders = []; - - /// Folder names of disabled extensions. - public HashSet DisabledExtensions = []; - /// Simple holder of information about extensions available online. public record class ExtensionInfo(string Name, string Author, string License, string Description, string URL, string[] Tags, string FolderName) { @@ -68,8 +62,7 @@ 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"))]; - InstalledExtensionFolders = [.. extras.Select(e => e.AfterLast('/'))]; - HashSet disabledFolders = BuildDisabledExtensionsAndGetDisabledFolders(); + HashSet disabledFolders = [.. Program.ServerSettings.Extensions.DisabledExtensions]; extras = [.. extras.Where(e => !disabledFolders.Contains(e.AfterLast('/')))]; foreach (string deletable in deleteMe) { @@ -283,56 +276,12 @@ public static string GetFolderNameFromPath(string path) return path?.Replace('\\', '/').TrimEnd('/').AfterLast('/') ?? ""; } - /// Returns normalized "src/Extensions/{folderName}/" for a direct child folder match, or null if not found. - public static string GetNormalizedExtensionFolderPath(string folderName) - { - folderName = folderName?.Trim(); - if (string.IsNullOrWhiteSpace(folderName)) - { - return null; - } - string extensionsRoot = Path.GetFullPath(Utilities.CombinePathWithAbsolute(Environment.CurrentDirectory, "src/Extensions")); - if (!Directory.Exists(extensionsRoot)) - { - return null; - } - string matchingFolder = Directory.EnumerateDirectories(extensionsRoot) - .Select(Path.GetFileName) - .FirstOrDefault(name => string.Equals(name, folderName, StringComparison.OrdinalIgnoreCase)); - if (string.IsNullOrWhiteSpace(matchingFolder)) - { - return null; - } - return $"src/Extensions/{matchingFolder}/"; - } - - /// Builds and returns disabled extension folders. - public HashSet BuildDisabledExtensionsAndGetDisabledFolders() - { - if (Program.ServerSettings?.Extensions.DisabledExtensions is null) - { - Program.ServerSettings.Extensions.DisabledExtensions = []; - } - DisabledExtensions = []; - HashSet disabledFolders = []; - foreach (string rawFolderName in Program.ServerSettings.Extensions.DisabledExtensions) - { - string folderName = rawFolderName?.Trim(); - if (string.IsNullOrWhiteSpace(folderName) || !InstalledExtensionFolders.Contains(folderName) || !disabledFolders.Add(folderName)) - { - continue; - } - DisabledExtensions.Add(folderName); - } - return disabledFolders; - } - /// Returns disabled extensions for UI display. public IEnumerable GetDisabledExtensionsForUi() { - foreach (string folderName in DisabledExtensions.OrderBy(e => e, StringComparer.OrdinalIgnoreCase)) + foreach (string folderName in Program.ServerSettings.Extensions.DisabledExtensions.OrderBy(e => e)) { - ExtensionInfo info = KnownExtensions.FirstOrDefault(e => string.Equals(e.FolderName, folderName, StringComparison.OrdinalIgnoreCase)); + ExtensionInfo info = KnownExtensions.FirstOrDefault(e => e.FolderName == folderName); info ??= new ExtensionInfo(folderName, "(Unknown)", "(Unknown)", "(Disabled - restart to load)", "", ["none"], folderName); yield return info; } @@ -341,29 +290,16 @@ public IEnumerable GetDisabledExtensionsForUi() /// Removes an extension folder from the disabled list in settings. public bool RemoveDisabledExtensionSetting(string folderName) { - folderName = folderName?.Trim(); - if (string.IsNullOrWhiteSpace(folderName)) - { - return false; - } - int removed = Program.ServerSettings.Extensions.DisabledExtensions.RemoveAll(f => string.Equals(f, folderName, StringComparison.OrdinalIgnoreCase)); - DisabledExtensions.RemoveWhere(f => string.Equals(f, folderName, StringComparison.OrdinalIgnoreCase)); - return removed > 0; + return Program.ServerSettings.Extensions.DisabledExtensions.Remove(folderName); } /// Adds an extension folder to the disabled list in settings. public bool AddDisabledExtensionSetting(string folderName) { - folderName = folderName?.Trim(); - if (string.IsNullOrWhiteSpace(folderName) || !InstalledExtensionFolders.Contains(folderName)) - { - return false; - } - if (!Program.ServerSettings.Extensions.DisabledExtensions.Any(f => string.Equals(f, folderName, StringComparison.OrdinalIgnoreCase))) + if (!Program.ServerSettings.Extensions.DisabledExtensions.Contains(folderName)) { Program.ServerSettings.Extensions.DisabledExtensions.Add(folderName); } - DisabledExtensions.Add(folderName); return true; } } diff --git a/src/Pages/_Generate/ServerTab.cshtml b/src/Pages/_Generate/ServerTab.cshtml index 4a3a74f1e..062b23d03 100644 --- a/src/Pages/_Generate/ServerTab.cshtml +++ b/src/Pages/_Generate/ServerTab.cshtml @@ -271,7 +271,7 @@ License Actions - @foreach (ExtensionsManager.ExtensionInfo ext in Program.Extensions.KnownExtensions.Where(e => !Program.Extensions.InstalledExtensionFolders.Contains(e.FolderName) && !e.Tags.Contains("hidden"))) + @foreach (ExtensionsManager.ExtensionInfo ext in Program.Extensions.KnownExtensions.Where(e => !Program.Extensions.LoadedExtensionFolders.Contains(e.FolderName) && !e.Tags.Contains("hidden"))) { @ext.Name diff --git a/src/WebAPI/AdminAPI.cs b/src/WebAPI/AdminAPI.cs index d9c22d5c6..1378ca156 100644 --- a/src/WebAPI/AdminAPI.cs +++ b/src/WebAPI/AdminAPI.cs @@ -788,8 +788,13 @@ public static async Task UninstallExtension(Session session, [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); - string folder = ext?.FilePath ?? ExtensionsManager.GetNormalizedExtensionFolderPath(extensionName); + string folder = ext?.FilePath; if (folder is null) + { + folder = $"src/Extensions/{extensionName}/"; + } + string path = Path.GetFullPath(Utilities.CombinePathWithAbsolute(Environment.CurrentDirectory, folder)); + if (!Directory.Exists(path)) { return new JObject() { ["error"] = "Unknown extension." }; } @@ -797,12 +802,7 @@ public static async Task UninstallExtension(Session session, { Program.SaveSettingsFile(); } - string path = Path.GetFullPath(Utilities.CombinePathWithAbsolute(Environment.CurrentDirectory, folder)); Logs.Debug($"Will clear out Extension path: {path}"); - if (!Directory.Exists(path)) - { - return new JObject() { ["error"] = "Extension has invalid path, cannot delete." }; - } try { FileSystem.DeleteDirectory(path, UIOption.OnlyErrorDialogs, RecycleOption.SendToRecycleBin, UICancelOption.ThrowException);