Implemented zip and tar compressing and decompressing. Implemented chunked file uploading
This commit is contained in:
@@ -6,6 +6,8 @@ using MoonCore.Extended.Abstractions;
|
|||||||
using Moonlight.ApiServer.Database.Entities;
|
using Moonlight.ApiServer.Database.Entities;
|
||||||
using MoonlightServers.ApiServer.Database.Entities;
|
using MoonlightServers.ApiServer.Database.Entities;
|
||||||
using MoonlightServers.ApiServer.Services;
|
using MoonlightServers.ApiServer.Services;
|
||||||
|
using MoonlightServers.DaemonShared.Enums;
|
||||||
|
using MoonlightServers.Shared.Http.Requests.Client.Servers.Files;
|
||||||
using MoonlightServers.Shared.Http.Responses.Client.Servers.Files;
|
using MoonlightServers.Shared.Http.Responses.Client.Servers.Files;
|
||||||
|
|
||||||
namespace MoonlightServers.ApiServer.Http.Controllers.Client;
|
namespace MoonlightServers.ApiServer.Http.Controllers.Client;
|
||||||
@@ -132,6 +134,28 @@ public class ServerFileSystemController : Controller
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPost("{serverId:int}/files/compress")]
|
||||||
|
public async Task Compress([FromRoute] int serverId, [FromBody] ServerFilesCompressRequest request)
|
||||||
|
{
|
||||||
|
var server = await GetServerById(serverId);
|
||||||
|
|
||||||
|
if (!Enum.TryParse(request.Type, true, out CompressType type))
|
||||||
|
throw new HttpApiException("Invalid compress type provided", 400);
|
||||||
|
|
||||||
|
await ServerFileSystemService.Compress(server, type, request.Items, request.Destination);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("{serverId:int}/files/decompress")]
|
||||||
|
public async Task Decompress([FromRoute] int serverId, [FromBody] ServerFilesDecompressRequest request)
|
||||||
|
{
|
||||||
|
var server = await GetServerById(serverId);
|
||||||
|
|
||||||
|
if (!Enum.TryParse(request.Type, true, out CompressType type))
|
||||||
|
throw new HttpApiException("Invalid compress type provided", 400);
|
||||||
|
|
||||||
|
await ServerFileSystemService.Decompress(server, type, request.Path, request.Destination);
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<Server> GetServerById(int serverId)
|
private async Task<Server> GetServerById(int serverId)
|
||||||
{
|
{
|
||||||
var server = await ServerRepository
|
var server = await ServerRepository
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ using MoonCore.Attributes;
|
|||||||
using MoonCore.Extended.Abstractions;
|
using MoonCore.Extended.Abstractions;
|
||||||
using MoonCore.Helpers;
|
using MoonCore.Helpers;
|
||||||
using MoonlightServers.ApiServer.Database.Entities;
|
using MoonlightServers.ApiServer.Database.Entities;
|
||||||
|
using MoonlightServers.DaemonShared.DaemonSide.Http.Requests;
|
||||||
using MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Servers;
|
using MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Servers;
|
||||||
|
using MoonlightServers.DaemonShared.Enums;
|
||||||
|
|
||||||
namespace MoonlightServers.ApiServer.Services;
|
namespace MoonlightServers.ApiServer.Services;
|
||||||
|
|
||||||
@@ -59,6 +61,36 @@ public class ServerFileSystemService
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task Compress(Server server, CompressType type, string[] items, string destination)
|
||||||
|
{
|
||||||
|
using var apiClient = await GetApiClient(server);
|
||||||
|
|
||||||
|
await apiClient.Post(
|
||||||
|
$"api/servers/{server.Id}/files/compress",
|
||||||
|
new ServerFilesCompressRequest()
|
||||||
|
{
|
||||||
|
Type = type,
|
||||||
|
Items = items,
|
||||||
|
Destination = destination
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Decompress(Server server, CompressType type, string path, string destination)
|
||||||
|
{
|
||||||
|
using var apiClient = await GetApiClient(server);
|
||||||
|
|
||||||
|
await apiClient.Post(
|
||||||
|
$"api/servers/{server.Id}/files/decompress",
|
||||||
|
new ServerFilesDecompressRequest()
|
||||||
|
{
|
||||||
|
Type = type,
|
||||||
|
Path = path,
|
||||||
|
Destination = destination
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#region Helpers
|
#region Helpers
|
||||||
|
|
||||||
private async Task<HttpApiClient> GetApiClient(Server server)
|
private async Task<HttpApiClient> GetApiClient(Server server)
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
|
using System.Text;
|
||||||
|
using ICSharpCode.SharpZipLib.GZip;
|
||||||
|
using ICSharpCode.SharpZipLib.Tar;
|
||||||
|
using ICSharpCode.SharpZipLib.Zip;
|
||||||
using Mono.Unix.Native;
|
using Mono.Unix.Native;
|
||||||
|
using MoonCore.Unix.Exceptions;
|
||||||
using MoonCore.Unix.SecureFs;
|
using MoonCore.Unix.SecureFs;
|
||||||
using MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Servers;
|
using MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Servers;
|
||||||
|
using MoonlightServers.DaemonShared.Enums;
|
||||||
|
|
||||||
namespace MoonlightServers.Daemon.Helpers;
|
namespace MoonlightServers.Daemon.Helpers;
|
||||||
|
|
||||||
@@ -36,49 +42,78 @@ public class ServerFileSystem
|
|||||||
{
|
{
|
||||||
var oldPath = Normalize(inputOldPath);
|
var oldPath = Normalize(inputOldPath);
|
||||||
var newPath = Normalize(inputNewPath);
|
var newPath = Normalize(inputNewPath);
|
||||||
|
|
||||||
FileSystem.Rename(oldPath, newPath);
|
FileSystem.Rename(oldPath, newPath);
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task Delete(string inputPath)
|
public Task Delete(string inputPath)
|
||||||
{
|
{
|
||||||
var path = Normalize(inputPath);
|
var path = Normalize(inputPath);
|
||||||
|
|
||||||
FileSystem.RemoveAll(path);
|
FileSystem.RemoveAll(path);
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task Mkdir(string inputPath)
|
public Task Mkdir(string inputPath)
|
||||||
{
|
{
|
||||||
var path = Normalize(inputPath);
|
var path = Normalize(inputPath);
|
||||||
|
|
||||||
FileSystem.MkdirAll(path, FilePermissions.ACCESSPERMS);
|
FileSystem.MkdirAll(path, FilePermissions.ACCESSPERMS);
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task CreateChunk(string inputPath, long totalSize, long positionToSkip, Stream chunkStream)
|
||||||
|
{
|
||||||
|
var path = Normalize(inputPath);
|
||||||
|
|
||||||
|
var parentDirectory = Path.GetDirectoryName(path);
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(parentDirectory) && parentDirectory != "/")
|
||||||
|
FileSystem.MkdirAll(parentDirectory, FilePermissions.ACCESSPERMS);
|
||||||
|
|
||||||
|
FileSystem.OpenFileWrite(path, fileStream =>
|
||||||
|
{
|
||||||
|
if (fileStream.Length != totalSize)
|
||||||
|
fileStream.SetLength(totalSize);
|
||||||
|
|
||||||
|
fileStream.Position = positionToSkip;
|
||||||
|
|
||||||
|
chunkStream.CopyTo(fileStream);
|
||||||
|
fileStream.Flush();
|
||||||
|
}, OpenFlags.O_CREAT | OpenFlags.O_RDWR); // We use these custom flags to ensure we aren't overwriting the file
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task Create(string inputPath, Stream dataStream)
|
public Task Create(string inputPath, Stream dataStream)
|
||||||
{
|
{
|
||||||
var path = Normalize(inputPath);
|
var path = Normalize(inputPath);
|
||||||
|
|
||||||
var parentDirectory = Path.GetDirectoryName(path);
|
var parentDirectory = Path.GetDirectoryName(path);
|
||||||
|
|
||||||
if(!string.IsNullOrEmpty(parentDirectory) && parentDirectory != "/")
|
if (!string.IsNullOrEmpty(parentDirectory) && parentDirectory != "/")
|
||||||
FileSystem.MkdirAll(parentDirectory, FilePermissions.ACCESSPERMS);
|
FileSystem.MkdirAll(parentDirectory, FilePermissions.ACCESSPERMS);
|
||||||
|
|
||||||
FileSystem.WriteFile(path, dataStream);
|
FileSystem.OpenFileWrite(path, stream =>
|
||||||
|
{
|
||||||
|
stream.Position = 0;
|
||||||
|
dataStream.CopyTo(stream);
|
||||||
|
|
||||||
|
stream.Flush();
|
||||||
|
});
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task Read(string inputPath, Func<Stream, Task> onHandle)
|
public Task Read(string inputPath, Func<Stream, Task> onHandle)
|
||||||
{
|
{
|
||||||
var path = Normalize(inputPath);
|
var path = Normalize(inputPath);
|
||||||
|
|
||||||
FileSystem.OpenFile(path, stream =>
|
FileSystem.OpenFileRead(path, stream =>
|
||||||
{
|
{
|
||||||
// No try catch here because the safe fs abstraction already handles every error occuring in the handle
|
// No try catch here because the safe fs abstraction already handles every error occuring in the handle
|
||||||
onHandle.Invoke(stream).Wait();
|
onHandle.Invoke(stream).Wait();
|
||||||
@@ -87,6 +122,201 @@ public class ServerFileSystem
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#region Compression
|
||||||
|
|
||||||
|
public Task Compress(string[] itemsInput, string destinationInput, CompressType type)
|
||||||
|
{
|
||||||
|
var destination = Normalize(destinationInput);
|
||||||
|
var items = itemsInput.Select(Normalize);
|
||||||
|
|
||||||
|
if (type == CompressType.Zip)
|
||||||
|
{
|
||||||
|
FileSystem.OpenFileWrite(destination, stream =>
|
||||||
|
{
|
||||||
|
using var zipStream = new ZipOutputStream(stream);
|
||||||
|
|
||||||
|
foreach (var item in items)
|
||||||
|
AddItemToZip(item, zipStream);
|
||||||
|
|
||||||
|
zipStream.Flush();
|
||||||
|
stream.Flush();
|
||||||
|
|
||||||
|
zipStream.Close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if (type == CompressType.TarGz)
|
||||||
|
{
|
||||||
|
FileSystem.OpenFileWrite(destination, stream =>
|
||||||
|
{
|
||||||
|
using var gzStream = new GZipOutputStream(stream);
|
||||||
|
using var tarStream = new TarOutputStream(gzStream, Encoding.UTF8);
|
||||||
|
|
||||||
|
foreach (var item in items)
|
||||||
|
AddItemToTar(item, tarStream);
|
||||||
|
|
||||||
|
tarStream.Flush();
|
||||||
|
gzStream.Flush();
|
||||||
|
stream.Flush();
|
||||||
|
|
||||||
|
tarStream.Close();
|
||||||
|
gzStream.Close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task Decompress(string pathInput, string destinationInput, CompressType type)
|
||||||
|
{
|
||||||
|
var path = Normalize(pathInput);
|
||||||
|
var destination = Normalize(destinationInput);
|
||||||
|
|
||||||
|
if (type == CompressType.Zip)
|
||||||
|
{
|
||||||
|
FileSystem.OpenFileRead(path, fileStream =>
|
||||||
|
{
|
||||||
|
var zipInputStream = new ZipInputStream(fileStream);
|
||||||
|
|
||||||
|
ExtractZip(zipInputStream, destination);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if (type == CompressType.TarGz)
|
||||||
|
{
|
||||||
|
FileSystem.OpenFileRead(path, fileStream =>
|
||||||
|
{
|
||||||
|
var gzInputStream = new GZipInputStream(fileStream);
|
||||||
|
var zipInputStream = new TarInputStream(gzInputStream, Encoding.UTF8);
|
||||||
|
|
||||||
|
ExtractTar(zipInputStream, destination);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddItemToZip(string path, ZipOutputStream outputStream)
|
||||||
|
{
|
||||||
|
var item = FileSystem.Stat(path);
|
||||||
|
|
||||||
|
if (item.IsDirectory)
|
||||||
|
{
|
||||||
|
var contents = FileSystem.ReadDir(path);
|
||||||
|
|
||||||
|
foreach (var content in contents)
|
||||||
|
{
|
||||||
|
AddItemToZip(
|
||||||
|
Path.Combine(path, content.Name),
|
||||||
|
outputStream
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var entry = new ZipEntry(path)
|
||||||
|
{
|
||||||
|
Size = item.Size,
|
||||||
|
DateTime = item.LastChanged
|
||||||
|
};
|
||||||
|
|
||||||
|
outputStream.PutNextEntry(entry);
|
||||||
|
|
||||||
|
FileSystem.OpenFileRead(path, stream => { stream.CopyTo(outputStream); });
|
||||||
|
|
||||||
|
outputStream.CloseEntry();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddItemToTar(string path, TarOutputStream outputStream)
|
||||||
|
{
|
||||||
|
var item = FileSystem.Stat(path);
|
||||||
|
|
||||||
|
if (item.IsDirectory)
|
||||||
|
{
|
||||||
|
var contents = FileSystem.ReadDir(path);
|
||||||
|
|
||||||
|
foreach (var content in contents)
|
||||||
|
{
|
||||||
|
AddItemToTar(
|
||||||
|
Path.Combine(path, content.Name),
|
||||||
|
outputStream
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var entry = TarEntry.CreateTarEntry(path);
|
||||||
|
|
||||||
|
entry.Name = path;
|
||||||
|
entry.Size = item.Size;
|
||||||
|
entry.ModTime = item.LastChanged;
|
||||||
|
|
||||||
|
outputStream.PutNextEntry(entry);
|
||||||
|
|
||||||
|
FileSystem.OpenFileRead(path, stream =>
|
||||||
|
{
|
||||||
|
stream.CopyTo(outputStream);
|
||||||
|
});
|
||||||
|
|
||||||
|
outputStream.CloseEntry();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ExtractZip(ZipInputStream inputStream, string destination)
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var entry = inputStream.GetNextEntry();
|
||||||
|
|
||||||
|
if(entry == null || entry.IsDirectory)
|
||||||
|
break;
|
||||||
|
|
||||||
|
var fileDestination = Path.Combine(destination, entry.Name);
|
||||||
|
|
||||||
|
var parentDirectory = Path.GetDirectoryName(fileDestination);
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(parentDirectory) && parentDirectory != "/")
|
||||||
|
FileSystem.MkdirAll(parentDirectory, FilePermissions.ACCESSPERMS);
|
||||||
|
|
||||||
|
FileSystem.OpenFileWrite(fileDestination, stream =>
|
||||||
|
{
|
||||||
|
stream.Position = 0;
|
||||||
|
|
||||||
|
inputStream.CopyTo(stream);
|
||||||
|
|
||||||
|
stream.Flush();
|
||||||
|
}); // This will override the file if it exists
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ExtractTar(TarInputStream inputStream, string destination)
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var entry = inputStream.GetNextEntry();
|
||||||
|
|
||||||
|
if(entry == null || entry.IsDirectory)
|
||||||
|
break;
|
||||||
|
|
||||||
|
var fileDestination = Path.Combine(destination, entry.Name);
|
||||||
|
|
||||||
|
var parentDirectory = Path.GetDirectoryName(fileDestination);
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(parentDirectory) && parentDirectory != "/")
|
||||||
|
FileSystem.MkdirAll(parentDirectory, FilePermissions.ACCESSPERMS);
|
||||||
|
|
||||||
|
FileSystem.OpenFileWrite(fileDestination, stream =>
|
||||||
|
{
|
||||||
|
stream.Position = 0;
|
||||||
|
|
||||||
|
inputStream.CopyTo(stream);
|
||||||
|
|
||||||
|
stream.Flush();
|
||||||
|
}); // This will override the file if it exists
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
private string Normalize(string path)
|
private string Normalize(string path)
|
||||||
{
|
{
|
||||||
return path
|
return path
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Authorization;
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using MoonCore.Exceptions;
|
using MoonCore.Exceptions;
|
||||||
using MoonlightServers.Daemon.Services;
|
using MoonlightServers.Daemon.Services;
|
||||||
|
using MoonlightServers.DaemonShared.DaemonSide.Http.Requests;
|
||||||
using MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Servers;
|
using MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Servers;
|
||||||
|
|
||||||
namespace MoonlightServers.Daemon.Http.Controllers.Servers;
|
namespace MoonlightServers.Daemon.Http.Controllers.Servers;
|
||||||
@@ -61,4 +62,34 @@ public class ServerFileSystemController : Controller
|
|||||||
|
|
||||||
await server.FileSystem.Mkdir(path);
|
await server.FileSystem.Mkdir(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPost("{id:int}/files/compress")]
|
||||||
|
public async Task Compress([FromRoute] int id, [FromBody] ServerFilesCompressRequest request)
|
||||||
|
{
|
||||||
|
var server = ServerService.GetServer(id);
|
||||||
|
|
||||||
|
if (server == null)
|
||||||
|
throw new HttpApiException("No server with this id found", 404);
|
||||||
|
|
||||||
|
await server.FileSystem.Compress(
|
||||||
|
request.Items,
|
||||||
|
request.Destination,
|
||||||
|
request.Type
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("{id:int}/files/decompress")]
|
||||||
|
public async Task Decompress([FromRoute] int id, [FromBody] ServerFilesDecompressRequest request)
|
||||||
|
{
|
||||||
|
var server = ServerService.GetServer(id);
|
||||||
|
|
||||||
|
if (server == null)
|
||||||
|
throw new HttpApiException("No server with this id found", 404);
|
||||||
|
|
||||||
|
await server.FileSystem.Decompress(
|
||||||
|
request.Path,
|
||||||
|
request.Destination,
|
||||||
|
request.Type
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -17,6 +17,8 @@ public class UploadController : Controller
|
|||||||
private readonly AppConfiguration Configuration;
|
private readonly AppConfiguration Configuration;
|
||||||
private readonly ServerService ServerService;
|
private readonly ServerService ServerService;
|
||||||
|
|
||||||
|
private readonly long ChunkSize = ByteConverter.FromMegaBytes(20).Bytes; // TODO config
|
||||||
|
|
||||||
public UploadController(
|
public UploadController(
|
||||||
AccessTokenHelper accessTokenHelper,
|
AccessTokenHelper accessTokenHelper,
|
||||||
ServerService serverService,
|
ServerService serverService,
|
||||||
@@ -29,15 +31,24 @@ public class UploadController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
public async Task Upload([FromQuery] string token)
|
public async Task Upload(
|
||||||
|
[FromQuery] string token,
|
||||||
|
[FromQuery] long totalSize, // TODO: Add limit in config
|
||||||
|
[FromQuery] int chunkId,
|
||||||
|
[FromQuery] string path
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var file = Request.Form.Files.FirstOrDefault();
|
#region File validation
|
||||||
|
|
||||||
if (file == null)
|
if (Request.Form.Files.Count != 1)
|
||||||
throw new HttpApiException("No file provided", 400);
|
throw new HttpApiException("You need to provide exactly one file", 400);
|
||||||
|
|
||||||
|
var file = Request.Form.Files[0];
|
||||||
|
|
||||||
if(file.Length > ByteConverter.FromMegaBytes(Configuration.Files.UploadLimit).Bytes)
|
if (file.Length > ChunkSize)
|
||||||
throw new HttpApiException("The provided file is bigger than the upload limit", 400);
|
throw new HttpApiException("The provided data exceeds the chunk size limit", 400);
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
#region Token validation
|
#region Token validation
|
||||||
|
|
||||||
@@ -56,14 +67,30 @@ public class UploadController : Controller
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region Chunk calculation and validation
|
||||||
|
|
||||||
|
var chunks = totalSize / ChunkSize;
|
||||||
|
chunks += totalSize % ChunkSize > 0 ? 1 : 0;
|
||||||
|
|
||||||
|
if (chunkId > chunks)
|
||||||
|
throw new HttpApiException("Invalid chunk id: Out of bounds", 400);
|
||||||
|
|
||||||
|
var positionToSkipTo = ChunkSize * chunkId;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
var server = ServerService.GetServer(serverId);
|
var server = ServerService.GetServer(serverId);
|
||||||
|
|
||||||
if (server == null)
|
if (server == null)
|
||||||
throw new HttpApiException("No server with this id found", 404);
|
throw new HttpApiException("No server with this id found", 404);
|
||||||
|
|
||||||
var dataStream = file.OpenReadStream();
|
var dataStream = file.OpenReadStream();
|
||||||
var path = file.FileName;
|
|
||||||
|
|
||||||
await server.FileSystem.Create(path, dataStream);
|
await server.FileSystem.CreateChunk(
|
||||||
|
path,
|
||||||
|
totalSize,
|
||||||
|
positionToSkipTo,
|
||||||
|
dataStream
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -9,9 +9,10 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Docker.DotNet" Version="3.125.15" />
|
<PackageReference Include="Docker.DotNet" Version="3.125.15" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.1.0" />
|
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.1.0" />
|
||||||
<PackageReference Include="MoonCore" Version="1.8.4" />
|
<PackageReference Include="MoonCore" Version="1.8.5" />
|
||||||
<PackageReference Include="MoonCore.Extended" Version="1.3.0" />
|
<PackageReference Include="MoonCore.Extended" Version="1.3.2" />
|
||||||
<PackageReference Include="MoonCore.Unix" Version="1.0.3" />
|
<PackageReference Include="MoonCore.Unix" Version="1.0.6" />
|
||||||
|
<PackageReference Include="SharpZipLib" Version="1.4.2" />
|
||||||
<PackageReference Include="Stateless" Version="5.17.0" />
|
<PackageReference Include="Stateless" Version="5.17.0" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2"/>
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
using MoonlightServers.DaemonShared.Enums;
|
||||||
|
|
||||||
|
namespace MoonlightServers.DaemonShared.DaemonSide.Http.Requests;
|
||||||
|
|
||||||
|
public class ServerFilesCompressRequest
|
||||||
|
{
|
||||||
|
public string Destination { get; set; }
|
||||||
|
public string[] Items { get; set; }
|
||||||
|
public CompressType Type { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
using MoonlightServers.DaemonShared.Enums;
|
||||||
|
|
||||||
|
namespace MoonlightServers.DaemonShared.DaemonSide.Http.Requests;
|
||||||
|
|
||||||
|
public class ServerFilesDecompressRequest
|
||||||
|
{
|
||||||
|
public string Destination { get; set; }
|
||||||
|
public string Path { get; set; }
|
||||||
|
public CompressType Type { get; set; }
|
||||||
|
}
|
||||||
7
MoonlightServers.DaemonShared/Enums/CompressType.cs
Normal file
7
MoonlightServers.DaemonShared/Enums/CompressType.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace MoonlightServers.DaemonShared.Enums;
|
||||||
|
|
||||||
|
public enum CompressType
|
||||||
|
{
|
||||||
|
Zip = 0,
|
||||||
|
TarGz = 1
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Folder Include="DaemonSide\" />
|
<Folder Include="DaemonSide\" />
|
||||||
|
<Folder Include="DaemonSide\Http\Responses\Servers\Files\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,20 +1,41 @@
|
|||||||
using MoonCore.Blazor.Tailwind.Fm;
|
using MoonCore.Blazor.Tailwind.Fm;
|
||||||
|
using MoonCore.Blazor.Tailwind.Fm.Models;
|
||||||
|
using MoonCore.Blazor.Tailwind.Services;
|
||||||
|
using MoonCore.Helpers;
|
||||||
using MoonlightServers.Frontend.Services;
|
using MoonlightServers.Frontend.Services;
|
||||||
|
|
||||||
namespace MoonlightServers.Frontend.Helpers;
|
namespace MoonlightServers.Frontend.Helpers;
|
||||||
|
|
||||||
public class ServerFileSystemProvider : IFileSystemProvider
|
public class ServerFileSystemProvider : IFileSystemProvider, ICompressFileSystemProvider
|
||||||
{
|
{
|
||||||
private readonly int ServerId;
|
private readonly DownloadService DownloadService;
|
||||||
private readonly ServerFileSystemService FileSystemService;
|
private readonly ServerFileSystemService FileSystemService;
|
||||||
|
|
||||||
|
public CompressType[] CompressTypes { get; } =
|
||||||
|
[
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Extension = "zip",
|
||||||
|
DisplayName = "ZIP Archive"
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Extension = "tar.gz",
|
||||||
|
DisplayName = "GZ Compressed Tar Archive"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
private readonly int ServerId;
|
||||||
|
|
||||||
public ServerFileSystemProvider(
|
public ServerFileSystemProvider(
|
||||||
int serverId,
|
int serverId,
|
||||||
ServerFileSystemService fileSystemService
|
ServerFileSystemService fileSystemService,
|
||||||
|
DownloadService downloadService
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
ServerId = serverId;
|
ServerId = serverId;
|
||||||
FileSystemService = fileSystemService;
|
FileSystemService = fileSystemService;
|
||||||
|
DownloadService = downloadService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<FileSystemEntry[]> List(string path)
|
public async Task<FileSystemEntry[]> List(string path)
|
||||||
@@ -35,7 +56,7 @@ public class ServerFileSystemProvider : IFileSystemProvider
|
|||||||
|
|
||||||
public async Task Create(string path, Stream stream)
|
public async Task Create(string path, Stream stream)
|
||||||
{
|
{
|
||||||
await FileSystemService.Upload(ServerId, path, stream);
|
await Upload(_ => Task.CompletedTask, path, stream);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Move(string oldPath, string newPath)
|
public async Task Move(string oldPath, string newPath)
|
||||||
@@ -55,6 +76,72 @@ public class ServerFileSystemProvider : IFileSystemProvider
|
|||||||
|
|
||||||
public async Task<Stream> Read(string path)
|
public async Task<Stream> Read(string path)
|
||||||
{
|
{
|
||||||
return await FileSystemService.Download(ServerId, path);
|
var downloadSession = await FileSystemService.Download(ServerId, path);
|
||||||
|
|
||||||
|
using var httpClient = new HttpClient();
|
||||||
|
return await httpClient.GetStreamAsync(downloadSession.DownloadUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Download(Func<int, Task> updateProgress, string path, string fileName)
|
||||||
|
{
|
||||||
|
var downloadSession = await FileSystemService.Download(ServerId, path);
|
||||||
|
|
||||||
|
await DownloadService.DownloadUrl(fileName, downloadSession.DownloadUrl,
|
||||||
|
async (loaded, total) =>
|
||||||
|
{
|
||||||
|
var percent = total == 0 ? 0 : (int)Math.Round((float)loaded / total * 100);
|
||||||
|
await updateProgress.Invoke(percent);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Upload(Func<int, Task> updateProgress, string path, Stream stream)
|
||||||
|
{
|
||||||
|
using var httpClient = new HttpClient();
|
||||||
|
|
||||||
|
var uploadSession = await FileSystemService.Upload(ServerId);
|
||||||
|
|
||||||
|
var size = stream.Length;
|
||||||
|
var chunkSize = ByteConverter.FromMegaBytes(20).Bytes;
|
||||||
|
|
||||||
|
var chunks = size / chunkSize;
|
||||||
|
chunks += size % chunkSize > 0 ? 1 : 0;
|
||||||
|
|
||||||
|
for (var chunkId = 0; chunkId < chunks; chunkId++)
|
||||||
|
{
|
||||||
|
var percent = (int)Math.Round((chunkId + 1f) / chunks * 100);
|
||||||
|
await updateProgress.Invoke(percent);
|
||||||
|
|
||||||
|
var buffer = new byte[chunkSize];
|
||||||
|
var bytesRead = await stream.ReadAsync(buffer);
|
||||||
|
|
||||||
|
var uploadForm = new MultipartFormDataContent();
|
||||||
|
uploadForm.Add(new ByteArrayContent(buffer, 0, bytesRead), "file", "file");
|
||||||
|
|
||||||
|
await httpClient.PostAsync(
|
||||||
|
$"{uploadSession.UploadUrl}&totalSize={size}&chunkId={chunkId}&path={path}",
|
||||||
|
uploadForm
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Compress(CompressType type, string path, string[] itemsToCompress)
|
||||||
|
{
|
||||||
|
await FileSystemService.Compress(
|
||||||
|
ServerId,
|
||||||
|
type.Extension.Replace(".", ""),
|
||||||
|
itemsToCompress,
|
||||||
|
path
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Decompress(CompressType type, string path, string destination)
|
||||||
|
{
|
||||||
|
await FileSystemService.Decompress(
|
||||||
|
ServerId,
|
||||||
|
type.Extension.Replace(".", ""),
|
||||||
|
path,
|
||||||
|
destination
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
|
using System.Net;
|
||||||
using MoonCore.Attributes;
|
using MoonCore.Attributes;
|
||||||
using MoonCore.Helpers;
|
using MoonCore.Helpers;
|
||||||
|
using MoonlightServers.Shared.Http.Requests.Client.Servers.Files;
|
||||||
using MoonlightServers.Shared.Http.Responses.Client.Servers.Files;
|
using MoonlightServers.Shared.Http.Responses.Client.Servers.Files;
|
||||||
|
|
||||||
namespace MoonlightServers.Frontend.Services;
|
namespace MoonlightServers.Frontend.Services;
|
||||||
@@ -42,28 +44,43 @@ public class ServerFileSystemService
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Upload(int serverId, string path, Stream dataStream)
|
public async Task<ServerFilesUploadResponse> Upload(int serverId)
|
||||||
{
|
{
|
||||||
var uploadSession = await ApiClient.GetJson<ServerFilesUploadResponse>(
|
return await ApiClient.GetJson<ServerFilesUploadResponse>(
|
||||||
$"api/client/servers/{serverId}/files/upload"
|
$"api/client/servers/{serverId}/files/upload"
|
||||||
);
|
);
|
||||||
|
|
||||||
using var httpClient = new HttpClient();
|
|
||||||
|
|
||||||
var content = new MultipartFormDataContent();
|
|
||||||
content.Add(new StreamContent(dataStream), "file", path);
|
|
||||||
|
|
||||||
await httpClient.PostAsync(uploadSession.UploadUrl, content);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Stream> Download(int serverId, string path)
|
public async Task<ServerFilesDownloadResponse> Download(int serverId, string path)
|
||||||
{
|
{
|
||||||
var downloadSession = await ApiClient.GetJson<ServerFilesDownloadResponse>(
|
return await ApiClient.GetJson<ServerFilesDownloadResponse>(
|
||||||
$"api/client/servers/{serverId}/files/download?path={path}"
|
$"api/client/servers/{serverId}/files/download?path={path}"
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
using var httpClient = new HttpClient();
|
public async Task Compress(int serverId, string type, string[] items, string destination)
|
||||||
|
{
|
||||||
return await httpClient.GetStreamAsync(downloadSession.DownloadUrl);
|
await ApiClient.Post(
|
||||||
|
$"api/client/servers/{serverId}/files/compress",
|
||||||
|
new ServerFilesCompressRequest()
|
||||||
|
{
|
||||||
|
Type = type,
|
||||||
|
Items = items,
|
||||||
|
Destination = destination
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Decompress(int serverId, string type, string path, string destination)
|
||||||
|
{
|
||||||
|
await ApiClient.Post(
|
||||||
|
$"api/client/servers/{serverId}/files/decompress",
|
||||||
|
new ServerFilesDecompressRequest()
|
||||||
|
{
|
||||||
|
Type = type,
|
||||||
|
Path = path,
|
||||||
|
Destination = destination
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,19 +1,25 @@
|
|||||||
@using MoonlightServers.Frontend.Services
|
@using MoonlightServers.Frontend.Services
|
||||||
@using MoonCore.Blazor.Tailwind.Fm
|
@using MoonCore.Blazor.Tailwind.Fm
|
||||||
|
@using MoonCore.Blazor.Tailwind.Services
|
||||||
@using MoonlightServers.Frontend.Helpers
|
@using MoonlightServers.Frontend.Helpers
|
||||||
|
|
||||||
@inherits BaseServerTab
|
@inherits BaseServerTab
|
||||||
|
|
||||||
@inject ServerFileSystemService FileSystemService
|
@inject ServerFileSystemService FileSystemService
|
||||||
|
@inject DownloadService DownloadService
|
||||||
|
|
||||||
<FileManager FileSystemProvider="Provider" />
|
<FileManager FileSystemProvider="Provider"/>
|
||||||
|
|
||||||
@code
|
@code
|
||||||
{
|
{
|
||||||
private IFileSystemProvider Provider;
|
private IFileSystemProvider Provider;
|
||||||
|
|
||||||
protected override void OnInitialized()
|
protected override void OnInitialized()
|
||||||
{
|
{
|
||||||
Provider = new ServerFileSystemProvider(Server.Id, FileSystemService);
|
Provider = new ServerFileSystemProvider(
|
||||||
|
Server.Id,
|
||||||
|
FileSystemService,
|
||||||
|
DownloadService
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,8 +26,10 @@
|
|||||||
</PageHeader>
|
</PageHeader>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DataTable @ref="Table" TItem="ServerDetailResponse" PageSize="15" LoadItemsPaginatedAsync="LoadData">
|
<DataTable @ref="Table" TItem="ServerDetailResponse">
|
||||||
<Configuration>
|
<Configuration>
|
||||||
|
<Pagination TItem="ServerDetailResponse" ItemSource="LoadData" />
|
||||||
|
|
||||||
<DataTableColumn TItem="ServerDetailResponse" Field="@(x => x.Id)" Name="Id"/>
|
<DataTableColumn TItem="ServerDetailResponse" Field="@(x => x.Id)" Name="Id"/>
|
||||||
<DataTableColumn TItem="ServerDetailResponse" Field="@(x => x.Name)" Name="Name"/>
|
<DataTableColumn TItem="ServerDetailResponse" Field="@(x => x.Name)" Name="Name"/>
|
||||||
<DataTableColumn TItem="ServerDetailResponse" Field="@(x => x.NodeId)" Name="Node">
|
<DataTableColumn TItem="ServerDetailResponse" Field="@(x => x.NodeId)" Name="Node">
|
||||||
|
|||||||
@@ -27,8 +27,10 @@
|
|||||||
</PageHeader>
|
</PageHeader>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DataTable TItem="NodeDetailResponse" PageSize="15" LoadItemsPaginatedAsync="LoadData">
|
<DataTable TItem="NodeDetailResponse">
|
||||||
<Configuration>
|
<Configuration>
|
||||||
|
<Pagination TItem="NodeDetailResponse" ItemSource="LoadData" />
|
||||||
|
|
||||||
<DataTableColumn TItem="NodeDetailResponse" Field="@(x => x.Id)" Name="Id"/>
|
<DataTableColumn TItem="NodeDetailResponse" Field="@(x => x.Id)" Name="Id"/>
|
||||||
<DataTableColumn TItem="NodeDetailResponse" Field="@(x => x.Name)" Name="Name">
|
<DataTableColumn TItem="NodeDetailResponse" Field="@(x => x.Name)" Name="Name">
|
||||||
<ColumnTemplate>
|
<ColumnTemplate>
|
||||||
@@ -103,8 +105,8 @@
|
|||||||
</ColumnTemplate>
|
</ColumnTemplate>
|
||||||
</DataTableColumn>
|
</DataTableColumn>
|
||||||
<DataTableColumn TItem="NodeDetailResponse"
|
<DataTableColumn TItem="NodeDetailResponse"
|
||||||
HeaderCss="p-2 font-semibold text-left hidden xl:table-cell"
|
HeaderCss="p-2 font-semibold text-left hidden xl:table-cell"
|
||||||
ColumnCss="p-2 text-left font-normal hidden xl:table-cell">
|
ColumnCss="p-2 text-left font-normal hidden xl:table-cell">
|
||||||
<ColumnTemplate>
|
<ColumnTemplate>
|
||||||
<div>
|
<div>
|
||||||
<i class="icon-memory-stick text-lg me-1 align-middle text-primary-500"></i>
|
<i class="icon-memory-stick text-lg me-1 align-middle text-primary-500"></i>
|
||||||
|
|||||||
@@ -33,9 +33,11 @@
|
|||||||
</PageHeader>
|
</PageHeader>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DataTable @ref="Table" TItem="StarDetailResponse" LoadItemsPaginatedAsync="LoadData">
|
<DataTable @ref="Table" TItem="StarDetailResponse">
|
||||||
<Configuration>
|
<Configuration>
|
||||||
<DataTableColumn TItem="StarDetailResponse" Field="@(x => x.Id)" Name="Id" />
|
<Pagination TItem="StarDetailResponse" ItemSource="LoadData" />
|
||||||
|
|
||||||
|
<DataTableColumn TItem="StarDetailResponse" Field="@(x => x.Id)" Name="Id"/>
|
||||||
<DataTableColumn TItem="StarDetailResponse" Field="@(x => x.Name)" Name="Name">
|
<DataTableColumn TItem="StarDetailResponse" Field="@(x => x.Name)" Name="Name">
|
||||||
<ColumnTemplate>
|
<ColumnTemplate>
|
||||||
<a class="text-primary-500" href="/admin/servers/stars/update/@(context.Id)">
|
<a class="text-primary-500" href="/admin/servers/stars/update/@(context.Id)">
|
||||||
@@ -43,8 +45,8 @@
|
|||||||
</a>
|
</a>
|
||||||
</ColumnTemplate>
|
</ColumnTemplate>
|
||||||
</DataTableColumn>
|
</DataTableColumn>
|
||||||
<DataTableColumn TItem="StarDetailResponse" Field="@(x => x.Version)" Name="Version" />
|
<DataTableColumn TItem="StarDetailResponse" Field="@(x => x.Version)" Name="Version"/>
|
||||||
<DataTableColumn TItem="StarDetailResponse" Field="@(x => x.Author)" Name="Author" />
|
<DataTableColumn TItem="StarDetailResponse" Field="@(x => x.Author)" Name="Author"/>
|
||||||
<DataTableColumn TItem="StarDetailResponse">
|
<DataTableColumn TItem="StarDetailResponse">
|
||||||
<ColumnTemplate>
|
<ColumnTemplate>
|
||||||
<div class="flex justify-end">
|
<div class="flex justify-end">
|
||||||
@@ -68,7 +70,7 @@
|
|||||||
<i class="icon-download align-middle"></i>
|
<i class="icon-download align-middle"></i>
|
||||||
<span class="align-middle">Export</span>
|
<span class="align-middle">Export</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a href="/admin/servers/stars/update/@(context.Id)" class="text-primary-500 mr-2 sm:mr-3">
|
<a href="/admin/servers/stars/update/@(context.Id)" class="text-primary-500 mr-2 sm:mr-3">
|
||||||
<i class="icon-pencil text-base"></i>
|
<i class="icon-pencil text-base"></i>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace MoonlightServers.Shared.Http.Requests.Client.Servers.Files;
|
||||||
|
|
||||||
|
public class ServerFilesCompressRequest
|
||||||
|
{
|
||||||
|
[Required(ErrorMessage = "You need to specify a type")]
|
||||||
|
public string Type { get; set; }
|
||||||
|
|
||||||
|
public string[] Items { get; set; } = [];
|
||||||
|
|
||||||
|
[Required(ErrorMessage = "You need to specify a destination")]
|
||||||
|
public string Destination { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace MoonlightServers.Shared.Http.Requests.Client.Servers.Files;
|
||||||
|
|
||||||
|
public class ServerFilesDecompressRequest
|
||||||
|
{
|
||||||
|
[Required(ErrorMessage = "You need to specify a type")]
|
||||||
|
public string Type { get; set; }
|
||||||
|
|
||||||
|
[Required(ErrorMessage = "You need to specify a path")]
|
||||||
|
public string Path { get; set; }
|
||||||
|
|
||||||
|
[Required(ErrorMessage = "You need to specify a destination")]
|
||||||
|
public string Destination { get; set; }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user