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.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<HostedPluginsManifest> 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<PluginsAssetManifest> GetAssetManifest()
{
return Task.FromResult(PluginService.PluginsAssetManifest);
}
}

View File

@@ -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<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 readonly ILogger<PluginService> 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<string, string> 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<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 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()

View File

@@ -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<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
#region Logging

View File

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

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