Implemented basic server file system endpoints and services. Implemented server files tab
This commit is contained in:
@@ -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<Server> ServerRepository;
|
||||
private readonly DatabaseRepository<User> UserRepository;
|
||||
private readonly ServerFileSystemService ServerFileSystemService;
|
||||
private readonly ServerService ServerService;
|
||||
private readonly NodeService NodeService;
|
||||
|
||||
public ServerFileSystemController(
|
||||
DatabaseRepository<Server> serverRepository,
|
||||
DatabaseRepository<User> 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<ServerFilesEntryResponse[]> 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<ServerFilesUploadResponse> 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<Server> 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;
|
||||
}
|
||||
}
|
||||
@@ -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<Server> ServerRepository;
|
||||
|
||||
public ServerFileSystemService(
|
||||
NodeService nodeService,
|
||||
DatabaseRepository<Server> serverRepository
|
||||
)
|
||||
{
|
||||
NodeService = nodeService;
|
||||
ServerRepository = serverRepository;
|
||||
}
|
||||
|
||||
public async Task<ServerFileSystemResponse[]> List(Server server, string path)
|
||||
{
|
||||
using var apiClient = await GetApiClient(server);
|
||||
|
||||
return await apiClient.GetJson<ServerFileSystemResponse[]>(
|
||||
$"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<HttpApiClient> 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
|
||||
}
|
||||
@@ -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<DockerClient>();
|
||||
|
||||
var parameters = Configuration.ToRuntimeCreateParameters(
|
||||
hostPath: hostPath,
|
||||
hostPath: RuntimeVolumePath,
|
||||
containerName: RuntimeContainerName
|
||||
);
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<string> 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<AppConfiguration>();
|
||||
|
||||
// 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<AppConfiguration>();
|
||||
|
||||
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<string> 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<AppConfiguration>();
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
84
MoonlightServers.Daemon/Helpers/ServerFileSystem.cs
Normal file
84
MoonlightServers.Daemon/Helpers/ServerFileSystem.cs
Normal file
@@ -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<ServerFileSystemResponse[]> 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('/');
|
||||
}
|
||||
}
|
||||
@@ -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<ServerFileSystemResponse[]> 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,6 @@
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Http\Middleware\" />
|
||||
<Folder Include="storage\volumes\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -8,7 +8,9 @@
|
||||
"launchUrl": "swagger",
|
||||
"applicationUrl": "http://localhost:5275",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"HTTPS_PROXY": "",
|
||||
"HTTP_PROXY": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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<FileSystemEntry[]> 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<Stream> Read(string path)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
@@ -19,10 +19,6 @@
|
||||
<_ContentIncludedByDefault Remove="Pages\Home.razor"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Helpers\"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="Properties\launchSettings.json"/>
|
||||
</ItemGroup>
|
||||
|
||||
@@ -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<ServerFilesEntryResponse[]> List(int serverId, string path)
|
||||
{
|
||||
return await ApiClient.GetJson<ServerFilesEntryResponse[]>(
|
||||
$"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<ServerFilesUploadResponse>(
|
||||
$"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);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,19 @@
|
||||
@using MoonlightServers.Frontend.Services
|
||||
@using MoonCore.Blazor.Tailwind.Fm
|
||||
@using MoonlightServers.Frontend.Helpers
|
||||
|
||||
@inherits BaseServerTab
|
||||
|
||||
@inject ServerFileSystemService FileSystemService
|
||||
|
||||
<FileManager FileSystemProvider="Provider" />
|
||||
|
||||
@code
|
||||
{
|
||||
|
||||
private IFileSystemProvider Provider;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
Provider = new ServerFileSystemProvider(Server.Id, FileSystemService);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace MoonlightServers.Shared.Http.Responses.Client.Servers.Files;
|
||||
|
||||
public class ServerFilesUploadResponse
|
||||
{
|
||||
public string UploadUrl { get; set; }
|
||||
}
|
||||
Reference in New Issue
Block a user