Implemented importing and exporting of stars

This commit is contained in:
2024-12-08 13:02:07 +01:00
parent c469814782
commit ebab0daf26
6 changed files with 329 additions and 24 deletions

View File

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

View File

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

View File

@@ -23,7 +23,6 @@
<Folder Include="Http\Middleware\"/>
<Folder Include="Implementations\"/>
<Folder Include="Interfaces\"/>
<Folder Include="Models\"/>
</ItemGroup>
</Project>

View File

@@ -1,22 +0,0 @@
using MoonCore.Attributes;
namespace MoonlightServers.ApiServer.Services;
[Singleton]
public class ExampleService
{
private readonly Random Random;
private readonly ILogger<ExampleService> Logger;
public ExampleService(ILogger<ExampleService> logger)
{
Logger = logger;
Random = new();
}
public async Task<int> GetValue()
{
Logger.LogInformation("Generating value");
return Random.Next(0, 10324);
}
}

View File

@@ -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<Star> StarRepository;
private readonly ILogger<StarImportExportService> Logger;
public StarImportExportService(DatabaseRepository<Star> starRepository, ILogger<StarImportExportService> logger)
{
StarRepository = starRepository;
Logger = logger;
}
public async Task<string> 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<Star> 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<Star> ImportStar(string json)
{
try
{
var model = JsonSerializer.Deserialize<StarExportModel>(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<Star> ImportImage(string json)
{
throw new NotImplementedException();
}
public async Task<Star> ImportEgg(string json)
{
throw new NotImplementedException();
}
}

View File

@@ -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
<div class="mb-3">
<NavTabs Index="3" Names="@UiConstants.AdminNavNames" Links="@UiConstants.AdminNavLinks"/>
</div>
<MinimalCrud TItem="StarDetailResponse" OnConfigure="OnConfigure">
<MinimalCrud @ref="Crud" TItem="StarDetailResponse" OnConfigure="OnConfigure">
<ChildContent>
<DataColumn TItem="StarDetailResponse" Field="@(x => x.Id)" Title="Id" IsSortable="true"/>
<DataColumn TItem="StarDetailResponse" Field="@(x => x.Name)" Title="Name" IsSortable="true"/>
<DataColumn TItem="StarDetailResponse" Field="@(x => x.Version)" Title="Version" IsSortable="true"/>
<DataColumn TItem="StarDetailResponse" Field="@(x => x.Author)" Title="Author"/>
</ChildContent>
<Actions>
<InputFile id="import-file" hidden="" multiple OnChange="OnImportFiles"/>
<label for="import-file" class="btn btn-tertiary cursor-pointer">
<i class="icon-file-up mr-2"></i>
Import
</label>
</Actions>
<ItemActions>
@if (!string.IsNullOrEmpty(context.DonateUrl))
{
@@ -36,11 +48,18 @@
<span class="align-middle">Update</span>
</a>
}
<a href="#" @onclick="() => Export(context)" @onclick:preventDefault class="text-success-500 mr-3">
<i class="icon-download align-middle"></i>
<span class="align-middle">Export</span>
</a>
</ItemActions>
</MinimalCrud>
@code
{
private MinimalCrud<StarDetailResponse> Crud;
private void OnConfigure(MinimalCrudOptions<StarDetailResponse> options)
{
options.Title = "Stars";
@@ -51,4 +70,51 @@
options.UpdateUrl = item => ComponentHelper.GetRouteOfComponent<Update>(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<StarDetailResponse>("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}");
}
}
}
}