Implemented basic server file system endpoints and services. Implemented server files tab

This commit is contained in:
2025-03-03 18:07:49 +01:00
parent 30390dab71
commit 43b04ff630
21 changed files with 735 additions and 86 deletions

View File

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

View File

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

View File

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

View File

@@ -24,9 +24,12 @@ 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)

View File

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

View File

@@ -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()
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
if (!Directory.Exists(hostPath))
Directory.CreateDirectory(hostPath);
// 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;
}
}

View File

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

View 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('/');
}
}

View File

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

View File

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

View File

@@ -18,7 +18,6 @@
<ItemGroup>
<Folder Include="Http\Middleware\" />
<Folder Include="storage\volumes\" />
</ItemGroup>
<ItemGroup>

View File

@@ -8,7 +8,9 @@
"launchUrl": "swagger",
"applicationUrl": "http://localhost:5275",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
"ASPNETCORE_ENVIRONMENT": "Development",
"HTTPS_PROXY": "",
"HTTP_PROXY": ""
}
}
}

View File

@@ -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)
{
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");
@@ -205,7 +219,7 @@ public class ServerService : IHostedLifecycleService
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)
@@ -224,6 +238,7 @@ public class ServerService : IHostedLifecycleService
async Task DeleteServer()
{
await server.CancelTasks();
await server.DestroyStorage();
await server.RemoveInstallationVolume();
await server.RemoveRuntimeVolume();

View File

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

View File

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

View File

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

View File

@@ -19,10 +19,6 @@
<_ContentIncludedByDefault Remove="Pages\Home.razor"/>
</ItemGroup>
<ItemGroup>
<Folder Include="Helpers\"/>
</ItemGroup>
<ItemGroup>
<None Remove="Properties\launchSettings.json"/>
</ItemGroup>

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,6 @@
namespace MoonlightServers.Shared.Http.Responses.Client.Servers.Files;
public class ServerFilesUploadResponse
{
public string UploadUrl { get; set; }
}