diff --git a/MoonlightServers.ApiServer/Http/Controllers/Client/ServerFileSystemController.cs b/MoonlightServers.ApiServer/Http/Controllers/Client/ServerFileSystemController.cs new file mode 100644 index 0000000..1f6fe0a --- /dev/null +++ b/MoonlightServers.ApiServer/Http/Controllers/Client/ServerFileSystemController.cs @@ -0,0 +1,127 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using MoonCore.Exceptions; +using MoonCore.Extended.Abstractions; +using Moonlight.ApiServer.Database.Entities; +using MoonlightServers.ApiServer.Database.Entities; +using MoonlightServers.ApiServer.Services; +using MoonlightServers.Shared.Http.Responses.Client.Servers.Files; + +namespace MoonlightServers.ApiServer.Http.Controllers.Client; + +[Authorize] +[ApiController] +[Route("api/client/servers")] +public class ServerFileSystemController : Controller +{ + private readonly DatabaseRepository ServerRepository; + private readonly DatabaseRepository UserRepository; + private readonly ServerFileSystemService ServerFileSystemService; + private readonly ServerService ServerService; + private readonly NodeService NodeService; + + public ServerFileSystemController( + DatabaseRepository serverRepository, + DatabaseRepository userRepository, + ServerFileSystemService serverFileSystemService, + ServerService serverService, + NodeService nodeService + ) + { + ServerRepository = serverRepository; + UserRepository = userRepository; + ServerFileSystemService = serverFileSystemService; + ServerService = serverService; + NodeService = nodeService; + } + + [HttpGet("{serverId:int}/files/list")] + public async Task List([FromRoute] int serverId, [FromQuery] string path) + { + var server = await GetServerById(serverId); + + var entries = await ServerFileSystemService.List(server, path); + + return entries.Select(x => new ServerFilesEntryResponse() + { + Name = x.Name, + Size = x.Size, + IsFile = x.IsFile, + CreatedAt = x.CreatedAt, + UpdatedAt = x.UpdatedAt + }).ToArray(); + } + + [HttpPost("{serverId:int}/files/move")] + public async Task Move([FromRoute] int serverId, [FromQuery] string oldPath, [FromQuery] string newPath) + { + var server = await GetServerById(serverId); + + await ServerFileSystemService.Move(server, oldPath, newPath); + } + + [HttpDelete("{serverId:int}/files/delete")] + public async Task Delete([FromRoute] int serverId, [FromQuery] string path) + { + var server = await GetServerById(serverId); + + await ServerFileSystemService.Delete(server, path); + } + + [HttpPost("{serverId:int}/files/mkdir")] + public async Task Mkdir([FromRoute] int serverId, [FromQuery] string path) + { + var server = await GetServerById(serverId); + + await ServerFileSystemService.Mkdir(server, path); + } + + [HttpGet("{serverId:int}/files/upload")] + public async Task Upload([FromRoute] int serverId, [FromQuery] string path) + { + var server = await GetServerById(serverId); + + var accessToken = NodeService.CreateAccessToken( + server.Node, + parameters => + { + parameters.Add("type", "upload"); + parameters.Add("serverId", server.Id); + parameters.Add("path", path); + }, + TimeSpan.FromMinutes(5) + ); + + var url = ""; + + url += server.Node.UseSsl ? "https://" : "http://"; + url += $"{server.Node.Fqdn}:{server.Node.HttpPort}/"; + url += $"api/servers/upload?token={accessToken}"; + + return new ServerFilesUploadResponse() + { + UploadUrl = url + }; + } + + private async Task GetServerById(int serverId) + { + var server = await ServerRepository + .Get() + .Include(x => x.Node) + .FirstOrDefaultAsync(x => x.Id == serverId); + + if (server == null) + throw new HttpApiException("No server with this id found", 404); + + var userIdClaim = User.Claims.First(x => x.Type == "userId"); + var userId = int.Parse(userIdClaim.Value); + var user = await UserRepository.Get().FirstAsync(x => x.Id == userId); + + if (!ServerService.IsAllowedToAccess(user, server)) + throw new HttpApiException("No server with this id found", 404); + + return server; + } +} \ No newline at end of file diff --git a/MoonlightServers.ApiServer/Services/ServerFileSystemService.cs b/MoonlightServers.ApiServer/Services/ServerFileSystemService.cs new file mode 100644 index 0000000..7f8cd64 --- /dev/null +++ b/MoonlightServers.ApiServer/Services/ServerFileSystemService.cs @@ -0,0 +1,82 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using MoonCore.Attributes; +using MoonCore.Extended.Abstractions; +using MoonCore.Helpers; +using MoonlightServers.ApiServer.Database.Entities; +using MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Servers; + +namespace MoonlightServers.ApiServer.Services; + +[Scoped] +public class ServerFileSystemService +{ + private readonly NodeService NodeService; + private readonly DatabaseRepository ServerRepository; + + public ServerFileSystemService( + NodeService nodeService, + DatabaseRepository serverRepository + ) + { + NodeService = nodeService; + ServerRepository = serverRepository; + } + + public async Task List(Server server, string path) + { + using var apiClient = await GetApiClient(server); + + return await apiClient.GetJson( + $"api/servers/{server.Id}/files/list?path={path}" + ); + } + + public async Task Move(Server server, string oldPath, string newPath) + { + using var apiClient = await GetApiClient(server); + + await apiClient.Post( + $"api/servers/{server.Id}/files/move?oldPath={oldPath}&newPath={newPath}" + ); + } + + public async Task Delete(Server server, string path) + { + using var apiClient = await GetApiClient(server); + + await apiClient.Delete( + $"api/servers/{server.Id}/files/delete?path={path}" + ); + } + + public async Task Mkdir(Server server, string path) + { + using var apiClient = await GetApiClient(server); + + await apiClient.Post( + $"api/servers/{server.Id}/files/mkdir?path={path}" + ); + } + + #region Helpers + + private async Task GetApiClient(Server server) + { + var serverWithNode = server; + + // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + // It can be null when its not included when loading via ef !!! + if (server.Node == null) + { + serverWithNode = await ServerRepository + .Get() + .Include(x => x.Node) + .FirstAsync(x => x.Id == server.Id); + } + + return NodeService.CreateApiClient(serverWithNode.Node); + } + + #endregion +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/Abstractions/Server.Create.cs b/MoonlightServers.Daemon/Abstractions/Server.Create.cs index 050a179..0511e76 100644 --- a/MoonlightServers.Daemon/Abstractions/Server.Create.cs +++ b/MoonlightServers.Daemon/Abstractions/Server.Create.cs @@ -14,14 +14,14 @@ public partial class Server // for analytics and automatic deletion await dockerImageService.Ensure(Configuration.DockerImage, async message => { await LogToConsole(message); }); - var hostPath = await EnsureRuntimeVolume(); + await EnsureRuntimeVolume(); await LogToConsole("Creating container"); var dockerClient = ServiceProvider.GetRequiredService(); var parameters = Configuration.ToRuntimeCreateParameters( - hostPath: hostPath, + hostPath: RuntimeVolumePath, containerName: RuntimeContainerName ); diff --git a/MoonlightServers.Daemon/Abstractions/Server.Initialize.cs b/MoonlightServers.Daemon/Abstractions/Server.Initialize.cs index 095a7de..b5e37c7 100644 --- a/MoonlightServers.Daemon/Abstractions/Server.Initialize.cs +++ b/MoonlightServers.Daemon/Abstractions/Server.Initialize.cs @@ -23,10 +23,13 @@ public partial class Server } else await InitializeStateMachine(ServerState.Offline); - - // And at last we initialize all events, so we can react to certain state changes and outputs. + + // Now we initialize all events, so we can react to certain state changes and outputs. // We need to do this regardless if the server was reattached or not, as it hasn't been initialized yet await InitializeEvents(); + + // Load storage configuration + await InitializeStorage(); } private Task InitializeStateMachine(ServerState initialState) diff --git a/MoonlightServers.Daemon/Abstractions/Server.Installation.cs b/MoonlightServers.Daemon/Abstractions/Server.Installation.cs index 9326244..17c9983 100644 --- a/MoonlightServers.Daemon/Abstractions/Server.Installation.cs +++ b/MoonlightServers.Daemon/Abstractions/Server.Installation.cs @@ -40,18 +40,18 @@ public partial class Server // for analytics and automatic deletion await dockerImageService.Ensure(installData.DockerImage, async message => { await LogToConsole(message); }); - // Ensuring storage configuration - var installationHostPath = await EnsureInstallationVolume(); - var runtimeHostPath = await EnsureRuntimeVolume(); + // Ensuring storage + await EnsureInstallationVolume(); + await EnsureRuntimeVolume(); // Write installation script to path var content = installData.Script.Replace("\r\n", "\n"); - await File.WriteAllTextAsync(PathBuilder.File(installationHostPath, "install.sh"), content); + await File.WriteAllTextAsync(PathBuilder.File(InstallationVolumePath, "install.sh"), content); // Creating container configuration var parameters = Configuration.ToInstallationCreateParameters( - runtimeHostPath, - installationHostPath, + RuntimeVolumePath, + InstallationVolumePath, InstallationContainerName, installData.DockerImage, installData.Shell diff --git a/MoonlightServers.Daemon/Abstractions/Server.Storage.cs b/MoonlightServers.Daemon/Abstractions/Server.Storage.cs index 2eb3ec0..ed69e2b 100644 --- a/MoonlightServers.Daemon/Abstractions/Server.Storage.cs +++ b/MoonlightServers.Daemon/Abstractions/Server.Storage.cs @@ -1,89 +1,121 @@ using MoonCore.Helpers; +using MoonCore.Unix.SecureFs; using MoonlightServers.Daemon.Configuration; +using MoonlightServers.Daemon.Helpers; namespace MoonlightServers.Daemon.Abstractions; public partial class Server { - private async Task EnsureRuntimeVolume() - { - var hostPath = GetRuntimeVolumePath(); - - await LogToConsole("Creating storage"); + public ServerFileSystem FileSystem { get; private set; } + + private SpinLock FsLock = new(); + + private SecureFileSystem? InternalFileSystem; + + private string RuntimeVolumePath; + private string InstallationVolumePath; - // Create volume if missing - if (!Directory.Exists(hostPath)) - Directory.CreateDirectory(hostPath); + private async Task InitializeStorage() + { + #region Configure paths + + var appConfiguration = ServiceProvider.GetRequiredService(); + + // Runtime + var runtimePath = PathBuilder.Dir(appConfiguration.Storage.Volumes, Configuration.Id.ToString()); + + if (appConfiguration.Storage.Volumes.StartsWith("/")) + RuntimeVolumePath = runtimePath; + else + RuntimeVolumePath = PathBuilder.Dir(Directory.GetCurrentDirectory(), runtimePath); + + // Installation + var installationPath = PathBuilder.Dir(appConfiguration.Storage.Install, Configuration.Id.ToString()); + + if (appConfiguration.Storage.Install.StartsWith("/")) + InstallationVolumePath = installationPath; + else + InstallationVolumePath = PathBuilder.Dir(Directory.GetCurrentDirectory(), installationPath); + + #endregion + + await ConnectRuntimeVolume(); + } + + public async Task DestroyStorage() + { + await DisconnectRuntimeVolume(); + } + + private async Task ConnectRuntimeVolume() + { + var gotLock = false; + + try + { + FsLock.Enter(ref gotLock); + + // We want to dispose the old fs if existing, to make sure we wont leave any file descriptors open + if(InternalFileSystem != null && !InternalFileSystem.IsDisposed) + InternalFileSystem.Dispose(); + + await EnsureRuntimeVolume(); + + InternalFileSystem = new SecureFileSystem(RuntimeVolumePath); + FileSystem = new ServerFileSystem(InternalFileSystem); + } + finally + { + if(gotLock) + FsLock.Exit(); + } + } + + private Task DisconnectRuntimeVolume() + { + if(InternalFileSystem != null && !InternalFileSystem.IsDisposed) + InternalFileSystem.Dispose(); + + return Task.CompletedTask; + } + + private Task EnsureRuntimeVolume() + { + if (!Directory.Exists(RuntimeVolumePath)) + Directory.CreateDirectory(RuntimeVolumePath); // TODO: Virtual disk - return hostPath; + return Task.CompletedTask; } - private string GetRuntimeVolumePath() + public Task RemoveRuntimeVolume() { - var appConfiguration = ServiceProvider.GetRequiredService(); - - var hostPath = PathBuilder.Dir( - appConfiguration.Storage.Volumes, - Configuration.Id.ToString() - ); - - if (hostPath.StartsWith("/")) - return hostPath; - else - return PathBuilder.JoinPaths(Directory.GetCurrentDirectory(), hostPath); - } - - public async Task RemoveRuntimeVolume() - { - var hostPath = GetRuntimeVolumePath(); - - await LogToConsole("Removing storage"); - // Remove volume if existing - if (Directory.Exists(hostPath)) - Directory.Delete(hostPath, true); + if (Directory.Exists(RuntimeVolumePath)) + Directory.Delete(RuntimeVolumePath, true); // TODO: Virtual disk + + return Task.CompletedTask; } - private async Task EnsureInstallationVolume() + private Task EnsureInstallationVolume() { - var hostPath = GetInstallationVolumePath(); - - await LogToConsole("Creating installation storage"); - // Create volume if missing - if (!Directory.Exists(hostPath)) - Directory.CreateDirectory(hostPath); + if (!Directory.Exists(InstallationVolumePath)) + Directory.CreateDirectory(InstallationVolumePath); - return hostPath; + return Task.CompletedTask; } - private string GetInstallationVolumePath() + public Task RemoveInstallationVolume() { - var appConfiguration = ServiceProvider.GetRequiredService(); + // Remove install volume if existing + if (Directory.Exists(InstallationVolumePath)) + Directory.Delete(InstallationVolumePath, true); - var hostPath = PathBuilder.Dir( - appConfiguration.Storage.Install, - Configuration.Id.ToString() - ); - - if (hostPath.StartsWith("/")) - return hostPath; - else - return PathBuilder.JoinPaths(Directory.GetCurrentDirectory(), hostPath); - } - - public async Task RemoveInstallationVolume() - { - var hostPath = GetInstallationVolumePath(); - - await LogToConsole("Removing installation storage"); - - // Remove volume if existing - if (Directory.Exists(hostPath)) - Directory.Delete(hostPath, true); + return Task.CompletedTask; } } \ No newline at end of file diff --git a/MoonlightServers.Daemon/Configuration/AppConfiguration.cs b/MoonlightServers.Daemon/Configuration/AppConfiguration.cs index 34bb52a..5273cf9 100644 --- a/MoonlightServers.Daemon/Configuration/AppConfiguration.cs +++ b/MoonlightServers.Daemon/Configuration/AppConfiguration.cs @@ -8,6 +8,7 @@ public class AppConfiguration public StorageData Storage { get; set; } = new(); public SecurityData Security { get; set; } = new(); public RemoteData Remote { get; set; } = new(); + public FilesData Files { get; set; } = new(); public class RemoteData { @@ -32,4 +33,9 @@ public class AppConfiguration public string Backups { get; set; } = PathBuilder.Dir("backups"); public string Install { get; set; } = PathBuilder.Dir("install"); } + + public class FilesData + { + public int UploadLimit { get; set; } = 500; + } } \ No newline at end of file diff --git a/MoonlightServers.Daemon/Helpers/ServerFileSystem.cs b/MoonlightServers.Daemon/Helpers/ServerFileSystem.cs new file mode 100644 index 0000000..0c4234f --- /dev/null +++ b/MoonlightServers.Daemon/Helpers/ServerFileSystem.cs @@ -0,0 +1,84 @@ +using Mono.Unix.Native; +using MoonCore.Unix.SecureFs; +using MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Servers; + +namespace MoonlightServers.Daemon.Helpers; + +public class ServerFileSystem +{ + private readonly SecureFileSystem FileSystem; + + public ServerFileSystem(SecureFileSystem fileSystem) + { + FileSystem = fileSystem; + } + + public Task List(string inputPath) + { + var path = Normalize(inputPath); + var entries = FileSystem.ReadDir(path); + + var result = entries + .Select(x => new ServerFileSystemResponse() + { + Name = x.Name, + IsFile = x.IsFile, + Size = x.Size, + UpdatedAt = x.LastChanged, + CreatedAt = x.CreatedAt + }) + .ToArray(); + + return Task.FromResult(result); + } + + public Task Move(string inputOldPath, string inputNewPath) + { + var oldPath = Normalize(inputOldPath); + var newPath = Normalize(inputNewPath); + + FileSystem.Rename(oldPath, newPath); + + return Task.CompletedTask; + } + + public Task Delete(string inputPath) + { + var path = Normalize(inputPath); + + FileSystem.RemoveAll(path); + + return Task.CompletedTask; + } + + public Task Mkdir(string inputPath) + { + var path = Normalize(inputPath); + + FileSystem.MkdirAll(path, FilePermissions.ACCESSPERMS); + + return Task.CompletedTask; + } + + public Task Create(string inputPath, Stream dataStream) + { + var path = Normalize(inputPath); + + var parentDirectory = Path.GetDirectoryName(path); + + if(!string.IsNullOrEmpty(parentDirectory) && parentDirectory != "/") + FileSystem.MkdirAll(parentDirectory, FilePermissions.ACCESSPERMS); + + FileSystem.WriteFile(path, dataStream); + + return Task.CompletedTask; + } + + private string Normalize(string path) + { + return path + .Replace("//", "/") + .Replace("..", "") + .TrimStart('/'); + } +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/Http/Controllers/Servers/ServerFileSystemController.cs b/MoonlightServers.Daemon/Http/Controllers/Servers/ServerFileSystemController.cs new file mode 100644 index 0000000..9285490 --- /dev/null +++ b/MoonlightServers.Daemon/Http/Controllers/Servers/ServerFileSystemController.cs @@ -0,0 +1,64 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using MoonCore.Exceptions; +using MoonlightServers.Daemon.Services; +using MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Servers; + +namespace MoonlightServers.Daemon.Http.Controllers.Servers; + +[Authorize] +[ApiController] +[Route("api/servers")] +public class ServerFileSystemController : Controller +{ + private readonly ServerService ServerService; + + public ServerFileSystemController(ServerService serverService) + { + ServerService = serverService; + } + + [HttpGet("{id:int}/files/list")] + public async Task List([FromRoute] int id, [FromQuery] string path = "") + { + var server = ServerService.GetServer(id); + + if (server == null) + throw new HttpApiException("No server with this id found", 404); + + return await server.FileSystem.List(path); + } + + [HttpPost("{id:int}/files/move")] + public async Task Move([FromRoute] int id, [FromQuery] string oldPath, [FromQuery] string newPath) + { + var server = ServerService.GetServer(id); + + if (server == null) + throw new HttpApiException("No server with this id found", 404); + + await server.FileSystem.Move(oldPath, newPath); + } + + [HttpDelete("{id:int}/files/delete")] + public async Task Delete([FromRoute] int id, [FromQuery] string path) + { + var server = ServerService.GetServer(id); + + if (server == null) + throw new HttpApiException("No server with this id found", 404); + + await server.FileSystem.Delete(path); + } + + [HttpPost("{id:int}/files/mkdir")] + public async Task Mkdir([FromRoute] int id, [FromQuery] string path) + { + var server = ServerService.GetServer(id); + + if (server == null) + throw new HttpApiException("No server with this id found", 404); + + await server.FileSystem.Mkdir(path); + } +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/Http/Controllers/Servers/UploadController.cs b/MoonlightServers.Daemon/Http/Controllers/Servers/UploadController.cs new file mode 100644 index 0000000..8556268 --- /dev/null +++ b/MoonlightServers.Daemon/Http/Controllers/Servers/UploadController.cs @@ -0,0 +1,69 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using MoonCore.Exceptions; +using MoonCore.Helpers; +using MoonlightServers.Daemon.Configuration; +using MoonlightServers.Daemon.Helpers; +using MoonlightServers.Daemon.Services; + +namespace MoonlightServers.Daemon.Http.Controllers.Servers; + +[ApiController] +[AllowAnonymous] +[Route("api/servers/upload")] +public class UploadController : Controller +{ + private readonly AccessTokenHelper AccessTokenHelper; + private readonly AppConfiguration Configuration; + private readonly ServerService ServerService; + + public UploadController( + AccessTokenHelper accessTokenHelper, + ServerService serverService, + AppConfiguration configuration + ) + { + AccessTokenHelper = accessTokenHelper; + ServerService = serverService; + Configuration = configuration; + } + + [HttpPost] + public async Task Upload([FromQuery] string token) + { + var file = Request.Form.Files.FirstOrDefault(); + + if (file == null) + throw new HttpApiException("No file provided", 400); + + if(file.Length > ByteConverter.FromMegaBytes(Configuration.Files.UploadLimit).Bytes) + throw new HttpApiException("The provided file is bigger than the upload limit", 400); + + #region Token validation + + if (!AccessTokenHelper.Process(token, out var claims)) + throw new HttpApiException("Invalid access token provided", 401); + + var typeClaim = claims.FirstOrDefault(x => x.Type == "type"); + + if (typeClaim == null || typeClaim.Value != "upload") + throw new HttpApiException("Invalid access token provided: Missing or invalid type", 401); + + var serverIdClaim = claims.FirstOrDefault(x => x.Type == "serverId"); + + if (serverIdClaim == null || !int.TryParse(serverIdClaim.Value, out var serverId)) + throw new HttpApiException("Invalid access token provided: Missing or invalid server id", 401); + + #endregion + + var server = ServerService.GetServer(serverId); + + if (server == null) + throw new HttpApiException("No server with this id found", 404); + + var dataStream = file.OpenReadStream(); + var path = file.FileName; + + await server.FileSystem.Create(path, dataStream); + } +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/MoonlightServers.Daemon.csproj b/MoonlightServers.Daemon/MoonlightServers.Daemon.csproj index ca4055e..e30d837 100644 --- a/MoonlightServers.Daemon/MoonlightServers.Daemon.csproj +++ b/MoonlightServers.Daemon/MoonlightServers.Daemon.csproj @@ -18,7 +18,6 @@ - diff --git a/MoonlightServers.Daemon/Properties/launchSettings.json b/MoonlightServers.Daemon/Properties/launchSettings.json index b3b3329..c0fa8c6 100644 --- a/MoonlightServers.Daemon/Properties/launchSettings.json +++ b/MoonlightServers.Daemon/Properties/launchSettings.json @@ -8,7 +8,9 @@ "launchUrl": "swagger", "applicationUrl": "http://localhost:5275", "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" + "ASPNETCORE_ENVIRONMENT": "Development", + "HTTPS_PROXY": "", + "HTTP_PROXY": "" } } } diff --git a/MoonlightServers.Daemon/Services/ServerService.cs b/MoonlightServers.Daemon/Services/ServerService.cs index 7b2d264..b91f99f 100644 --- a/MoonlightServers.Daemon/Services/ServerService.cs +++ b/MoonlightServers.Daemon/Services/ServerService.cs @@ -72,10 +72,24 @@ public class ServerService : IHostedLifecycleService servers = Servers.ToArray(); // - Logger.LogTrace("Canceling server tasks"); + Logger.LogTrace("Canceling server tasks and disconnecting storage"); foreach (var server in servers) - await server.CancelTasks(); + { + try + { + await server.CancelTasks(); + await server.DestroyStorage(); + } + catch (Exception e) + { + Logger.LogCritical( + "An unhandled error occured while stopping the server management for server {id}: {e}", + server.Id, + e + ); + } + } // Logger.LogTrace("Canceling own tasks"); @@ -203,9 +217,9 @@ public class ServerService : IHostedLifecycleService public async Task Delete(int serverId) { var server = GetServer(serverId); - + // If a server with this id doesn't exist we can just exit - if(server == null) + if (server == null) return; if (server.State == ServerState.Installing) @@ -214,7 +228,7 @@ public class ServerService : IHostedLifecycleService #region Callbacks var deleteCompletion = new TaskCompletionSource(); - + async Task HandleStateChange(ServerState state) { if (state == ServerState.Offline) @@ -224,9 +238,10 @@ public class ServerService : IHostedLifecycleService async Task DeleteServer() { await server.CancelTasks(); + await server.DestroyStorage(); await server.RemoveInstallationVolume(); await server.RemoveRuntimeVolume(); - + deleteCompletion.SetResult(); lock (Servers) @@ -234,7 +249,7 @@ public class ServerService : IHostedLifecycleService } #endregion - + // If the server is still online, we are killing it and then // waiting for the callback to trigger notifying us that the server is now offline // so we can delete it. The request will pause until then using the deleteCompletion task @@ -254,7 +269,7 @@ public class ServerService : IHostedLifecycleService lock (Servers) return Servers.FirstOrDefault(x => x.Id == id); } - + #region Lifecycle public Task StartAsync(CancellationToken cancellationToken) diff --git a/MoonlightServers.Daemon/Startup.cs b/MoonlightServers.Daemon/Startup.cs index 5838ba4..715937d 100644 --- a/MoonlightServers.Daemon/Startup.cs +++ b/MoonlightServers.Daemon/Startup.cs @@ -41,6 +41,7 @@ public class Startup await CreateWebApplicationBuilder(); + await ConfigureKestrel(); await RegisterAppConfiguration(); await RegisterLogging(); await RegisterBase(); @@ -84,6 +85,16 @@ public class Startup return Task.CompletedTask; } + private Task ConfigureKestrel() + { + WebApplicationBuilder.WebHost.ConfigureKestrel(options => + { + options.Limits.MaxRequestBodySize = ByteConverter.FromMegaBytes(Configuration.Files.UploadLimit).Bytes; + }); + + return Task.CompletedTask; + } + private Task UseBase() { WebApplication.UseRouting(); diff --git a/MoonlightServers.DaemonShared/DaemonSide/Http/Responses/Servers/ServerFileSystemResponse.cs b/MoonlightServers.DaemonShared/DaemonSide/Http/Responses/Servers/ServerFileSystemResponse.cs new file mode 100644 index 0000000..8126590 --- /dev/null +++ b/MoonlightServers.DaemonShared/DaemonSide/Http/Responses/Servers/ServerFileSystemResponse.cs @@ -0,0 +1,11 @@ +namespace MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Servers; + +public class ServerFileSystemResponse +{ + public string Name { get; set; } + public bool IsFile { get; set; } + public long Size { get; set; } + + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } +} \ No newline at end of file diff --git a/MoonlightServers.Frontend/Helpers/ServerFileSystemProvider.cs b/MoonlightServers.Frontend/Helpers/ServerFileSystemProvider.cs new file mode 100644 index 0000000..ee511b7 --- /dev/null +++ b/MoonlightServers.Frontend/Helpers/ServerFileSystemProvider.cs @@ -0,0 +1,60 @@ +using MoonCore.Blazor.Tailwind.Fm; +using MoonlightServers.Frontend.Services; + +namespace MoonlightServers.Frontend.Helpers; + +public class ServerFileSystemProvider : IFileSystemProvider +{ + private readonly int ServerId; + private readonly ServerFileSystemService FileSystemService; + + public ServerFileSystemProvider( + int serverId, + ServerFileSystemService fileSystemService + ) + { + ServerId = serverId; + FileSystemService = fileSystemService; + } + + public async Task List(string path) + { + var result = await FileSystemService.List(ServerId, path); + + return result + .Select(x => new FileSystemEntry() + { + Name = x.Name, + Size = x.Size, + IsFile = x.IsFile, + CreatedAt = x.CreatedAt, + UpdatedAt = x.UpdatedAt + }) + .ToArray(); + } + + public async Task Create(string path, Stream stream) + { + await FileSystemService.Upload(ServerId, path, stream); + } + + public async Task Move(string oldPath, string newPath) + { + await FileSystemService.Move(ServerId, oldPath, newPath); + } + + public async Task Delete(string path) + { + await FileSystemService.Delete(ServerId, path); + } + + public async Task CreateDirectory(string path) + { + await FileSystemService.Mkdir(ServerId, path); + } + + public Task Read(string path) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/MoonlightServers.Frontend/MoonlightServers.Frontend.csproj b/MoonlightServers.Frontend/MoonlightServers.Frontend.csproj index e6d57b7..7908bd6 100644 --- a/MoonlightServers.Frontend/MoonlightServers.Frontend.csproj +++ b/MoonlightServers.Frontend/MoonlightServers.Frontend.csproj @@ -19,10 +19,6 @@ <_ContentIncludedByDefault Remove="Pages\Home.razor"/> - - - - diff --git a/MoonlightServers.Frontend/Services/ServerFileSystemService.cs b/MoonlightServers.Frontend/Services/ServerFileSystemService.cs new file mode 100644 index 0000000..6e12e68 --- /dev/null +++ b/MoonlightServers.Frontend/Services/ServerFileSystemService.cs @@ -0,0 +1,58 @@ +using MoonCore.Attributes; +using MoonCore.Helpers; +using MoonlightServers.Shared.Http.Responses.Client.Servers.Files; + +namespace MoonlightServers.Frontend.Services; + +[Scoped] +public class ServerFileSystemService +{ + private readonly HttpApiClient ApiClient; + + public ServerFileSystemService(HttpApiClient apiClient) + { + ApiClient = apiClient; + } + + public async Task List(int serverId, string path) + { + return await ApiClient.GetJson( + $"api/client/servers/{serverId}/files/list?path={path}" + ); + } + + public async Task Move(int serverId, string oldPath, string newPath) + { + await ApiClient.Post( + $"api/client/servers/{serverId}/files/move?oldPath={oldPath}&newPath={newPath}" + ); + } + + public async Task Delete(int serverId, string path) + { + await ApiClient.Delete( + $"api/client/servers/{serverId}/files/delete?path={path}" + ); + } + + public async Task Mkdir(int serverId, string path) + { + await ApiClient.Post( + $"api/client/servers/{serverId}/files/mkdir?path={path}" + ); + } + + public async Task Upload(int serverId, string path, Stream dataStream) + { + var uploadSession = await ApiClient.GetJson( + $"api/client/servers/{serverId}/files/upload?path={path}" + ); + + using var httpClient = new HttpClient(); + + var content = new MultipartFormDataContent(); + content.Add(new StreamContent(dataStream), "file", path); + + await httpClient.PostAsync(uploadSession.UploadUrl, content); + } +} \ No newline at end of file diff --git a/MoonlightServers.Frontend/UI/Components/Servers/ServerTabs/FilesTab.razor b/MoonlightServers.Frontend/UI/Components/Servers/ServerTabs/FilesTab.razor index a41ada5..8b8e008 100644 --- a/MoonlightServers.Frontend/UI/Components/Servers/ServerTabs/FilesTab.razor +++ b/MoonlightServers.Frontend/UI/Components/Servers/ServerTabs/FilesTab.razor @@ -1,6 +1,19 @@ +@using MoonlightServers.Frontend.Services +@using MoonCore.Blazor.Tailwind.Fm +@using MoonlightServers.Frontend.Helpers + @inherits BaseServerTab +@inject ServerFileSystemService FileSystemService + + + @code { - + private IFileSystemProvider Provider; + + protected override void OnInitialized() + { + Provider = new ServerFileSystemProvider(Server.Id, FileSystemService); + } } diff --git a/MoonlightServers.Shared/Http/Responses/Client/Servers/Files/ServerFilesEntryResponse.cs b/MoonlightServers.Shared/Http/Responses/Client/Servers/Files/ServerFilesEntryResponse.cs new file mode 100644 index 0000000..77ca2d2 --- /dev/null +++ b/MoonlightServers.Shared/Http/Responses/Client/Servers/Files/ServerFilesEntryResponse.cs @@ -0,0 +1,11 @@ +namespace MoonlightServers.Shared.Http.Responses.Client.Servers.Files; + +public class ServerFilesEntryResponse +{ + public string Name { get; set; } + public bool IsFile { get; set; } + public long Size { get; set; } + + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } +} \ No newline at end of file diff --git a/MoonlightServers.Shared/Http/Responses/Client/Servers/Files/ServerFilesUploadResponse.cs b/MoonlightServers.Shared/Http/Responses/Client/Servers/Files/ServerFilesUploadResponse.cs new file mode 100644 index 0000000..14a9f66 --- /dev/null +++ b/MoonlightServers.Shared/Http/Responses/Client/Servers/Files/ServerFilesUploadResponse.cs @@ -0,0 +1,6 @@ +namespace MoonlightServers.Shared.Http.Responses.Client.Servers.Files; + +public class ServerFilesUploadResponse +{ + public string UploadUrl { get; set; } +} \ No newline at end of file