diff --git a/MoonlightServers.ApiServer/Http/Controllers/Admin/Stars/StarImportExportController.cs b/MoonlightServers.ApiServer/Http/Controllers/Admin/Stars/StarImportExportController.cs new file mode 100644 index 0000000..1ae592f --- /dev/null +++ b/MoonlightServers.ApiServer/Http/Controllers/Admin/Stars/StarImportExportController.cs @@ -0,0 +1,52 @@ +using System.Text; +using Microsoft.AspNetCore.Mvc; +using MoonCore.Attributes; +using MoonCore.Exceptions; +using MoonCore.Helpers; +using MoonlightServers.ApiServer.Services; +using MoonlightServers.Shared.Http.Responses.Admin.Stars; + +namespace MoonlightServers.ApiServer.Http.Controllers.Admin.Stars; + +[ApiController] +[Route("api/admin/servers/stars")] +public class StarImportExportController : Controller +{ + private readonly StarImportExportService ImportExportService; + + public StarImportExportController(StarImportExportService importExportService) + { + ImportExportService = importExportService; + } + + [HttpGet("{starId:int}/export")] + [RequirePermission("admin.servers.stars.get")] + public async Task Export([FromRoute] int starId) + { + var exportedStar = await ImportExportService.Export(starId); + + Response.StatusCode = 200; + Response.ContentType = "application/json"; + await Response.WriteAsync(exportedStar); + } + + [HttpPost("import")] + [RequirePermission("admin.servers.stars.create")] + public async Task Import() + { + if (Request.Form.Files.Count == 0) + throw new HttpApiException("No file to import provided", 400); + + if (Request.Form.Files.Count > 1) + throw new HttpApiException("Only one file to import allowed", 400); + + var file = Request.Form.Files[0]; + await using var stream = file.OpenReadStream(); + using var sr = new StreamReader(stream, Encoding.UTF8); + var content = await sr.ReadToEndAsync(); + + var star = await ImportExportService.Import(content); + + return Mapper.Map(star); + } +} \ No newline at end of file diff --git a/MoonlightServers.ApiServer/Models/Stars/StarExportModel.cs b/MoonlightServers.ApiServer/Models/Stars/StarExportModel.cs new file mode 100644 index 0000000..5c06c28 --- /dev/null +++ b/MoonlightServers.ApiServer/Models/Stars/StarExportModel.cs @@ -0,0 +1,54 @@ +using MoonlightServers.Shared.Enums; + +namespace MoonlightServers.ApiServer.Models.Stars; + +public class StarExportModel +{ + // Meta + public string Name { get; set; } + public string Author { get; set; } + public string Version { get; set; } + public string? DonateUrl { get; set; } + public string? UpdateUrl { get; set; } + + // Start and stop + public string StartupCommand { get; set; } + public string StopCommand { get; set; } + public string OnlineDetection { get; set; } + + // Install + public string InstallShell { get; set; } + public string InstallDockerImage { get; set; } + public string InstallScript { get; set; } + + // Misc + public int RequiredAllocations { get; set; } + public bool AllowDockerImageChange { get; set; } + public string ParseConfiguration { get; set; } + + // Relations + public StarVariableExportModel[] Variables { get; set; } + public StarDockerImageExportModel[] DockerImages { get; set; } + + public class StarDockerImageExportModel + { + public string DisplayName { get; set; } + public string Identifier { get; set; } + public bool AutoPulling { get; set; } + } + + public class StarVariableExportModel + { + public string Name { get; set; } + public string Description { get; set; } + + public string Key { get; set; } + public string DefaultValue { get; set; } + + public bool AllowViewing { get; set; } + public bool AllowEditing { get; set; } + + public StarVariableType Type { get; set; } + public string? Filter { get; set; } + } +} \ No newline at end of file diff --git a/MoonlightServers.ApiServer/MoonlightServers.ApiServer.csproj b/MoonlightServers.ApiServer/MoonlightServers.ApiServer.csproj index 2d0d049..4617d4f 100644 --- a/MoonlightServers.ApiServer/MoonlightServers.ApiServer.csproj +++ b/MoonlightServers.ApiServer/MoonlightServers.ApiServer.csproj @@ -23,7 +23,6 @@ - diff --git a/MoonlightServers.ApiServer/Services/ExampleService.cs b/MoonlightServers.ApiServer/Services/ExampleService.cs deleted file mode 100644 index 767084f..0000000 --- a/MoonlightServers.ApiServer/Services/ExampleService.cs +++ /dev/null @@ -1,22 +0,0 @@ -using MoonCore.Attributes; - -namespace MoonlightServers.ApiServer.Services; - -[Singleton] -public class ExampleService -{ - private readonly Random Random; - private readonly ILogger Logger; - - public ExampleService(ILogger logger) - { - Logger = logger; - Random = new(); - } - - public async Task GetValue() - { - Logger.LogInformation("Generating value"); - return Random.Next(0, 10324); - } -} \ No newline at end of file diff --git a/MoonlightServers.ApiServer/Services/StarImportExportService.cs b/MoonlightServers.ApiServer/Services/StarImportExportService.cs new file mode 100644 index 0000000..ece90fa --- /dev/null +++ b/MoonlightServers.ApiServer/Services/StarImportExportService.cs @@ -0,0 +1,156 @@ +using System.Text.Json; +using Microsoft.EntityFrameworkCore; +using MoonCore.Attributes; +using MoonCore.Exceptions; +using MoonCore.Extended.Abstractions; +using MoonlightServers.ApiServer.Database.Entities; +using MoonlightServers.ApiServer.Models.Stars; + +namespace MoonlightServers.ApiServer.Services; + +[Scoped] +public class StarImportExportService +{ + private readonly DatabaseRepository StarRepository; + private readonly ILogger Logger; + + public StarImportExportService(DatabaseRepository starRepository, ILogger logger) + { + StarRepository = starRepository; + Logger = logger; + } + + public async Task Export(int id) + { + var star = StarRepository + .Get() + .Include(x => x.DockerImages) + .Include(x => x.Variables) + .FirstOrDefault(x => x.Id == id); + + if (star == null) + throw new HttpApiException("No star with this id found", 404); + + var exportModel = new StarExportModel() + { + Name = star.Name, + Author = star.Author, + Version = star.Version, + DonateUrl = star.DonateUrl, + UpdateUrl = star.UpdateUrl, + InstallScript = star.InstallScript, + InstallShell = star.InstallShell, + InstallDockerImage = star.InstallDockerImage, + OnlineDetection = star.OnlineDetection, + StopCommand = star.StopCommand, + StartupCommand = star.StartupCommand, + ParseConfiguration = star.ParseConfiguration, + RequiredAllocations = star.RequiredAllocations, + AllowDockerImageChange = star.AllowDockerImageChange, + Variables = star.Variables.Select(x => new StarExportModel.StarVariableExportModel() + { + Name = x.Name, + Type = x.Type, + Description = x.Description, + Filter = x.Filter, + Key = x.Key, + AllowEditing = x.AllowEditing, + AllowViewing = x.AllowViewing, + DefaultValue = x.DefaultValue + }).ToArray(), + DockerImages = star.DockerImages.Select(x => new StarExportModel.StarDockerImageExportModel() + { + Identifier = x.Identifier, + AutoPulling = x.AutoPulling, + DisplayName = x.DisplayName + }).ToArray() + }; + + var json = JsonSerializer.Serialize(exportModel, new JsonSerializerOptions() + { + WriteIndented = true + }); + + return json; + } + + public async Task Import(string json) + { + // Determine which importer to use based on simple patterns + if (json.Contains("RequiredAllocations")) + return await ImportStar(json); + else if (json.Contains("AllocationsNeeded")) + return await ImportImage(json); + else if (json.Contains("_comment")) + return await ImportEgg(json); + else + throw new HttpApiException("Unable to determine the format of the imported star/image/egg", 400); + } + + public async Task ImportStar(string json) + { + try + { + var model = JsonSerializer.Deserialize(json, new JsonSerializerOptions() + { + PropertyNameCaseInsensitive = true + }); + + ArgumentNullException.ThrowIfNull(model); + + var star = new Star() + { + Name = model.Name, + Author = model.Author, + Version = model.Version, + DonateUrl = model.DonateUrl, + UpdateUrl = model.UpdateUrl, + InstallScript = model.InstallScript, + InstallShell = model.InstallShell, + InstallDockerImage = model.InstallDockerImage, + OnlineDetection = model.OnlineDetection, + StopCommand = model.StopCommand, + StartupCommand = model.StartupCommand, + ParseConfiguration = model.ParseConfiguration, + RequiredAllocations = model.RequiredAllocations, + AllowDockerImageChange = model.AllowDockerImageChange, + Variables = model.Variables.Select(x => new StarVariable() + { + DefaultValue = x.DefaultValue, + Description = x.Description, + Filter = x.Filter, + Key = x.Key, + AllowEditing = x.AllowEditing, + AllowViewing = x.AllowViewing, + Type = x.Type, + Name = x.Name + }).ToList(), + DockerImages = model.DockerImages.Select(x => new StarDockerImage() + { + DisplayName = x.DisplayName, + AutoPulling = x.AutoPulling, + Identifier = x.Identifier + }).ToList() + }; + + var finalStar = StarRepository.Add(star); + + return finalStar; + } + catch (Exception e) + { + Logger.LogError("An unhandled error occured while importing star: {e}", e); + throw new HttpApiException("An unhandled error occured while importing the star", 400); + } + } + + public async Task ImportImage(string json) + { + throw new NotImplementedException(); + } + + public async Task ImportEgg(string json) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/MoonlightServers.Frontend/UI/Views/Admin/Stars/Index.razor b/MoonlightServers.Frontend/UI/Views/Admin/Stars/Index.razor index a4582bf..abf2b5f 100644 --- a/MoonlightServers.Frontend/UI/Views/Admin/Stars/Index.razor +++ b/MoonlightServers.Frontend/UI/Views/Admin/Stars/Index.razor @@ -6,20 +6,32 @@ @using MoonCore.Blazor.Tailwind.DataTable @using MoonlightServers.Shared.Http.Responses.Admin.Stars @using MoonCore.Blazor.Tailwind.Components +@using MoonCore.Blazor.Tailwind.Toasts +@using MoonCore.Exceptions +@using Moonlight.Client.Services @inject HttpApiClient ApiClient +@inject DownloadService DownloadService +@inject ToastService ToastService
- + + + @if (!string.IsNullOrEmpty(context.DonateUrl)) { @@ -36,11 +48,18 @@ Update } + + + + Export + @code { + private MinimalCrud Crud; + private void OnConfigure(MinimalCrudOptions options) { options.Title = "Stars"; @@ -51,4 +70,51 @@ options.UpdateUrl = item => ComponentHelper.GetRouteOfComponent(item.Id)!; options.DeleteFunction = async item => await ApiClient.Delete($"api/admin/servers/stars/{item.Id}"); } + + private async Task Export(StarDetailResponse star) + { + var json = await ApiClient.GetString($"api/admin/servers/stars/{star.Id}/export"); + + var formattedFileName = star.Name.Replace(" ", "_") + ".json"; + + await DownloadService.DownloadString(formattedFileName, json); + await ToastService.Success($"Successfully exported '{star.Name}'"); + } + + private async Task OnImportFiles(InputFileChangeEventArgs eventArgs) + { + IBrowserFile[] files; + + if(eventArgs.FileCount == 0) + return; + else if (eventArgs.FileCount > 1) + files = eventArgs.GetMultipleFiles().ToArray(); + else + files = [eventArgs.File]; + + foreach (var file in files) + { + try + { + if (!file.Name.EndsWith(".json")) + { + await ToastService.Danger($"Failed to import '{file.Name}': Only json files are supported"); + continue; + } + + await using var stream = file.OpenReadStream(); + var content = new MultipartFormDataContent(); + content.Add(new StreamContent(stream), "file", file.Name); + + var star = await ApiClient.PostJson("api/admin/servers/stars/import", content); + + await ToastService.Success($"Successfully imported '{star.Name}'"); + await Crud.Refresh(isSilent: false, bypassCache: true); + } + catch (HttpApiException e) + { + await ToastService.Danger($"Failed to import '{file.Name}': {e.Title}"); + } + } + } } \ No newline at end of file