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
101 changes: 100 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 All @@ -15,6 +15,12 @@ public class ExtensionsManager
/// <summary>Hashset of folder names of all extensions currently loaded.</summary>
public HashSet<string> LoadedExtensionFolders = [];

/// <summary>Hashset of folder names of all extensions currently installed (loaded or disabled).</summary>
public HashSet<string> InstalledExtensionFolders = [];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a redundant duplicate of already tracked data


/// <summary>Folder names of disabled extensions.</summary>
public HashSet<string> DisabledExtensions = [];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This field is a duplicate tracker of what's already in settings


/// <summary>Simple holder of information about extensions available online.</summary>
public record class ExtensionInfo(string Name, string Author, string License, string Description, string URL, string[] Tags, string FolderName)
{
Expand Down Expand Up @@ -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 = [.. extras.Select(e => e.AfterLast('/'))];
HashSet<string> disabledFolders = BuildDisabledExtensionsAndGetDisabledFolders();
extras = [.. extras.Where(e => !disabledFolders.Contains(e.AfterLast('/')))];
foreach (string deletable in deleteMe)
{
try
Expand Down Expand Up @@ -267,4 +276,94 @@ 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 normalized "src/Extensions/{folderName}/" for a direct child folder match, or null if not found.</summary>
public static string GetNormalizedExtensionFolderPath(string folderName)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function does nothing

{
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}/";
}

/// <summary>Builds <see cref="DisabledExtensions"/> and returns disabled extension folders.</summary>
public HashSet<string> BuildDisabledExtensionsAndGetDisabledFolders()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is slowly manually rebuilding a list that's already saved? And is redundant

{
if (Program.ServerSettings?.Extensions.DisabledExtensions is null)
{
Program.ServerSettings.Extensions.DisabledExtensions = [];
}
DisabledExtensions = [];
HashSet<string> 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;
}

/// <summary>Returns disabled extensions for UI display.</summary>
public IEnumerable<ExtensionInfo> GetDisabledExtensionsForUi()
{
foreach (string folderName in DisabledExtensions.OrderBy(e => e, StringComparer.OrdinalIgnoreCase))
{
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;
}
}

/// <summary>Removes an extension folder from the disabled list in settings.</summary>
public bool RemoveDisabledExtensionSetting(string folderName)
{
folderName = folderName?.Trim();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all these cleanups and checks aren't needed

if (string.IsNullOrWhiteSpace(folderName))
{
return false;
}
int removed = Program.ServerSettings.Extensions.DisabledExtensions.RemoveAll(f => string.Equals(f, folderName, StringComparison.OrdinalIgnoreCase));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these string.equals ordinal things still do not need to exist as commented before

DisabledExtensions.RemoveWhere(f => string.Equals(f, folderName, StringComparison.OrdinalIgnoreCase));
return removed > 0;
}

/// <summary>Adds an extension folder to the disabled list in settings.</summary>
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)))
{
Program.ServerSettings.Extensions.DisabledExtensions.Add(folderName);
}
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
19 changes: 18 additions & 1 deletion 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 All @@ -254,7 +271,7 @@
<th>License</th>
<th>Actions</th>
</tr>
@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")))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

put this back

{
<tr>
<td>@ext.Name</td>
Expand Down
50 changes: 47 additions & 3 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,10 +713,48 @@ 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("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();
Logs.Debug($"User {session.User.UserID} {(enabled ? "enabled" : "disabled")} extension '{extensionName}'.");
return new JObject() { ["success"] = true };
}

[API.APIDescription("Triggers an extension update for an installed extension. Does not trigger a restart.",
"""
"success": true // or false if no update available
Expand Down Expand Up @@ -745,14 +784,19 @@ 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 ?? ExtensionsManager.GetNormalizedExtensionFolderPath(extensionName);
if (folder is null)
{
return new JObject() { ["error"] = "Unknown extension." };
}
string path = Path.GetFullPath(Utilities.CombinePathWithAbsolute(Environment.CurrentDirectory, ext.FilePath));
if (Program.Extensions.RemoveDisabledExtensionSetting(ExtensionsManager.GetFolderNameFromPath(folder)))
{
Program.SaveSettingsFile();
}
string path = Path.GetFullPath(Utilities.CombinePathWithAbsolute(Environment.CurrentDirectory, folder));
Logs.Debug($"Will clear out Extension path: {path}");
if (!Directory.Exists(path))
{
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