Started adding asset api/auto import
This commit is contained in:
37
Moonlight.ApiServer/Helpers/PluginAssetFileProvider.cs
Normal file
37
Moonlight.ApiServer/Helpers/PluginAssetFileProvider.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Moonlight.Shared.Http.Responses.PluginsStream;
|
||||||
|
|
||||||
|
public class PluginsAssetManifest
|
||||||
|
{
|
||||||
|
public string[] CssFiles { get; set; }
|
||||||
|
public string[] JavascriptFiles { get; set; }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user