Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion src/Core/ExtensionsManager.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using FreneticUtilities.FreneticDataSyntax;
using FreneticUtilities.FreneticDataSyntax;
using FreneticUtilities.FreneticExtensions;
using Microsoft.AspNetCore.Html;
using SwarmUI.Utils;
Expand Down Expand Up @@ -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<string> disabledFolders = [.. Program.ServerSettings.Extensions.DisabledExtensions];
extras = [.. extras.Where(e => !disabledFolders.Contains(e.AfterLast('/')))];
foreach (string deletable in deleteMe)
{
try
Expand Down Expand Up @@ -267,4 +269,37 @@ public T GetExtension<T>() where T : Extension
{
return Extensions.FirstOrDefault(e => e is T) as T;
}

/// <summary>Returns folder name from an extension path.</summary>
public static string GetFolderNameFromPath(string path)
{
return path?.Replace('\\', '/').TrimEnd('/').AfterLast('/') ?? "";
}

/// <summary>Returns disabled extensions for UI display.</summary>
public IEnumerable<ExtensionInfo> 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;
}
}

/// <summary>Removes an extension folder from the disabled list in settings.</summary>
public bool RemoveDisabledExtensionSetting(string folderName)
{
return Program.ServerSettings.Extensions.DisabledExtensions.Remove(folderName);
}

/// <summary>Adds an extension folder to the disabled list in settings.</summary>
public bool AddDisabledExtensionSetting(string folderName)
{
if (!Program.ServerSettings.Extensions.DisabledExtensions.Contains(folderName))
{
Program.ServerSettings.Extensions.DisabledExtensions.Add(folderName);
}
return true;
}
}
13 changes: 12 additions & 1 deletion src/Core/Settings.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using FreneticUtilities.FreneticDataSyntax;
using FreneticUtilities.FreneticDataSyntax;
using SwarmUI.Backends;
using SwarmUI.Media;
using SwarmUI.Utils;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -67,6 +70,14 @@ public class Settings : AutoConfiguration
[ConfigComment("Settings related to server performance.")]
public PerformanceData Performance = new();

/// <summary>Settings related to extensions.</summary>
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<string> DisabledExtensions = [];
}

/// <summary>Settings related to Swarm server maintenance..</summary>
public class ServerMaintenanceData : AutoConfiguration
{
Expand Down
17 changes: 17 additions & 0 deletions src/Pages/_Generate/ServerTab.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@
<td>@(ext.ReadmeURL == "" ? "(Missing)": new HtmlString($"<a target=\"_blank\" href=\"{ext.ReadmeURL}\">Here</a>"))</td>
<td class="@(ext.License == "MIT" ? "" : "ext-danger-license")">@ext.License</td>
<td>
<button class="basic-button danger-button translate" onclick="extensionsManager.setExtensionEnabled('@ext.ExtensionName', false, this)">Disable</button>
@if (ext.CanUpdate)
{
<button class="basic-button" onclick="extensionsManager.updateExtension('@ext.ExtensionName', this)">Update</button>
Expand All @@ -241,6 +242,22 @@
</td>
</tr>
}
@foreach (ExtensionsManager.ExtensionInfo ext in Program.Extensions.GetDisabledExtensionsForUi())
{
<tr>
<td>@ext.Name</td>
<td><code>(Disabled)</code></td>
<td>@ExtensionsManager.HtmlTags(ext.Tags)</td>
<td>@ext.Author</td>
<td>@ext.Description</td>
<td>@(ext.URL == "" ? "(Missing)" : new HtmlString($"<a target=\"_blank\" href=\"{ext.URL}\">Here</a>"))</td>
<td class="@(ext.License == "MIT" ? "" : "ext-danger-license")">@ext.License</td>
<td>
<button class="basic-button translate" onclick="extensionsManager.setExtensionEnabled('@ext.FolderName', true, this)">Enable</button>
<button class="basic-button" onclick="extensionsManager.uninstallExtension('@ext.FolderName', this)">Uninstall</button>
</td>
</tr>
}
</table>
<br>
<h3>Available Extensions</h3>
Expand Down
59 changes: 52 additions & 7 deletions src/WebAPI/AdminAPI.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -712,11 +713,50 @@ public static async Task<JObject> 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<JObject> 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
""")]
Expand Down Expand Up @@ -745,19 +785,24 @@ public static async Task<JObject> UpdateExtension(Session session,
"success": true
""")]
public static async Task<JObject> 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);
Expand Down
14 changes: 14 additions & 0 deletions src/wwwroot/js/genpage/server/servertab.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down