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
|
// for analytics and automatic deletion
|
||||||
await dockerImageService.Ensure(Configuration.DockerImage, async message => { await LogToConsole(message); });
|
await dockerImageService.Ensure(Configuration.DockerImage, async message => { await LogToConsole(message); });
|
||||||
|
|
||||||
var hostPath = await EnsureRuntimeVolume();
|
await EnsureRuntimeVolume();
|
||||||
|
|
||||||
await LogToConsole("Creating container");
|
await LogToConsole("Creating container");
|
||||||
|
|
||||||
var dockerClient = ServiceProvider.GetRequiredService<DockerClient>();
|
var dockerClient = ServiceProvider.GetRequiredService<DockerClient>();
|
||||||
|
|
||||||
var parameters = Configuration.ToRuntimeCreateParameters(
|
var parameters = Configuration.ToRuntimeCreateParameters(
|
||||||
hostPath: hostPath,
|
hostPath: RuntimeVolumePath,
|
||||||
containerName: RuntimeContainerName
|
containerName: RuntimeContainerName
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -24,9 +24,12 @@ public partial class Server
|
|||||||
else
|
else
|
||||||
await InitializeStateMachine(ServerState.Offline);
|
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
|
// We need to do this regardless if the server was reattached or not, as it hasn't been initialized yet
|
||||||
await InitializeEvents();
|
await InitializeEvents();
|
||||||
|
|
||||||
|
// Load storage configuration
|
||||||
|
await InitializeStorage();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task InitializeStateMachine(ServerState initialState)
|
private Task InitializeStateMachine(ServerState initialState)
|
||||||
|
|||||||
@@ -40,18 +40,18 @@ public partial class Server
|
|||||||
// for analytics and automatic deletion
|
// for analytics and automatic deletion
|
||||||
await dockerImageService.Ensure(installData.DockerImage, async message => { await LogToConsole(message); });
|
await dockerImageService.Ensure(installData.DockerImage, async message => { await LogToConsole(message); });
|
||||||
|
|
||||||
// Ensuring storage configuration
|
// Ensuring storage
|
||||||
var installationHostPath = await EnsureInstallationVolume();
|
await EnsureInstallationVolume();
|
||||||
var runtimeHostPath = await EnsureRuntimeVolume();
|
await EnsureRuntimeVolume();
|
||||||
|
|
||||||
// Write installation script to path
|
// Write installation script to path
|
||||||
var content = installData.Script.Replace("\r\n", "\n");
|
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
|
// Creating container configuration
|
||||||
var parameters = Configuration.ToInstallationCreateParameters(
|
var parameters = Configuration.ToInstallationCreateParameters(
|
||||||
runtimeHostPath,
|
RuntimeVolumePath,
|
||||||
installationHostPath,
|
InstallationVolumePath,
|
||||||
InstallationContainerName,
|
InstallationContainerName,
|
||||||
installData.DockerImage,
|
installData.DockerImage,
|
||||||
installData.Shell
|
installData.Shell
|
||||||
|
|||||||
@@ -1,89 +1,121 @@
|
|||||||
using MoonCore.Helpers;
|
using MoonCore.Helpers;
|
||||||
|
using MoonCore.Unix.SecureFs;
|
||||||
using MoonlightServers.Daemon.Configuration;
|
using MoonlightServers.Daemon.Configuration;
|
||||||
|
using MoonlightServers.Daemon.Helpers;
|
||||||
|
|
||||||
namespace MoonlightServers.Daemon.Abstractions;
|
namespace MoonlightServers.Daemon.Abstractions;
|
||||||
|
|
||||||
public partial class Server
|
public partial class Server
|
||||||
{
|
{
|
||||||
private async Task<string> EnsureRuntimeVolume()
|
public ServerFileSystem FileSystem { get; private set; }
|
||||||
|
|
||||||
|
private SpinLock FsLock = new();
|
||||||
|
|
||||||
|
private SecureFileSystem? InternalFileSystem;
|
||||||
|
|
||||||
|
private string RuntimeVolumePath;
|
||||||
|
private string InstallationVolumePath;
|
||||||
|
|
||||||
|
private async Task InitializeStorage()
|
||||||
{
|
{
|
||||||
var hostPath = GetRuntimeVolumePath();
|
#region Configure paths
|
||||||
|
|
||||||
await LogToConsole("Creating storage");
|
var appConfiguration = ServiceProvider.GetRequiredService<AppConfiguration>();
|
||||||
|
|
||||||
// Create volume if missing
|
// Runtime
|
||||||
if (!Directory.Exists(hostPath))
|
var runtimePath = PathBuilder.Dir(appConfiguration.Storage.Volumes, Configuration.Id.ToString());
|
||||||
Directory.CreateDirectory(hostPath);
|
|
||||||
|
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
|
// 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
|
// Remove volume if existing
|
||||||
if (Directory.Exists(hostPath))
|
if (Directory.Exists(RuntimeVolumePath))
|
||||||
Directory.Delete(hostPath, true);
|
Directory.Delete(RuntimeVolumePath, true);
|
||||||
|
|
||||||
// TODO: Virtual disk
|
// 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
|
// Create volume if missing
|
||||||
if (!Directory.Exists(hostPath))
|
if (!Directory.Exists(InstallationVolumePath))
|
||||||
Directory.CreateDirectory(hostPath);
|
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(
|
return Task.CompletedTask;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8,6 +8,7 @@ public class AppConfiguration
|
|||||||
public StorageData Storage { get; set; } = new();
|
public StorageData Storage { get; set; } = new();
|
||||||
public SecurityData Security { get; set; } = new();
|
public SecurityData Security { get; set; } = new();
|
||||||
public RemoteData Remote { get; set; } = new();
|
public RemoteData Remote { get; set; } = new();
|
||||||
|
public FilesData Files { get; set; } = new();
|
||||||
|
|
||||||
public class RemoteData
|
public class RemoteData
|
||||||
{
|
{
|
||||||
@@ -32,4 +33,9 @@ public class AppConfiguration
|
|||||||
public string Backups { get; set; } = PathBuilder.Dir("backups");
|
public string Backups { get; set; } = PathBuilder.Dir("backups");
|
||||||
public string Install { get; set; } = PathBuilder.Dir("install");
|
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>
|
<ItemGroup>
|
||||||
<Folder Include="Http\Middleware\" />
|
<Folder Include="Http\Middleware\" />
|
||||||
<Folder Include="storage\volumes\" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -8,7 +8,9 @@
|
|||||||
"launchUrl": "swagger",
|
"launchUrl": "swagger",
|
||||||
"applicationUrl": "http://localhost:5275",
|
"applicationUrl": "http://localhost:5275",
|
||||||
"environmentVariables": {
|
"environmentVariables": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||||
|
"HTTPS_PROXY": "",
|
||||||
|
"HTTP_PROXY": ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,10 +72,24 @@ public class ServerService : IHostedLifecycleService
|
|||||||
servers = Servers.ToArray();
|
servers = Servers.ToArray();
|
||||||
|
|
||||||
//
|
//
|
||||||
Logger.LogTrace("Canceling server tasks");
|
Logger.LogTrace("Canceling server tasks and disconnecting storage");
|
||||||
|
|
||||||
foreach (var server in servers)
|
foreach (var server in servers)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
await server.CancelTasks();
|
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");
|
Logger.LogTrace("Canceling own tasks");
|
||||||
@@ -205,7 +219,7 @@ public class ServerService : IHostedLifecycleService
|
|||||||
var server = GetServer(serverId);
|
var server = GetServer(serverId);
|
||||||
|
|
||||||
// If a server with this id doesn't exist we can just exit
|
// If a server with this id doesn't exist we can just exit
|
||||||
if(server == null)
|
if (server == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (server.State == ServerState.Installing)
|
if (server.State == ServerState.Installing)
|
||||||
@@ -224,6 +238,7 @@ public class ServerService : IHostedLifecycleService
|
|||||||
async Task DeleteServer()
|
async Task DeleteServer()
|
||||||
{
|
{
|
||||||
await server.CancelTasks();
|
await server.CancelTasks();
|
||||||
|
await server.DestroyStorage();
|
||||||
await server.RemoveInstallationVolume();
|
await server.RemoveInstallationVolume();
|
||||||
await server.RemoveRuntimeVolume();
|
await server.RemoveRuntimeVolume();
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ public class Startup
|
|||||||
|
|
||||||
await CreateWebApplicationBuilder();
|
await CreateWebApplicationBuilder();
|
||||||
|
|
||||||
|
await ConfigureKestrel();
|
||||||
await RegisterAppConfiguration();
|
await RegisterAppConfiguration();
|
||||||
await RegisterLogging();
|
await RegisterLogging();
|
||||||
await RegisterBase();
|
await RegisterBase();
|
||||||
@@ -84,6 +85,16 @@ public class Startup
|
|||||||
return Task.CompletedTask;
|
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()
|
private Task UseBase()
|
||||||
{
|
{
|
||||||
WebApplication.UseRouting();
|
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"/>
|
<_ContentIncludedByDefault Remove="Pages\Home.razor"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Folder Include="Helpers\"/>
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Remove="Properties\launchSettings.json"/>
|
<None Remove="Properties\launchSettings.json"/>
|
||||||
</ItemGroup>
|
</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
|
@inherits BaseServerTab
|
||||||
|
|
||||||
|
@inject ServerFileSystemService FileSystemService
|
||||||
|
|
||||||
|
<FileManager FileSystemProvider="Provider" />
|
||||||
|
|
||||||
@code
|
@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