From bc737c830f86b6bc66c24b97ed7285eb4c6db6d9 Mon Sep 17 00:00:00 2001 From: Masu-Baumgartner <68913099+Masu-Baumgartner@users.noreply.github.com> Date: Tue, 26 Nov 2024 17:33:51 +0100 Subject: [PATCH] Started adding asset api/auto import --- .../Helpers/PluginAssetFileProvider.cs | 37 +++++++++ .../Controllers/PluginsStreamController.cs | 34 +++----- Moonlight.ApiServer/Services/PluginService.cs | 79 ++++++++++++++++++- Moonlight.ApiServer/Startup.cs | 11 +++ Moonlight.Client/Startup.cs | 18 +++++ Moonlight.Client/wwwroot/js/moonlight.js | 19 +++++ .../PluginsStream/PluginsAssetManifest.cs | 7 ++ 7 files changed, 176 insertions(+), 29 deletions(-) create mode 100644 Moonlight.ApiServer/Helpers/PluginAssetFileProvider.cs create mode 100644 Moonlight.Shared/Http/Responses/PluginsStream/PluginsAssetManifest.cs diff --git a/Moonlight.ApiServer/Helpers/PluginAssetFileProvider.cs b/Moonlight.ApiServer/Helpers/PluginAssetFileProvider.cs new file mode 100644 index 00000000..9eff0231 --- /dev/null +++ b/Moonlight.ApiServer/Helpers/PluginAssetFileProvider.cs @@ -0,0 +1,37 @@ +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.FileProviders.Physical; +using Microsoft.Extensions.Primitives; +using Moonlight.ApiServer.Services; + +namespace Moonlight.ApiServer.Helpers; + +public class PluginAssetFileProvider : IFileProvider +{ + private readonly PluginService PluginService; + + public PluginAssetFileProvider(PluginService pluginService) + { + PluginService = pluginService; + } + + public IDirectoryContents GetDirectoryContents(string subpath) + { + return NotFoundDirectoryContents.Singleton; + } + + public IFileInfo GetFileInfo(string subpath) + { + if (!PluginService.AssetMap.TryGetValue(subpath, out var physicalPath)) + return new NotFoundFileInfo(subpath); + + if (!File.Exists(physicalPath)) + return new NotFoundFileInfo(subpath); + + return new PhysicalFileInfo(new FileInfo(physicalPath)); + } + + public IChangeToken Watch(string filter) + { + return NullChangeToken.Singleton; + } +} \ No newline at end of file diff --git a/Moonlight.ApiServer/Http/Controllers/PluginsStreamController.cs b/Moonlight.ApiServer/Http/Controllers/PluginsStreamController.cs index 1c4e1d72..e145faca 100644 --- a/Moonlight.ApiServer/Http/Controllers/PluginsStreamController.cs +++ b/Moonlight.ApiServer/Http/Controllers/PluginsStreamController.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.Caching.Memory; using MoonCore.Exceptions; using MoonCore.Models; using Moonlight.ApiServer.Services; +using Moonlight.Shared.Http.Responses.PluginsStream; namespace Moonlight.ApiServer.Http.Controllers; @@ -22,36 +23,13 @@ public class PluginsStreamController : Controller [HttpGet] public Task GetManifest() { - var assembliesMap = Cache.GetOrCreate("clientPluginAssemblies", entry => - { - entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(15); - - return PluginService.GetAssemblies("client"); - })!; - - var entrypoints = Cache.GetOrCreate("clientPluginEntrypoints", entry => - { - entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(15); - - return PluginService.GetEntrypoints("client"); - })!; - - return Task.FromResult(new HostedPluginsManifest() - { - Assemblies = assembliesMap.Keys.ToArray(), - Entrypoints = entrypoints - }); + return Task.FromResult(PluginService.HostedPluginsManifest); } [HttpGet("stream")] public async Task GetAssembly([FromQuery(Name = "assembly")] string assembly) { - var assembliesMap = Cache.GetOrCreate("clientPluginAssemblies", entry => - { - entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(15); - - return PluginService.GetAssemblies("client"); - })!; + var assembliesMap = PluginService.AssemblyMap; if (assembliesMap.ContainsKey(assembly)) throw new HttpApiException("The requested assembly could not be found", 404); @@ -60,4 +38,10 @@ public class PluginsStreamController : Controller await Results.File(path).ExecuteAsync(HttpContext); } + + [HttpGet("assets")] + public Task GetAssetManifest() + { + return Task.FromResult(PluginService.PluginsAssetManifest); + } } \ No newline at end of file diff --git a/Moonlight.ApiServer/Services/PluginService.cs b/Moonlight.ApiServer/Services/PluginService.cs index 2139ee53..0b52e636 100644 --- a/Moonlight.ApiServer/Services/PluginService.cs +++ b/Moonlight.ApiServer/Services/PluginService.cs @@ -1,13 +1,19 @@ using System.Text.Json; using MoonCore.Helpers; +using MoonCore.Models; using Moonlight.ApiServer.Models; +using Moonlight.Shared.Http.Responses.PluginsStream; namespace Moonlight.ApiServer.Services; public class PluginService { public readonly List Plugins = new(); - + public readonly Dictionary AssetMap = new(); + public HostedPluginsManifest HostedPluginsManifest; + public PluginsAssetManifest PluginsAssetManifest; + public Dictionary AssemblyMap; + private static string PluginsFolder = PathBuilder.Dir("storage", "plugins"); private readonly ILogger Logger; @@ -74,14 +80,34 @@ public class PluginService plugin.Path, dependency ); - + pluginsToNotLoad.Add(plugin.Manifest.Id); break; } } - + // Remove unloadable plugins from cache Plugins.RemoveAll(x => pluginsToNotLoad.Contains(x.Manifest.Id)); + + // Generate assembly map for client + AssemblyMap = GetAssemblies("client"); + + // Generate plugin stream manifest for client + HostedPluginsManifest = new() + { + Assemblies = AssemblyMap.Keys.ToArray(), + Entrypoints = GetEntrypoints("client") + }; + + // Generate asset map + GenerateAssetMap(); + + // Generate asset manifest + PluginsAssetManifest = new() + { + CssFiles = AssetMap.Keys.Where(x => x.EndsWith(".css")).ToArray(), + JavascriptFiles = AssetMap.Keys.Where(x => x.EndsWith(".js")).ToArray(), + }; } public Dictionary GetAssemblies(string section) @@ -92,7 +118,7 @@ public class PluginService { foreach (var file in Directory.EnumerateFiles(PathBuilder.Dir(plugin.Path, "bin", section))) { - if(!file.EndsWith(".dll")) + if (!file.EndsWith(".dll")) continue; var fileName = Path.GetFileName(file); @@ -110,4 +136,49 @@ public class PluginService .SelectMany(x => x.Manifest.Entrypoints[section]) .ToArray(); } + + private void GenerateAssetMap() + { + AssetMap.Clear(); + + foreach (var plugin in Plugins) + { + var assetPath = PathBuilder.Dir(plugin.Path, "wwwroot"); + + if (!Directory.Exists(assetPath)) + continue; + + var files = new List(); + GetFilesInDirectory(assetPath, files); + + foreach (var file in files) + { + var mapPath = Formatter.ReplaceStart(file, assetPath, ""); + + mapPath = mapPath.Replace("\\", "/"); // To handle fucking windows + mapPath = mapPath.StartsWith("/") ? mapPath : "/" + mapPath; // Ensure starting / + + if (AssetMap.ContainsKey(mapPath)) + { + Logger.LogWarning( + "The plugin '{name}' tries to map an asset to the path '{path}' which is already used by another plugin. Ignoring asset mapping", + plugin.Manifest.Id, + mapPath + ); + + continue; + } + + AssetMap[mapPath] = file; + } + } + } + + private void GetFilesInDirectory(string directory, List files) + { + files.AddRange(Directory.EnumerateFiles(directory)); + + foreach (var dir in Directory.EnumerateDirectories(directory)) + GetFilesInDirectory(dir, files); + } } \ No newline at end of file diff --git a/Moonlight.ApiServer/Startup.cs b/Moonlight.ApiServer/Startup.cs index 62ec15b1..dabf8bc3 100644 --- a/Moonlight.ApiServer/Startup.cs +++ b/Moonlight.ApiServer/Startup.cs @@ -87,6 +87,7 @@ public class Startup await UseOAuth2(); await UseBaseMiddleware(); await HookPluginConfigure(); + await UsePluginAssets(); await MapBase(); await MapOAuth2(); @@ -271,6 +272,16 @@ public class Startup return Task.CompletedTask; } + private Task UsePluginAssets() + { + WebApplication.UseStaticFiles(new StaticFileOptions() + { + FileProvider = new PluginAssetFileProvider(PluginService) + }); + + return Task.CompletedTask; + } + #region Hooks private async Task HookPluginBuild() diff --git a/Moonlight.Client/Startup.cs b/Moonlight.Client/Startup.cs index e4f1a64a..11d86dee 100644 --- a/Moonlight.Client/Startup.cs +++ b/Moonlight.Client/Startup.cs @@ -1,6 +1,7 @@ using System.Reflection; using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; +using Microsoft.JSInterop; using MoonCore.Blazor.Extensions; using MoonCore.Blazor.Services; using MoonCore.Blazor.Tailwind.Extensions; @@ -14,6 +15,7 @@ using Moonlight.Client.Interfaces; using Moonlight.Client.Services; using Moonlight.Client.UI; using Moonlight.Client.UI.Forms; +using Moonlight.Shared.Http.Responses.PluginsStream; namespace Moonlight.Client; @@ -59,6 +61,8 @@ public class Startup await BuildWebAssemblyHost(); + await LoadPluginAssets(); + await WebAssemblyHost.RunAsync(); } @@ -164,6 +168,20 @@ public class Startup WebAssemblyHostBuilder.Services.AddSingleton(ApplicationAssemblyService); } + private async Task LoadPluginAssets() + { + var apiClient = WebAssemblyHost.Services.GetRequiredService(); + var assetManifest = await apiClient.GetJson("api/pluginsStream/assets"); + + var jsRuntime = WebAssemblyHost.Services.GetRequiredService(); + + foreach (var cssFile in assetManifest.CssFiles) + await jsRuntime.InvokeVoidAsync("moonlight.assets.loadCss", cssFile); + + foreach (var javascriptFile in assetManifest.JavascriptFiles) + await jsRuntime.InvokeVoidAsync("moonlight.assets.loadJavascript", javascriptFile); + } + #endregion #region Logging diff --git a/Moonlight.Client/wwwroot/js/moonlight.js b/Moonlight.Client/wwwroot/js/moonlight.js index fb80e39c..f4a5b4a6 100644 --- a/Moonlight.Client/wwwroot/js/moonlight.js +++ b/Moonlight.Client/wwwroot/js/moonlight.js @@ -25,5 +25,24 @@ window.moonlight = { closeCurrent() { window.close(); } + }, + assets: { + loadCss: function (url) { + let linkElement = document.createElement('link'); + + linkElement.href = url; + linkElement.rel = 'stylesheet'; + linkElement.type = 'text/css'; + + (document.head || document.documentElement).appendChild(linkElement); + }, + loadJavascript: function (url) { + let scriptElement = document.createElement('script'); + + scriptElement.src = url; + scriptElement.type = 'text/javascript'; + + (document.head || document.documentElement).appendChild(scriptElement); + } } }; \ No newline at end of file diff --git a/Moonlight.Shared/Http/Responses/PluginsStream/PluginsAssetManifest.cs b/Moonlight.Shared/Http/Responses/PluginsStream/PluginsAssetManifest.cs new file mode 100644 index 00000000..1dfa5ffb --- /dev/null +++ b/Moonlight.Shared/Http/Responses/PluginsStream/PluginsAssetManifest.cs @@ -0,0 +1,7 @@ +namespace Moonlight.Shared.Http.Responses.PluginsStream; + +public class PluginsAssetManifest +{ + public string[] CssFiles { get; set; } + public string[] JavascriptFiles { get; set; } +} \ No newline at end of file