Implemented system files tab
This commit is contained in:
@@ -0,0 +1,413 @@
|
||||
using System.Text;
|
||||
using ICSharpCode.SharpZipLib.GZip;
|
||||
using ICSharpCode.SharpZipLib.Tar;
|
||||
using ICSharpCode.SharpZipLib.Zip;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using MoonCore.Extended.PermFilter;
|
||||
using MoonCore.Helpers;
|
||||
using Moonlight.Shared.Http.Requests.Admin.Sys.Files;
|
||||
using Moonlight.Shared.Http.Responses.Admin.Sys.Files;
|
||||
|
||||
namespace Moonlight.ApiServer.Http.Controllers.Admin.Sys;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/admin/system/files")]
|
||||
[RequirePermission("admin.system.files")]
|
||||
public class FilesController : Controller
|
||||
{
|
||||
private readonly string BaseDirectory = PathBuilder.Dir("storage");
|
||||
|
||||
[HttpGet("list")]
|
||||
public Task<FileSystemEntryResponse[]> List([FromQuery] string path)
|
||||
{
|
||||
var safePath = SanitizePath(path);
|
||||
var physicalPath = PathBuilder.Dir(BaseDirectory, safePath);
|
||||
|
||||
var entries = new List<FileSystemEntryResponse>();
|
||||
|
||||
var files = Directory.GetFiles(physicalPath);
|
||||
|
||||
foreach (var file in files)
|
||||
{
|
||||
var fi = new FileInfo(file);
|
||||
|
||||
entries.Add(new FileSystemEntryResponse()
|
||||
{
|
||||
Name = fi.Name,
|
||||
Size = fi.Length,
|
||||
CreatedAt = fi.CreationTimeUtc,
|
||||
IsFile = true,
|
||||
UpdatedAt = fi.LastWriteTimeUtc
|
||||
});
|
||||
}
|
||||
|
||||
var directories = Directory.GetDirectories(physicalPath);
|
||||
|
||||
foreach (var directory in directories)
|
||||
{
|
||||
var di = new DirectoryInfo(directory);
|
||||
|
||||
entries.Add(new FileSystemEntryResponse()
|
||||
{
|
||||
Name = di.Name,
|
||||
Size = 0,
|
||||
CreatedAt = di.CreationTimeUtc,
|
||||
UpdatedAt = di.LastWriteTimeUtc,
|
||||
IsFile = false
|
||||
});
|
||||
}
|
||||
|
||||
return Task.FromResult(
|
||||
entries.ToArray()
|
||||
);
|
||||
}
|
||||
|
||||
[HttpPost("create")]
|
||||
public async Task Create([FromQuery] string path)
|
||||
{
|
||||
var stream = Request.Body;
|
||||
|
||||
var safePath = SanitizePath(path);
|
||||
var physicalPath = PathBuilder.File(BaseDirectory, safePath);
|
||||
var baseDir = Path.GetDirectoryName(physicalPath);
|
||||
|
||||
if (!string.IsNullOrEmpty(baseDir))
|
||||
Directory.CreateDirectory(baseDir);
|
||||
|
||||
await using var fs = System.IO.File.Create(
|
||||
physicalPath
|
||||
);
|
||||
|
||||
await stream.CopyToAsync(fs);
|
||||
|
||||
await fs.FlushAsync();
|
||||
fs.Close();
|
||||
stream.Close();
|
||||
}
|
||||
|
||||
[HttpPost("move")]
|
||||
public Task Move([FromQuery] string oldPath, [FromQuery] string newPath)
|
||||
{
|
||||
var oldSafePath = SanitizePath(oldPath);
|
||||
var newSafePath = SanitizePath(newPath);
|
||||
|
||||
var oldPhysicalDirPath = PathBuilder.Dir(BaseDirectory, oldSafePath);
|
||||
|
||||
if (Directory.Exists(oldPhysicalDirPath))
|
||||
{
|
||||
var newPhysicalDirPath = PathBuilder.Dir(BaseDirectory, newSafePath);
|
||||
|
||||
Directory.Move(
|
||||
oldPhysicalDirPath,
|
||||
newPhysicalDirPath
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
var oldPhysicalFilePath = PathBuilder.File(BaseDirectory, oldSafePath);
|
||||
var newPhysicalFilePath = PathBuilder.File(BaseDirectory, newSafePath);
|
||||
|
||||
System.IO.File.Move(
|
||||
oldPhysicalFilePath,
|
||||
newPhysicalFilePath
|
||||
);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
[HttpDelete("delete")]
|
||||
public Task Delete([FromQuery] string path)
|
||||
{
|
||||
var safePath = SanitizePath(path);
|
||||
var physicalDirPath = PathBuilder.Dir(BaseDirectory, safePath);
|
||||
|
||||
if (Directory.Exists(physicalDirPath))
|
||||
Directory.Delete(physicalDirPath, true);
|
||||
else
|
||||
{
|
||||
var physicalFilePath = PathBuilder.File(BaseDirectory, safePath);
|
||||
|
||||
System.IO.File.Delete(physicalFilePath);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
[HttpPost("mkdir")]
|
||||
public Task CreateDirectory([FromQuery] string path)
|
||||
{
|
||||
var safePath = SanitizePath(path);
|
||||
var physicalPath = PathBuilder.Dir(BaseDirectory, safePath);
|
||||
|
||||
Directory.CreateDirectory(physicalPath);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
[HttpGet("read")]
|
||||
public async Task Read([FromQuery] string path)
|
||||
{
|
||||
var safePath = SanitizePath(path);
|
||||
var physicalPath = PathBuilder.File(BaseDirectory, safePath);
|
||||
|
||||
await using var fs = System.IO.File.Open(physicalPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
||||
await fs.CopyToAsync(Response.Body);
|
||||
|
||||
fs.Close();
|
||||
}
|
||||
|
||||
[HttpPost("compress")]
|
||||
public async Task Compress([FromBody] CompressRequest request)
|
||||
{
|
||||
if (request.Type == "tar.gz")
|
||||
await CompressTarGz(request.Path, request.ItemsToCompress);
|
||||
else if (request.Type == "zip")
|
||||
await CompressZip(request.Path, request.ItemsToCompress);
|
||||
}
|
||||
|
||||
#region Tar Gz
|
||||
|
||||
private async Task CompressTarGz(string path, string[] itemsToCompress)
|
||||
{
|
||||
var safePath = SanitizePath(path);
|
||||
var destination = PathBuilder.File(BaseDirectory, safePath);
|
||||
|
||||
await using var outStream = System.IO.File.Create(destination);
|
||||
await using var gzoStream = new GZipOutputStream(outStream);
|
||||
await using var tarStream = new TarOutputStream(gzoStream, Encoding.UTF8);
|
||||
|
||||
foreach (var itemName in itemsToCompress)
|
||||
{
|
||||
var safeFilePath = SanitizePath(itemName);
|
||||
var filePath = PathBuilder.File(BaseDirectory, safeFilePath);
|
||||
|
||||
var fi = new FileInfo(filePath);
|
||||
|
||||
if (fi.Exists)
|
||||
await AddFileToTarGz(tarStream, filePath);
|
||||
else
|
||||
{
|
||||
var safeDirePath = SanitizePath(itemName);
|
||||
var dirPath = PathBuilder.Dir(BaseDirectory, safeDirePath);
|
||||
|
||||
await AddDirectoryToTarGz(tarStream, dirPath);
|
||||
}
|
||||
}
|
||||
|
||||
await tarStream.FlushAsync();
|
||||
await gzoStream.FlushAsync();
|
||||
await outStream.FlushAsync();
|
||||
|
||||
tarStream.Close();
|
||||
gzoStream.Close();
|
||||
outStream.Close();
|
||||
}
|
||||
|
||||
private async Task AddDirectoryToTarGz(TarOutputStream tarOutputStream, string root)
|
||||
{
|
||||
foreach (var file in Directory.GetFiles(root))
|
||||
await AddFileToTarGz(tarOutputStream, file);
|
||||
|
||||
foreach (var directory in Directory.GetDirectories(root))
|
||||
await AddDirectoryToTarGz(tarOutputStream, directory);
|
||||
}
|
||||
|
||||
private async Task AddFileToTarGz(TarOutputStream tarOutputStream, string file)
|
||||
{
|
||||
// Open file stream
|
||||
var fs = System.IO.File.Open(file, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
||||
|
||||
// Meta
|
||||
var entry = TarEntry.CreateTarEntry(file);
|
||||
|
||||
// Fix path
|
||||
entry.Name = Formatter
|
||||
.ReplaceStart(entry.Name, BaseDirectory, "")
|
||||
.TrimStart('/');
|
||||
|
||||
entry.Size = fs.Length;
|
||||
|
||||
// Write entry
|
||||
await tarOutputStream.PutNextEntryAsync(entry, CancellationToken.None);
|
||||
|
||||
// Copy file content to tar stream
|
||||
await fs.CopyToAsync(tarOutputStream);
|
||||
fs.Close();
|
||||
|
||||
// Close the entry
|
||||
tarOutputStream.CloseEntry();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ZIP
|
||||
|
||||
private async Task CompressZip(string path, string[] itemsToCompress)
|
||||
{
|
||||
var safePath = SanitizePath(path);
|
||||
var destination = PathBuilder.File(BaseDirectory, safePath);
|
||||
|
||||
await using var outStream = System.IO.File.Create(destination);
|
||||
await using var zipOutputStream = new ZipOutputStream(outStream);
|
||||
|
||||
foreach (var itemName in itemsToCompress)
|
||||
{
|
||||
var safeFilePath = SanitizePath(itemName);
|
||||
var filePath = PathBuilder.File(BaseDirectory, safeFilePath);
|
||||
|
||||
var fi = new FileInfo(filePath);
|
||||
|
||||
if (fi.Exists)
|
||||
await AddFileToZip(zipOutputStream, filePath);
|
||||
else
|
||||
{
|
||||
var safeDirePath = SanitizePath(itemName);
|
||||
var dirPath = PathBuilder.Dir(BaseDirectory, safeDirePath);
|
||||
|
||||
await AddDirectoryToZip(zipOutputStream, dirPath);
|
||||
}
|
||||
}
|
||||
|
||||
await zipOutputStream.FlushAsync();
|
||||
await outStream.FlushAsync();
|
||||
|
||||
zipOutputStream.Close();
|
||||
outStream.Close();
|
||||
}
|
||||
|
||||
private async Task AddFileToZip(ZipOutputStream zipOutputStream, string path)
|
||||
{
|
||||
// Open file stream
|
||||
var fs = System.IO.File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
||||
|
||||
// Fix path
|
||||
var name = Formatter
|
||||
.ReplaceStart(path, BaseDirectory, "")
|
||||
.TrimStart('/');
|
||||
|
||||
// Meta
|
||||
var entry = new ZipEntry(name);
|
||||
|
||||
entry.Size = fs.Length;
|
||||
|
||||
// Write entry
|
||||
await zipOutputStream.PutNextEntryAsync(entry, CancellationToken.None);
|
||||
|
||||
// Copy file content to tar stream
|
||||
await fs.CopyToAsync(zipOutputStream);
|
||||
fs.Close();
|
||||
|
||||
// Close the entry
|
||||
zipOutputStream.CloseEntry();
|
||||
}
|
||||
|
||||
private async Task AddDirectoryToZip(ZipOutputStream zipOutputStream, string root)
|
||||
{
|
||||
foreach (var file in Directory.GetFiles(root))
|
||||
await AddFileToZip(zipOutputStream, file);
|
||||
|
||||
foreach (var directory in Directory.GetDirectories(root))
|
||||
await AddDirectoryToZip(zipOutputStream, directory);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
[HttpPost("decompress")]
|
||||
public async Task Decompress([FromBody] DecompressRequest request)
|
||||
{
|
||||
if (request.Type == "tar.gz")
|
||||
await DecompressTarGz(request.Path, request.Destination);
|
||||
else if (request.Type == "zip")
|
||||
await DecompressZip(request.Path, request.Destination);
|
||||
}
|
||||
|
||||
#region Tar Gz
|
||||
|
||||
private async Task DecompressTarGz(string path, string destination)
|
||||
{
|
||||
var safeDestination = SanitizePath(destination);
|
||||
|
||||
var safeArchivePath = SanitizePath(path);
|
||||
var archivePath = PathBuilder.File(BaseDirectory, safeArchivePath);
|
||||
|
||||
await using var fs = System.IO.File.Open(archivePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
||||
await using var gzipInputStream = new GZipInputStream(fs);
|
||||
await using var tarInputStream = new TarInputStream(gzipInputStream, Encoding.UTF8);
|
||||
|
||||
while (true)
|
||||
{
|
||||
var entry = await tarInputStream.GetNextEntryAsync(CancellationToken.None);
|
||||
|
||||
if (entry == null)
|
||||
break;
|
||||
|
||||
var safeFilePath = SanitizePath(entry.Name);
|
||||
var fileDestination = PathBuilder.File(BaseDirectory, safeDestination, safeFilePath);
|
||||
var parentFolder = Path.GetDirectoryName(fileDestination);
|
||||
|
||||
// Ensure parent directory exists, if it's not the base directory
|
||||
if (parentFolder != null && parentFolder != BaseDirectory)
|
||||
Directory.CreateDirectory(parentFolder);
|
||||
|
||||
await using var fileDestinationFs =
|
||||
System.IO.File.Open(fileDestination, FileMode.Create, FileAccess.ReadWrite, FileShare.Read);
|
||||
await tarInputStream.CopyToAsync(fileDestinationFs, CancellationToken.None);
|
||||
|
||||
await fileDestinationFs.FlushAsync();
|
||||
fileDestinationFs.Close();
|
||||
}
|
||||
|
||||
tarInputStream.Close();
|
||||
gzipInputStream.Close();
|
||||
fs.Close();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Zip
|
||||
|
||||
private async Task DecompressZip(string path, string destination)
|
||||
{
|
||||
var safeDestination = SanitizePath(destination);
|
||||
|
||||
var safeArchivePath = SanitizePath(path);
|
||||
var archivePath = PathBuilder.File(BaseDirectory, safeArchivePath);
|
||||
|
||||
await using var fs = System.IO.File.Open(archivePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
||||
await using var zipInputStream = new ZipInputStream(fs);
|
||||
|
||||
while (true)
|
||||
{
|
||||
var entry = zipInputStream.GetNextEntry();
|
||||
|
||||
if (entry == null)
|
||||
break;
|
||||
|
||||
if (entry.IsDirectory)
|
||||
continue;
|
||||
|
||||
var safeFilePath = SanitizePath(entry.Name);
|
||||
var fileDestination = PathBuilder.File(BaseDirectory, safeDestination, safeFilePath);
|
||||
var parentFolder = Path.GetDirectoryName(fileDestination);
|
||||
|
||||
// Ensure parent directory exists, if it's not the base directory
|
||||
if (parentFolder != null && parentFolder != BaseDirectory)
|
||||
Directory.CreateDirectory(parentFolder);
|
||||
|
||||
await using var fileDestinationFs =
|
||||
System.IO.File.Open(fileDestination, FileMode.Create, FileAccess.ReadWrite, FileShare.Read);
|
||||
await zipInputStream.CopyToAsync(fileDestinationFs, CancellationToken.None);
|
||||
|
||||
await fileDestinationFs.FlushAsync();
|
||||
fileDestinationFs.Close();
|
||||
}
|
||||
|
||||
zipInputStream.Close();
|
||||
fs.Close();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private string SanitizePath(string path)
|
||||
=> path.Replace("..", "");
|
||||
}
|
||||
@@ -28,6 +28,7 @@
|
||||
<PackageReference Include="MoonCore.Extended" Version="1.2.7" />
|
||||
<PackageReference Include="MoonCore.PluginFramework" Version="1.0.5" />
|
||||
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.2" />
|
||||
<PackageReference Include="SharpZipLib" Version="1.4.2" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0"/>
|
||||
<PackageReference Include="Ben.Demystifier" Version="0.4.1" />
|
||||
</ItemGroup>
|
||||
|
||||
83
Moonlight.Client/Implementations/SysFileSystemProvider.cs
Normal file
83
Moonlight.Client/Implementations/SysFileSystemProvider.cs
Normal file
@@ -0,0 +1,83 @@
|
||||
using MoonCore.Blazor.Tailwind.Fm;
|
||||
using MoonCore.Blazor.Tailwind.Fm.Models;
|
||||
using MoonCore.Helpers;
|
||||
using Moonlight.Shared.Http.Requests.Admin.Sys.Files;
|
||||
using Moonlight.Shared.Http.Responses.Admin.Sys.Files;
|
||||
|
||||
namespace Moonlight.Client.Implementations;
|
||||
|
||||
public class SysFileSystemProvider : IFileSystemProvider, ICompressFileSystemProvider
|
||||
{
|
||||
private readonly HttpApiClient HttpApiClient;
|
||||
private readonly string BaseApiUrl = "api/admin/system/files";
|
||||
|
||||
public CompressType[] CompressTypes { get; } =
|
||||
[
|
||||
new()
|
||||
{
|
||||
Extension = "zip",
|
||||
DisplayName = "ZIP Archive"
|
||||
},
|
||||
new()
|
||||
{
|
||||
Extension = "tar.gz",
|
||||
DisplayName = "GZ Compressed Tar Archive"
|
||||
}
|
||||
];
|
||||
|
||||
public SysFileSystemProvider(HttpApiClient httpApiClient)
|
||||
{
|
||||
HttpApiClient = httpApiClient;
|
||||
}
|
||||
|
||||
public async Task<FileSystemEntry[]> List(string path)
|
||||
{
|
||||
var entries = await HttpApiClient.GetJson<FileSystemEntryResponse[]>(
|
||||
$"{BaseApiUrl}/list?path={path}"
|
||||
);
|
||||
|
||||
return entries.Select(x => new FileSystemEntry()
|
||||
{
|
||||
Name = x.Name,
|
||||
Size = x.Size,
|
||||
CreatedAt = x.CreatedAt,
|
||||
IsFile = x.IsFile,
|
||||
UpdatedAt = x.UpdatedAt
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
public async Task Create(string path, Stream stream)
|
||||
=> await HttpApiClient.PostStream($"{BaseApiUrl}/create?path={path}", stream);
|
||||
|
||||
public async Task Move(string oldPath, string newPath)
|
||||
=> await HttpApiClient.Post($"{BaseApiUrl}/move?oldPath={oldPath}&newPath={newPath}");
|
||||
|
||||
public async Task Delete(string path)
|
||||
=> await HttpApiClient.Delete($"{BaseApiUrl}/delete?path={path}");
|
||||
|
||||
public async Task CreateDirectory(string path)
|
||||
=> await HttpApiClient.Post($"{BaseApiUrl}/mkdir?path={path}");
|
||||
|
||||
public async Task<Stream> Read(string path)
|
||||
=> await HttpApiClient.GetStream($"{BaseApiUrl}/read?path={path}");
|
||||
|
||||
public async Task Compress(CompressType type, string path, string[] itemsToCompress)
|
||||
{
|
||||
await HttpApiClient.Post($"{BaseApiUrl}/compress", new CompressRequest()
|
||||
{
|
||||
Type = type.Extension,
|
||||
Path = path,
|
||||
ItemsToCompress = itemsToCompress
|
||||
});
|
||||
}
|
||||
|
||||
public async Task Decompress(CompressType type, string path, string destination)
|
||||
{
|
||||
await HttpApiClient.Post($"{BaseApiUrl}/decompress", new DecompressRequest()
|
||||
{
|
||||
Type = type.Extension,
|
||||
Path = path,
|
||||
Destination = destination
|
||||
});
|
||||
}
|
||||
}
|
||||
26
Moonlight.Client/UI/Views/Admin/Sys/Files.razor
Normal file
26
Moonlight.Client/UI/Views/Admin/Sys/Files.razor
Normal file
@@ -0,0 +1,26 @@
|
||||
@page "/admin/system/files"
|
||||
|
||||
@using MoonCore.Attributes
|
||||
@using MoonCore.Helpers
|
||||
@using MoonCore.Blazor.Tailwind.Fm
|
||||
@using Moonlight.Client.Implementations
|
||||
|
||||
@attribute [RequirePermission("admin.system.overview")]
|
||||
|
||||
@inject HttpApiClient ApiClient
|
||||
|
||||
<div class="mb-3">
|
||||
<NavTabs Index="2" Names="UiConstants.AdminNavNames" Links="UiConstants.AdminNavLinks" />
|
||||
</div>
|
||||
|
||||
<FileManager FileSystemProvider="FileSystemProvider" />
|
||||
|
||||
@code
|
||||
{
|
||||
private IFileSystemProvider FileSystemProvider;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
FileSystemProvider = new SysFileSystemProvider(ApiClient);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,6 @@ namespace Moonlight.Client;
|
||||
|
||||
public static class UiConstants
|
||||
{
|
||||
public static readonly string[] AdminNavNames = ["Overview", "Theme"];
|
||||
public static readonly string[] AdminNavLinks = ["/admin/system", "/admin/system/theme"];
|
||||
public static readonly string[] AdminNavNames = ["Overview", "Theme", "Files"];
|
||||
public static readonly string[] AdminNavLinks = ["/admin/system", "/admin/system/theme", "/admin/system/files"];
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Moonlight.Shared.Http.Requests.Admin.Sys.Files;
|
||||
|
||||
public class CompressRequest
|
||||
{
|
||||
public string Type { get; set; }
|
||||
public string Path { get; set; }
|
||||
public string[] ItemsToCompress { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Moonlight.Shared.Http.Requests.Admin.Sys.Files;
|
||||
|
||||
public class DecompressRequest
|
||||
{
|
||||
public string Type { get; set; }
|
||||
public string Path { get; set; }
|
||||
public string Destination { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace Moonlight.Shared.Http.Responses.Admin.Sys.Files;
|
||||
|
||||
public class FileSystemEntryResponse
|
||||
{
|
||||
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; }
|
||||
}
|
||||
Reference in New Issue
Block a user