+@inject PluginManager PluginManager
+@inject NavigationManager NavigationManager
+@rendermode InteractiveServer
+
+
![@Localizer[SharedResource.sharpsite_plugin_icon]](@(string.IsNullOrEmpty(Plugin.Icon) ? DefaultPluginIcon : Plugin.Icon))
+ alt="@Localizer[SharedResource.sharpsite_plugin_icon]" class="card-img-top" />
@code {
-
private const string DefaultPluginIcon = "/img/plugin-icon.svg";
[Parameter, EditorRequired] public required PluginManifest Plugin { get; set; }
+ [Parameter] public string? AdditionalCssClass { get; set; }
+
+ private async Task RemovePluginAsync()
+ {
+ await PluginManager.RemovePlugin(Plugin.Id);
+ NavigationManager.NavigateTo(NavigationManager.Uri, forceLoad: true);
+ }
}
diff --git a/src/SharpSite.Web/Components/Admin/PluginList.razor b/src/SharpSite.Web/Components/Admin/PluginList.razor
index 770c743..548cbd0 100644
--- a/src/SharpSite.Web/Components/Admin/PluginList.razor
+++ b/src/SharpSite.Web/Components/Admin/PluginList.razor
@@ -9,9 +9,15 @@
@* Add a button that links to the add plugin page With the text add new plugin *@
+@if (!string.IsNullOrEmpty(ErrorMessage))
+{
+
+ @ErrorMessage
+
+}
@if (AppState.Plugins.Count == 0)
{
@@ -21,13 +27,35 @@
else
{
- @foreach (var plugin in AppState.Plugins)
+ @foreach (var plugin in AppState.Plugins.Values)
{
-
+ var isFileStorage = plugin.Features is not null && plugin.Features.Any(f => f == PluginFeatures.FileStorage);
+
}
}
@code {
+ private string ErrorMessage = string.Empty;
+ protected override void OnInitialized()
+ {
+ var fileStoragePlugins = AppState.Plugins.Values
+ .Where(p => p.Features is not null && p.Features.Any(f => f == PluginFeatures.FileStorage))
+ .ToList();
+
+ if (fileStoragePlugins.Count > 1)
+ {
+ ErrorMessage = SharedResource.sharpsite_plugin_filestorage_error_multiple;
+ }
+ else if (fileStoragePlugins.Count == 0)
+ {
+ ErrorMessage = SharedResource.sharpsite_plugin_filestorage_error_none;
+ }
+ else
+ {
+ ErrorMessage = string.Empty;
+ }
+ }
}
diff --git a/src/SharpSite.Web/Locales/SharedResource.Designer.cs b/src/SharpSite.Web/Locales/SharedResource.Designer.cs
index 301c6db..4d29f61 100644
--- a/src/SharpSite.Web/Locales/SharedResource.Designer.cs
+++ b/src/SharpSite.Web/Locales/SharedResource.Designer.cs
@@ -528,6 +528,24 @@ internal static string sharpsite_plugin_file {
}
}
+ ///
+ /// Looks up a localized string similar to More than one plugin provides the FileStorage feature. Please ensure only one FileStorage plugin is enabled at a time..
+ ///
+ internal static string sharpsite_plugin_filestorage_error_multiple {
+ get {
+ return ResourceManager.GetString("sharpsite_plugin_filestorage_error_multiple", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to No plugin with the FileStorage feature is installed. It is required for file storage functionality..
+ ///
+ internal static string sharpsite_plugin_filestorage_error_none {
+ get {
+ return ResourceManager.GetString("sharpsite_plugin_filestorage_error_none", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Icon of the plugin.
///
diff --git a/src/SharpSite.Web/Locales/SharedResource.bg.resx b/src/SharpSite.Web/Locales/SharedResource.bg.resx
index 5aa2f80..a5e478a 100644
--- a/src/SharpSite.Web/Locales/SharedResource.bg.resx
+++ b/src/SharpSite.Web/Locales/SharedResource.bg.resx
@@ -359,14 +359,6 @@
Това гарантира, че помощните технологии използват правилния език за съдържанието.
AI generated translation
-
- Име на сайта:
- AI generated translation
-
-
- Върни се на уебсайта
- AI generated translation
-
Персонализиране на съдържанието на страницата "Не е намерена"
@@ -376,6 +368,14 @@
Промени Темата
+
+ Име на сайта:
+ AI generated translation
+
+
+ Върни се на уебсайта
+ AI generated translation
+
@@ -391,4 +391,10 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/SharpSite.Web/Locales/SharedResource.ca.resx b/src/SharpSite.Web/Locales/SharedResource.ca.resx
index b8f87ff..227287f 100644
--- a/src/SharpSite.Web/Locales/SharedResource.ca.resx
+++ b/src/SharpSite.Web/Locales/SharedResource.ca.resx
@@ -355,14 +355,6 @@
Això garanteix que les tecnologies d'assistència utilitzin el llenguatge correcte per al contingut.
AI generated translation
-
- Nom del lloc:
- AI generated translation
-
-
- Tornar al lloc web
- AI generated translation
-
Personalitza el contingut de Pàgina no trobada.
@@ -372,6 +364,14 @@
Canvia el tema
+
+ Nom del lloc:
+ AI generated translation
+
+
+ Tornar al lloc web
+ AI generated translation
+
@@ -387,4 +387,10 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/SharpSite.Web/Locales/SharedResource.de.resx b/src/SharpSite.Web/Locales/SharedResource.de.resx
index 2948974..6deae95 100644
--- a/src/SharpSite.Web/Locales/SharedResource.de.resx
+++ b/src/SharpSite.Web/Locales/SharedResource.de.resx
@@ -355,14 +355,6 @@
Dies gewährleistet, dass assistive Technologien die richtige Sprache für den Inhalt verwenden.
AI generated translation
-
- Seitenname:
- AI generated translation
-
-
- Zurück zur Website
- AI generated translation
-
Individualisiere Inhalte für die Seite "Nicht gefunden"
@@ -372,6 +364,14 @@
Thema ändern
+
+ Seitenname:
+ AI generated translation
+
+
+ Zurück zur Website
+ AI generated translation
+
@@ -382,9 +382,15 @@
-
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/SharpSite.Web/Locales/SharedResource.en.resx b/src/SharpSite.Web/Locales/SharedResource.en.resx
index b982929..be2106c 100644
--- a/src/SharpSite.Web/Locales/SharedResource.en.resx
+++ b/src/SharpSite.Web/Locales/SharedResource.en.resx
@@ -341,6 +341,14 @@
Change Theme
Text of the button used to change the theme of the website
+
+ Site Name:
+ Label on admin pages that allows customization of the website name
+
+
+ Return to website
+ Link text on admin portal that returns the user to the public website
+
The markdown contains a script tag which will be executed once users load the page. Are you sure you want to proceed?
@@ -356,12 +364,10 @@
Plugin '{0}' is already installed.
-
- Site Name:
- Label on admin pages that allows customization of the website name
+
+ More than one plugin provides the FileStorage feature. Please ensure only one FileStorage plugin is enabled at a time.
-
- Return to website
- Link text on admin portal that returns the user to the public website
+
+ No plugin with the FileStorage feature is installed. It is required for file storage functionality.
\ No newline at end of file
diff --git a/src/SharpSite.Web/Locales/SharedResource.es.resx b/src/SharpSite.Web/Locales/SharedResource.es.resx
index 9d01100..0c33f4b 100644
--- a/src/SharpSite.Web/Locales/SharedResource.es.resx
+++ b/src/SharpSite.Web/Locales/SharedResource.es.resx
@@ -355,14 +355,6 @@
Esto asegura que las tecnologías de asistencia utilicen el idioma correcto para el contenido.
AI generated translation
-
- Nombre del sitio:
- AI generated translation
-
-
- Volver al sitio web.
- AI generated translation
-
Personalizar el contenido de la página no encontrada.
@@ -372,6 +364,14 @@
Cambiar Tema
+
+ Nombre del sitio:
+ AI generated translation
+
+
+ Volver al sitio web.
+ AI generated translation
+
@@ -382,9 +382,15 @@
-
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/SharpSite.Web/Locales/SharedResource.fi.resx b/src/SharpSite.Web/Locales/SharedResource.fi.resx
index faca17f..1341a4d 100644
--- a/src/SharpSite.Web/Locales/SharedResource.fi.resx
+++ b/src/SharpSite.Web/Locales/SharedResource.fi.resx
@@ -337,6 +337,14 @@
Vaihda teemaa
+
+ Sivuston nimi:
+ AI generated translation
+
+
+ Palaa verkkosivustolle
+ AI generated translation
+
Markdown sisältää script -tagin, joka ajetaan, kun käyttäjät lataavat sivun. Oletko varma, että haluat jatkaa?
@@ -352,12 +360,10 @@
Laajennus '{0}' on jo asennettu.
-
- Sivuston nimi:
- AI generated translation
+
+ Useampi laajennus tarjoaa FileStorage-ominaisuuden. Varmista, että vain yksi FileStorage-laajennus on käytössä kerrallaan.
-
- Palaa verkkosivustolle
- AI generated translation
+
+ Yhtään laajennusta, jossa on FileStorage-ominaisuus, ei ole asennettu. Se vaaditaan tiedostojen tallennustoimintoa varten.
\ No newline at end of file
diff --git a/src/SharpSite.Web/Locales/SharedResource.fr.resx b/src/SharpSite.Web/Locales/SharedResource.fr.resx
index a458fc4..b38b8d0 100644
--- a/src/SharpSite.Web/Locales/SharedResource.fr.resx
+++ b/src/SharpSite.Web/Locales/SharedResource.fr.resx
@@ -355,14 +355,6 @@
Cela garantit que les technologies d'assistance utilisent la langue correcte pour le contenu.
AI generated translation
-
- Nom du site :
- AI generated translation
-
-
- Retour au site web
- AI generated translation
-
Personnaliser le contenu de la page introuvable
@@ -372,6 +364,14 @@
Changer de thème
+
+ Nom du site :
+ AI generated translation
+
+
+ Retour au site web
+ AI generated translation
+
@@ -382,9 +382,15 @@
-
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/SharpSite.Web/Locales/SharedResource.it.resx b/src/SharpSite.Web/Locales/SharedResource.it.resx
index 4330a5d..e235da7 100644
--- a/src/SharpSite.Web/Locales/SharedResource.it.resx
+++ b/src/SharpSite.Web/Locales/SharedResource.it.resx
@@ -386,14 +386,6 @@
Questo garantisce che le tecnologie assistive utilizzino la lingua corretta per il contenuto.
AI generated translation
-
- Nome del sito:
- AI generated translation
-
-
- Torna al sito web.
- AI generated translation
-
Personalizza il contenuto della pagina non trovata.
@@ -403,6 +395,14 @@
Cambia tema
+
+ Nome del sito:
+ AI generated translation
+
+
+ Torna al sito web.
+ AI generated translation
+
@@ -413,9 +413,15 @@
-
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/SharpSite.Web/Locales/SharedResource.nl.resx b/src/SharpSite.Web/Locales/SharedResource.nl.resx
index 81b16ce..c60bbcb 100644
--- a/src/SharpSite.Web/Locales/SharedResource.nl.resx
+++ b/src/SharpSite.Web/Locales/SharedResource.nl.resx
@@ -355,14 +355,6 @@
Dit zorgt ervoor dat hulpmiddelen voor toegankelijkheid de juiste taal gebruiken voor de inhoud.
AI generated translation
-
- Website Naam:
- AI generated translation
-
-
- Terug naar website
- AI generated translation
-
Aanpassen van Pagina Niet Gevonden inhoud.
@@ -372,6 +364,14 @@
Verander thema
+
+ Website Naam:
+ AI generated translation
+
+
+ Terug naar website
+ AI generated translation
+
@@ -382,9 +382,15 @@
-
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/SharpSite.Web/Locales/SharedResource.pt.resx b/src/SharpSite.Web/Locales/SharedResource.pt.resx
index 8301ad4..2c16413 100644
--- a/src/SharpSite.Web/Locales/SharedResource.pt.resx
+++ b/src/SharpSite.Web/Locales/SharedResource.pt.resx
@@ -355,14 +355,6 @@
Isso garante que as tecnologias assistivas usem o idioma correto para o conteúdo.
AI generated translation
-
- Nome do Site:
- AI generated translation
-
-
- Voltar ao site
- AI generated translation
-
Personalizar o conteúdo da página não encontrada.
@@ -372,6 +364,14 @@
Alterar Tema
+
+ Nome do Site:
+ AI generated translation
+
+
+ Voltar ao site
+ AI generated translation
+
@@ -382,9 +382,15 @@
-
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/SharpSite.Web/Locales/SharedResource.resx b/src/SharpSite.Web/Locales/SharedResource.resx
index 0e299a7..d8a9f70 100644
--- a/src/SharpSite.Web/Locales/SharedResource.resx
+++ b/src/SharpSite.Web/Locales/SharedResource.resx
@@ -359,14 +359,14 @@
Change Theme
Text of the button used to change the theme of the website
-
+
Site Name:
Label on admin pages that allows customization of the website name
-
+
Return to website
Link text on admin portal that returns the user to the public website
-
+
The markdown contains a script tag which will be executed once users load the page. Are you sure you want to proceed?
Alert message that is showed when the markdown content for a page contains a script tag
@@ -387,4 +387,12 @@
Plugin '{0}' is already installed.
Error message to be desplayed when a plugin that already exists, is attempted to be uploaded.
+
+ More than one plugin provides the FileStorage feature. Please ensure only one FileStorage plugin is enabled at a time.
+ Error message when multiple plugins with FileStorage feature are installed.
+
+
+ No plugin with the FileStorage feature is installed. It is required for file storage functionality.
+ Error message when no plugin with FileStorage features is installed.
+
\ No newline at end of file
diff --git a/src/SharpSite.Web/Locales/SharedResource.sv.resx b/src/SharpSite.Web/Locales/SharedResource.sv.resx
index 49aa8fb..c3aaadb 100644
--- a/src/SharpSite.Web/Locales/SharedResource.sv.resx
+++ b/src/SharpSite.Web/Locales/SharedResource.sv.resx
@@ -400,6 +400,14 @@
Byt tema
+
+ Webbplatsnamn:
+ AI generated translation
+
+
+ Återgå till webbplatsen
+ AI generated translation
+
@@ -410,17 +418,15 @@
-
+
-
- Webbplatsnamn:
- AI generated translation
+
+
-
- Återgå till webbplatsen
- AI generated translation
+
+
\ No newline at end of file
diff --git a/src/SharpSite.Web/Locales/SharedResource.sw.resx b/src/SharpSite.Web/Locales/SharedResource.sw.resx
index 8dd3acf..16d2cf8 100644
--- a/src/SharpSite.Web/Locales/SharedResource.sw.resx
+++ b/src/SharpSite.Web/Locales/SharedResource.sw.resx
@@ -355,14 +355,6 @@
Hii hufanya teknolojia za msaada kutumia lugha sahihi kwa maudhui.
AI generated translation
-
- Jina la Tovuti:
- AI generated translation
-
-
- Rudi kwenye tovuti
- AI generated translation
-
Sawazisha Yaliyopatikana Ukurasa wa Yaliyopatikana maudhui kwa SharpSite ni mfumo wa usimamizi wa yaliyomo wa chanzo wazi uliojengwa na C# na Blazor.
@@ -372,6 +364,14 @@
Badili Mandhari
+
+ Jina la Tovuti:
+ AI generated translation
+
+
+ Rudi kwenye tovuti
+ AI generated translation
+
@@ -382,9 +382,15 @@
-
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/SharpSite.Web/PluginManager.cs b/src/SharpSite.Web/PluginManager.cs
index 0b294df..94428a8 100644
--- a/src/SharpSite.Web/PluginManager.cs
+++ b/src/SharpSite.Web/PluginManager.cs
@@ -21,6 +21,8 @@ public class PluginManager(
private readonly static IServiceCollection _ServiceDescriptors = new ServiceCollection();
private static IServiceProvider? _ServiceProvider;
+ private readonly Dictionary
_pluginAssemblies = [];
+
public static void Initialize()
{
Directory.CreateDirectory("plugins");
@@ -57,19 +59,47 @@ public void HandleUploadedPlugin(Plugin plugin)
}
- private PluginManifest? ReadManifest(string manifestPath)
+ private static PluginManifest? ReadManifest(string manifestPath)
{
using var manifestStream = File.OpenRead(manifestPath);
return ReadManifest(manifestStream);
}
- private PluginManifest ReadManifest(Stream manifestStream)
+ private static readonly JsonSerializerOptions _jsonOpts = new() { Converters = { new JsonStringEnumConverter() } };
+ private static PluginManifest ReadManifest(Stream manifestStream)
+ {
+ return JsonSerializer.Deserialize(manifestStream, _jsonOpts)!;
+ }
+
+ private void LoadNuGetDependenciesInContext(PluginManifest manifest, DirectoryInfo pluginLibFolder, PluginAssemblyLoadContext? context)
{
- var options = new JsonSerializerOptions
+ if (context is null)
{
- Converters = { new JsonStringEnumConverter() }
- };
- return JsonSerializer.Deserialize(manifestStream, options)!;
+ throw new ArgumentNullException(nameof(context), $"PluginAssemblyLoadContext cannot be null when loading NuGet dependencies. Plugin '{manifest.Id}'");
+ }
+ if (manifest.NuGetDependencies is null || manifest.NuGetDependencies.Length == 0)
+ {
+ return;
+ }
+
+ foreach (var dep in manifest.NuGetDependencies)
+ {
+ var depDll = Path.Combine(pluginLibFolder.FullName, dep.Package + ".dll");
+ if (File.Exists(depDll))
+ {
+ try
+ {
+ byte[] assemblyBytes = File.ReadAllBytes(depDll);
+ using var ms = new MemoryStream(assemblyBytes);
+ context.LoadFromStream(ms);
+ logger.LogInformation("Loaded dependency (from stream): {depDll}", depDll);
+ }
+ catch (Exception ex)
+ {
+ logger.LogWarning(ex, "Failed to load dependency: {depDll}", depDll);
+ }
+ }
+ }
}
public async Task SavePlugin()
@@ -81,31 +111,26 @@ public async Task SavePlugin()
throw exception;
}
- FileStream fileStream;
DirectoryInfo pluginLibFolder;
- ZipArchive archive;
- (fileStream, pluginLibFolder, archive) = await ExtractAndInstallPlugin(logger, plugin, Manifest);
+ (_, pluginLibFolder, _) = await ExtractAndInstallPlugin(logger, plugin, Manifest);
- // By convention it is a package_name of (@.(sspkg|.dll)
var key = Manifest.Id;
- // if there is a DLL in the pluginLibFolder with the same base name as the plugin file, reflection load that DLL
var pluginDll = Directory.GetFiles(pluginLibFolder.FullName, $"{key}*.dll").FirstOrDefault();
- if (!string.IsNullOrEmpty(pluginDll))
- {
- // Soft load of package without taking ownership for the process .dll
- using var pluginAssemblyFileStream = File.OpenRead(pluginDll);
- plugin = await Plugin.LoadFromStream(pluginAssemblyFileStream, key);
- var pluginAssembly = new PluginAssembly(Manifest, plugin);
- pluginAssemblyManager.AddAssembly(pluginAssembly);
- await RegisterWithServiceLocator(pluginAssembly);
- await AppState.Save();
- logger.LogInformation("Assembly {AssemblyName} loaded at runtime.", pluginDll);
+ if (string.IsNullOrEmpty(pluginDll))
+ throw new Exception($"Plugin DLL not found for {key}");
- }
+ using var pluginAssemblyFileStream = File.OpenRead(pluginDll);
+ plugin = await Plugin.LoadFromStream(pluginAssemblyFileStream, key);
+ var pluginAssembly = new PluginAssembly(Manifest, plugin);
+ pluginAssembly.LoadContext(pluginDll);
+ LoadNuGetDependenciesInContext(Manifest, pluginLibFolder, pluginAssembly.LoadContextInstance);
+ _pluginAssemblies[key] = pluginAssembly;
+ pluginAssemblyManager.AddAssembly(pluginAssembly);
+ await RegisterWithServiceLocator(pluginAssembly);
+ logger.LogInformation("Assembly {AssemblyName} loaded at runtime.", pluginDll);
- // Add plugin to the list of plugins in ApplicationState
AppState.AddPlugin(Manifest.Id, Manifest);
logger.LogInformation("Plugin {PluginName} loaded at runtime.", Manifest);
@@ -113,6 +138,7 @@ public async Task SavePlugin()
{
AppState.SetTheme(Manifest);
}
+ await AppState.Save();
logger.LogInformation("Plugin {PluginName} saved and registered.", plugin.Name);
@@ -145,7 +171,7 @@ public async Task LoadPluginsAtStartup()
foreach (var pluginFolder in Directory.GetDirectories("plugins"))
{
var pluginName = Path.GetFileName(pluginFolder);
- if (pluginName.StartsWith("_")) continue;
+ if (pluginName.StartsWith('_')) continue;
var manifestPath = Path.Combine(pluginFolder, "manifest.json");
if (!File.Exists(manifestPath)) continue;
@@ -159,15 +185,17 @@ public async Task LoadPluginsAtStartup()
var pluginDll = Directory.GetFiles(pluginFolder, $"{key}*.dll").FirstOrDefault();
if (!string.IsNullOrEmpty(pluginDll))
{
- // Soft load of package without taking ownership for the process .dll
using var pluginAssemblyFileStream = File.OpenRead(pluginDll);
plugin = await Plugin.LoadFromStream(pluginAssemblyFileStream, key);
var pluginAssembly = new PluginAssembly(manifest, plugin);
+ pluginAssembly.LoadContext(pluginDll);
+ LoadNuGetDependenciesInContext(manifest, new DirectoryInfo(pluginFolder), pluginAssembly.LoadContextInstance);
+ _pluginAssemblies[key] = pluginAssembly;
+
pluginAssemblyManager.AddAssembly(pluginAssembly);
logger.LogInformation("Assembly {AssemblyName} loaded at startup.", pluginDll);
await RegisterWithServiceLocator(pluginAssembly);
-
}
AppState.AddPlugin(key, manifest!);
@@ -179,6 +207,117 @@ public async Task LoadPluginsAtStartup()
}
+ // Remove a plugin by its Id (uninstalls and unregisters services)
+ public async Task RemovePlugin(string pluginId)
+ {
+
+ // Remove from AppState and get manifest
+ var manifest = AppState.RemovePlugin(pluginId);
+ if (manifest is null)
+ {
+ logger.LogInformation("Unable to remove plugin {pluginId}. Plugin not found.", pluginId);
+ return;
+ }
+
+ // Remove from service descriptors BEFORE unloading assembly
+ var descriptorsToRemove = new List();
+
+ // Remove services registered by the plugin assembly
+ if (_pluginAssemblies.TryGetValue(pluginId, out var assembly))
+ {
+ var pluginTypes = assembly.Assembly?.GetTypes() ?? [];
+
+ // Find all service descriptors that use types from this plugin assembly
+ foreach (var descriptor in _ServiceDescriptors.ToList())
+ {
+ bool shouldRemove = false;
+
+ // Check if implementation type is from this plugin
+ if (descriptor.ImplementationType is not null && pluginTypes.Contains(descriptor.ImplementationType))
+ {
+ shouldRemove = true;
+ }
+ // Check if implementation instance type is from this plugin
+ else if (descriptor.ImplementationInstance is not null && pluginTypes.Contains(descriptor.ImplementationInstance.GetType()))
+ {
+ shouldRemove = true;
+ }
+
+ if (shouldRemove)
+ {
+ descriptorsToRemove.Add(descriptor);
+ }
+ }
+ }
+
+ logger.LogInformation("Removing {count} service descriptors for plugin {pluginId}.", descriptorsToRemove.Count, pluginId);
+ foreach (var desc in descriptorsToRemove)
+ {
+ _ServiceDescriptors.Remove(desc);
+ }
+
+ // Now unload plugin AssemblyLoadContext
+ if (assembly is not null)
+ {
+ // Remove configuration section.
+ var configSectionType = assembly.Assembly?.GetTypes()
+ .FirstOrDefault(t => typeof(ISharpSiteConfigurationSection).IsAssignableFrom(t) && !t.IsAbstract);
+
+ if (configSectionType is not null)
+ {
+ var sectionInstance = (ISharpSiteConfigurationSection)Activator.CreateInstance(configSectionType)!;
+ AppState.ConfigurationSections.Remove(sectionInstance.SectionName);
+ }
+
+ _pluginAssemblies.Remove(pluginId);
+ assembly.UnloadContext();
+ }
+
+ // Remove plugin files/folder
+ var pluginFolder = Path.Combine("plugins", manifest.IdVersionToString());
+ if (Directory.Exists(pluginFolder))
+ {
+ try
+ {
+ Directory.Delete(pluginFolder, true);
+ logger.LogInformation("Deleted plugin files at {pluginFolder}", pluginFolder);
+ }
+ catch (Exception ex)
+ {
+ logger.LogWarning(ex, "Failed to delete plugin at {pluginFolder}", pluginFolder);
+ }
+ }
+ else
+ {
+ logger.LogWarning("Plugin folder does not exist at {pluginFolder}", pluginFolder);
+ }
+
+ // If this is a Theme plugin, remove related wwwroot folder in /plugins/_wwwroot/
+ if (manifest.Features is not null && manifest.Features.Contains(PluginFeatures.Theme))
+ {
+ var wwwrootThemeFolder = Path.Combine("plugins", "_wwwroot", manifest.IdVersionToString());
+ if (Directory.Exists(wwwrootThemeFolder))
+ {
+ try
+ {
+ Directory.Delete(wwwrootThemeFolder, true);
+ logger.LogInformation("Deleted theme wwwroot folder: {wwwrootThemeFolder}", wwwrootThemeFolder);
+ }
+ catch (Exception ex)
+ {
+ logger.LogWarning(ex, "Failed to delete theme wwwroot folder: {wwwrootThemeFolder}", wwwrootThemeFolder);
+ }
+ }
+ else
+ {
+ logger.LogWarning("Theme wwwroot folder not found: {wwwrootThemeFolder}", wwwrootThemeFolder);
+ }
+ }
+
+ _ServiceProvider = _ServiceDescriptors.BuildServiceProvider();
+ await AppState.Save();
+
+ }
private async Task RegisterWithServiceLocator(PluginAssembly pluginAssembly)
{
@@ -204,24 +343,22 @@ private async Task RegisterWithServiceLocator(PluginAssembly pluginAssembly)
_ => null
};
- var serviceDescriptor = new ServiceDescriptor(knownInterface!, type, pluginAttribute.Scope switch
+ if (knownInterface is not null)
{
- PluginServiceLocatorScope.Singleton => ServiceLifetime.Singleton,
- PluginServiceLocatorScope.Scoped => ServiceLifetime.Scoped,
- _ => ServiceLifetime.Transient
- });
- _ServiceDescriptors.Add(serviceDescriptor);
+ _ServiceDescriptors.Add(new ServiceDescriptor(knownInterface, type, pluginAttribute.Scope switch
+ {
+ PluginServiceLocatorScope.Singleton => ServiceLifetime.Singleton,
+ PluginServiceLocatorScope.Scoped => ServiceLifetime.Scoped,
+ _ => ServiceLifetime.Transient
+ }));
+ }
}
else if (typeof(ISharpSiteConfigurationSection).IsAssignableFrom(type))
{
var configurationSection = (ISharpSiteConfigurationSection)Activator.CreateInstance(type)!;
// we should only add the configuration section if it is not already present
- if (!AppState.ConfigurationSections.ContainsKey(configurationSection.SectionName))
- {
- AppState.ConfigurationSections.Add(configurationSection.SectionName, configurationSection);
- }
-
+ AppState.ConfigurationSections.TryAdd(configurationSection.SectionName, configurationSection);
_ServiceDescriptors.Add(new ServiceDescriptor(type, configurationSection));
if (AppState.Initialized)
@@ -297,7 +434,7 @@ public void CleanupCurrentUploadedPlugin()
public void ValidatePlugin(string pluginName)
{
- if (pluginName.StartsWith("_"))
+ if (pluginName.StartsWith('_'))
{
var exception = new Exception("Plugin filenames are not allowed to start with an underscore '_'");
logger.LogError(exception, "Invalid plugin filename: {FileName}", pluginName);
@@ -432,7 +569,7 @@ private static void EnsurePluginNotInstalled(PluginManifest? manifest, ILogger l
public async Task InstallDefaultPlugins()
{
-
+
var defaultPluginFolder = new DirectoryInfo("defaultplugins");
if (!defaultPluginFolder.Exists) return;
@@ -442,13 +579,18 @@ public async Task InstallDefaultPlugins()
using var stream = File.OpenRead(file.FullName);
var plugin = await Plugin.LoadFromStream(stream, file.Name);
- try {
+ try
+ {
HandleUploadedPlugin(plugin);
logger.LogInformation("Plugin {0} loaded from default plugins.", file.Name);
await SavePlugin();
- } catch (PluginException ex) {
+ }
+ catch (PluginException ex)
+ {
logger.LogError(ex, "Plugin {0} failed to load from default plugins.", file.Name);
- } finally {
+ }
+ finally
+ {
// Cleanup the plugin after processing
CleanupCurrentUploadedPlugin();
}