Started adding asset api/auto import

This commit is contained in:
Masu-Baumgartner
2024-11-26 17:33:51 +01:00
parent 23a74bdfc6
commit bc737c830f
7 changed files with 176 additions and 29 deletions

View File

@@ -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;
}
}

View File

@@ -3,6 +3,7 @@ using Microsoft.Extensions.Caching.Memory;
using MoonCore.Exceptions; using MoonCore.Exceptions;
using MoonCore.Models; using MoonCore.Models;
using Moonlight.ApiServer.Services; using Moonlight.ApiServer.Services;
using Moonlight.Shared.Http.Responses.PluginsStream;
namespace Moonlight.ApiServer.Http.Controllers; namespace Moonlight.ApiServer.Http.Controllers;
@@ -22,36 +23,13 @@ public class PluginsStreamController : Controller
[HttpGet] [HttpGet]
public Task<HostedPluginsManifest> GetManifest() public Task<HostedPluginsManifest> GetManifest()
{ {
var assembliesMap = Cache.GetOrCreate("clientPluginAssemblies", entry => return Task.FromResult(PluginService.HostedPluginsManifest);
{
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
});
} }
[HttpGet("stream")] [HttpGet("stream")]
public async Task GetAssembly([FromQuery(Name = "assembly")] string assembly) public async Task GetAssembly([FromQuery(Name = "assembly")] string assembly)
{ {
var assembliesMap = Cache.GetOrCreate("clientPluginAssemblies", entry => var assembliesMap = PluginService.AssemblyMap;
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(15);
return PluginService.GetAssemblies("client");
})!;
if (assembliesMap.ContainsKey(assembly)) if (assembliesMap.ContainsKey(assembly))
throw new HttpApiException("The requested assembly could not be found", 404); 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); await Results.File(path).ExecuteAsync(HttpContext);
} }
[HttpGet("assets")]
public Task<PluginsAssetManifest> GetAssetManifest()
{
return Task.FromResult(PluginService.PluginsAssetManifest);
}
} }

View File

@@ -1,13 +1,19 @@
using System.Text.Json; using System.Text.Json;
using MoonCore.Helpers; using MoonCore.Helpers;
using MoonCore.Models;
using Moonlight.ApiServer.Models; using Moonlight.ApiServer.Models;
using Moonlight.Shared.Http.Responses.PluginsStream;
namespace Moonlight.ApiServer.Services; namespace Moonlight.ApiServer.Services;
public class PluginService public class PluginService
{ {
public readonly List<PluginMeta> Plugins = new(); public readonly List<PluginMeta> Plugins = new();
public readonly Dictionary<string, string> AssetMap = new();
public HostedPluginsManifest HostedPluginsManifest;
public PluginsAssetManifest PluginsAssetManifest;
public Dictionary<string, string> AssemblyMap;
private static string PluginsFolder = PathBuilder.Dir("storage", "plugins"); private static string PluginsFolder = PathBuilder.Dir("storage", "plugins");
private readonly ILogger<PluginService> Logger; private readonly ILogger<PluginService> Logger;
@@ -74,14 +80,34 @@ public class PluginService
plugin.Path, plugin.Path,
dependency dependency
); );
pluginsToNotLoad.Add(plugin.Manifest.Id); pluginsToNotLoad.Add(plugin.Manifest.Id);
break; break;
} }
} }
// Remove unloadable plugins from cache // Remove unloadable plugins from cache
Plugins.RemoveAll(x => pluginsToNotLoad.Contains(x.Manifest.Id)); 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<string, string> GetAssemblies(string section) public Dictionary<string, string> GetAssemblies(string section)
@@ -92,7 +118,7 @@ public class PluginService
{ {
foreach (var file in Directory.EnumerateFiles(PathBuilder.Dir(plugin.Path, "bin", section))) foreach (var file in Directory.EnumerateFiles(PathBuilder.Dir(plugin.Path, "bin", section)))
{ {
if(!file.EndsWith(".dll")) if (!file.EndsWith(".dll"))
continue; continue;
var fileName = Path.GetFileName(file); var fileName = Path.GetFileName(file);
@@ -110,4 +136,49 @@ public class PluginService
.SelectMany(x => x.Manifest.Entrypoints[section]) .SelectMany(x => x.Manifest.Entrypoints[section])
.ToArray(); .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<string>();
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<string> files)
{
files.AddRange(Directory.EnumerateFiles(directory));
foreach (var dir in Directory.EnumerateDirectories(directory))
GetFilesInDirectory(dir, files);
}
} }

View File

@@ -87,6 +87,7 @@ public class Startup
await UseOAuth2(); await UseOAuth2();
await UseBaseMiddleware(); await UseBaseMiddleware();
await HookPluginConfigure(); await HookPluginConfigure();
await UsePluginAssets();
await MapBase(); await MapBase();
await MapOAuth2(); await MapOAuth2();
@@ -271,6 +272,16 @@ public class Startup
return Task.CompletedTask; return Task.CompletedTask;
} }
private Task UsePluginAssets()
{
WebApplication.UseStaticFiles(new StaticFileOptions()
{
FileProvider = new PluginAssetFileProvider(PluginService)
});
return Task.CompletedTask;
}
#region Hooks #region Hooks
private async Task HookPluginBuild() private async Task HookPluginBuild()

View File

@@ -1,6 +1,7 @@
using System.Reflection; using System.Reflection;
using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.JSInterop;
using MoonCore.Blazor.Extensions; using MoonCore.Blazor.Extensions;
using MoonCore.Blazor.Services; using MoonCore.Blazor.Services;
using MoonCore.Blazor.Tailwind.Extensions; using MoonCore.Blazor.Tailwind.Extensions;
@@ -14,6 +15,7 @@ using Moonlight.Client.Interfaces;
using Moonlight.Client.Services; using Moonlight.Client.Services;
using Moonlight.Client.UI; using Moonlight.Client.UI;
using Moonlight.Client.UI.Forms; using Moonlight.Client.UI.Forms;
using Moonlight.Shared.Http.Responses.PluginsStream;
namespace Moonlight.Client; namespace Moonlight.Client;
@@ -59,6 +61,8 @@ public class Startup
await BuildWebAssemblyHost(); await BuildWebAssemblyHost();
await LoadPluginAssets();
await WebAssemblyHost.RunAsync(); await WebAssemblyHost.RunAsync();
} }
@@ -164,6 +168,20 @@ public class Startup
WebAssemblyHostBuilder.Services.AddSingleton(ApplicationAssemblyService); WebAssemblyHostBuilder.Services.AddSingleton(ApplicationAssemblyService);
} }
private async Task LoadPluginAssets()
{
var apiClient = WebAssemblyHost.Services.GetRequiredService<HttpApiClient>();
var assetManifest = await apiClient.GetJson<PluginsAssetManifest>("api/pluginsStream/assets");
var jsRuntime = WebAssemblyHost.Services.GetRequiredService<IJSRuntime>();
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 #endregion
#region Logging #region Logging

View File

@@ -25,5 +25,24 @@ window.moonlight = {
closeCurrent() { closeCurrent() {
window.close(); 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);
}
} }
}; };

View File

@@ -0,0 +1,7 @@
namespace Moonlight.Shared.Http.Responses.PluginsStream;
public class PluginsAssetManifest
{
public string[] CssFiles { get; set; }
public string[] JavascriptFiles { get; set; }
}