Site manager

This commit is contained in:
2025-11-20 22:54:24 +08:00
parent afccb27bd4
commit c0ebb496fe
3 changed files with 320 additions and 1 deletions

View File

@@ -0,0 +1,133 @@
using DysonNetwork.Shared.Models;
namespace DysonNetwork.Zone.Publication;
public class FileEntry
{
public bool IsDirectory { get; set; }
public string RelativePath { get; set; } = null!;
public long Size { get; set; }
public DateTimeOffset Modified { get; set; }
}
public class PublicationSiteManager(
IConfiguration configuration,
IWebHostEnvironment hostEnvironment,
PublicationSiteService publicationSiteService
)
{
private readonly string _basePath = Path.Combine(
hostEnvironment.WebRootPath,
configuration["Sites:BasePath"]!.TrimStart('/')
);
private string GetFullPath(Guid siteId, string relativePath)
{
var fullPath = Path.Combine(_basePath, siteId.ToString(), relativePath);
var siteDirPath = Path.Combine(_basePath, siteId.ToString());
return !Path.GetRelativePath(siteDirPath, fullPath).StartsWith('.')
? throw new ArgumentException("Invalid path")
: fullPath;
}
private async Task EnsureSiteDirectory(Guid siteId)
{
var site = await publicationSiteService.GetSiteById(siteId);
if (site is not { Mode: PublicationSiteMode.SelfManaged })
throw new InvalidOperationException("Site not found or not self-managed");
var dir = Path.Combine(_basePath, siteId.ToString());
if (!Directory.Exists(dir))
Directory.CreateDirectory(dir);
}
public async Task<List<FileEntry>> ListFiles(Guid siteId, string relativePath = "")
{
await EnsureSiteDirectory(siteId);
var dir = Path.Combine(_basePath, siteId.ToString(), relativePath);
if (!Directory.Exists(dir))
throw new DirectoryNotFoundException("Directory not found");
var entries = (from file in Directory.GetFiles(dir)
let fileInfo = new FileInfo(file)
select new FileEntry
{
IsDirectory = false,
RelativePath = Path.GetRelativePath(Path.Combine(_basePath, siteId.ToString()), file),
Size = fileInfo.Length, Modified = fileInfo.LastWriteTimeUtc
}).ToList();
entries.AddRange(from subDir in Directory.GetDirectories(dir)
let dirInfo = new DirectoryInfo(subDir)
select new FileEntry
{
IsDirectory = true,
RelativePath = Path.GetRelativePath(Path.Combine(_basePath, siteId.ToString()), subDir),
Size = 0, // Directories don't have size
Modified = dirInfo.LastWriteTimeUtc
});
return entries;
}
public async Task UploadFile(Guid siteId, string relativePath, IFormFile file)
{
await EnsureSiteDirectory(siteId);
var fullPath = GetFullPath(siteId, relativePath);
var dir = Path.GetDirectoryName(fullPath);
if (dir != null && !Directory.Exists(dir))
Directory.CreateDirectory(dir);
await using var stream = new FileStream(fullPath, FileMode.Create);
await file.CopyToAsync(stream);
}
public async Task<string> ReadFileContent(Guid siteId, string relativePath)
{
await EnsureSiteDirectory(siteId);
var fullPath = GetFullPath(siteId, relativePath);
if (!File.Exists(fullPath))
throw new FileNotFoundException();
return await File.ReadAllTextAsync(fullPath);
}
public async Task<long> GetTotalSiteSize(Guid siteId)
{
await EnsureSiteDirectory(siteId);
var dir = new DirectoryInfo(Path.Combine(_basePath, siteId.ToString()));
return GetDirectorySize(dir);
}
private long GetDirectorySize(DirectoryInfo dir)
{
var files = dir.GetFiles();
var size = files.Sum(file => file.Length);
var subDirs = dir.GetDirectories();
size += subDirs.Sum(GetDirectorySize);
return size;
}
public string GetFullPathForDownload(Guid siteId, string relativePath)
{
// Internal method to get path without throwing if not exists
return Path.Combine(_basePath, siteId.ToString(), relativePath);
}
public async Task UpdateFile(Guid siteId, string relativePath, string newContent)
{
await EnsureSiteDirectory(siteId);
var fullPath = GetFullPath(siteId, relativePath);
await File.WriteAllTextAsync(fullPath, newContent);
}
public async Task DeleteFile(Guid siteId, string relativePath)
{
await EnsureSiteDirectory(siteId);
var fullPath = GetFullPath(siteId, relativePath);
if (File.Exists(fullPath))
File.Delete(fullPath);
else if (Directory.Exists(fullPath))
Directory.Delete(fullPath, true);
}
}

View File

@@ -0,0 +1,185 @@
using DysonNetwork.Shared.Models;
using DysonNetwork.Shared.Proto;
using DysonNetwork.Shared.Registry;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using PublisherMemberRole = DysonNetwork.Shared.Models.PublisherMemberRole;
namespace DysonNetwork.Zone.Publication;
[ApiController]
[Route("api/sites/{siteId:guid}/files")]
public class SiteManagerController(
PublicationSiteService publicationSiteService,
PublicationSiteManager fileManager,
RemotePublisherService remotePublisherService
) : ControllerBase
{
private async Task<ActionResult?> CheckAccess(Guid siteId)
{
var site = await publicationSiteService.GetSiteById(siteId);
if (site is not { Mode: PublicationSiteMode.SelfManaged })
return NotFound("Site not found or not self-managed");
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
var accountId = Guid.Parse(currentUser.Id);
var isMember = await remotePublisherService.IsMemberWithRole(site.PublisherId, accountId, PublisherMemberRole.Editor);
return !isMember ? Forbid() : null;
}
[HttpGet]
[Authorize]
public async Task<ActionResult<List<FileEntry>>> ListFiles(Guid siteId, [FromQuery] string path = "")
{
var check = await CheckAccess(siteId);
if (check != null) return check;
try
{
var entries = await fileManager.ListFiles(siteId, path);
return Ok(entries);
}
catch (DirectoryNotFoundException)
{
return NotFound("Directory not found");
}
catch (Exception ex)
{
return BadRequest(ex.Message);
}
}
[HttpPost("upload")]
[Authorize]
[Consumes("multipart/form-data")]
public async Task<IActionResult> UploadFile(Guid siteId, [FromForm] string filePath, IFormFile? file)
{
var check = await CheckAccess(siteId);
if (check != null) return check;
if (file == null || file.Length == 0)
return BadRequest("No file provided");
const long maxFileSize = 1048576; // 1MB
const long maxTotalSize = 26214400; // 25MB
if (file.Length > maxFileSize)
return BadRequest("File size exceeds 1MB limit");
var currentTotal = await fileManager.GetTotalSiteSize(siteId);
if (currentTotal + file.Length > maxTotalSize)
return BadRequest("Site total size would exceed 25MB limit");
try
{
await fileManager.UploadFile(siteId, filePath, file);
return Ok();
}
catch (Exception ex)
{
return BadRequest(ex.Message);
}
}
[HttpGet("content/{**relativePath}")]
[Authorize]
public async Task<ActionResult<string>> GetFileContent(Guid siteId, string relativePath)
{
var check = await CheckAccess(siteId);
if (check != null) return check;
try
{
var content = await fileManager.ReadFileContent(siteId, relativePath);
return Ok(content);
}
catch (FileNotFoundException)
{
return NotFound();
}
catch (Exception ex)
{
return BadRequest(ex.Message);
}
}
[HttpGet("download/{**relativePath}")]
[Authorize]
public async Task<IActionResult> DownloadFile(Guid siteId, string relativePath)
{
var check = await CheckAccess(siteId);
if (check != null) return check;
var fullPath = fileManager.GetFullPathForDownload(siteId, relativePath);
if (!System.IO.File.Exists(fullPath))
return NotFound();
// Determine MIME type
var mimeType = "application/octet-stream"; // default
var ext = Path.GetExtension(relativePath).ToLowerInvariant();
if (ext == ".txt") mimeType = "text/plain";
else if (ext == ".html" || ext == ".htm") mimeType = "text/html";
else if (ext == ".css") mimeType = "text/css";
else if (ext == ".js") mimeType = "application/javascript";
else if (ext == ".json") mimeType = "application/json";
return PhysicalFile(fullPath, mimeType, Path.GetFileName(relativePath));
}
[HttpPut("edit/{**relativePath}")]
[Authorize]
public async Task<IActionResult> UpdateFile(Guid siteId, string relativePath, [FromBody] UpdateFileRequest request)
{
var check = await CheckAccess(siteId);
if (check != null) return check;
const long maxFileSize = 1048576; // 1MB
const long maxTotalSize = 26214400; // 25MB
var fullPath = fileManager.GetFullPathForDownload(siteId, relativePath);
long oldSize = 0;
if (System.IO.File.Exists(fullPath))
oldSize = new FileInfo(fullPath).Length;
if (request.NewContent.Length > maxFileSize)
return BadRequest("New content exceeds 1MB limit");
var currentTotal = await fileManager.GetTotalSiteSize(siteId);
if (currentTotal - oldSize + request.NewContent.Length > maxTotalSize)
return BadRequest("Site total size would exceed 25MB limit");
try
{
await fileManager.UpdateFile(siteId, relativePath, request.NewContent);
return Ok();
}
catch (Exception ex)
{
return BadRequest(ex.Message);
}
}
[HttpDelete("delete/{**relativePath}")]
[Authorize]
public async Task<IActionResult> DeleteFile(Guid siteId, string relativePath)
{
var check = await CheckAccess(siteId);
if (check != null) return check;
try
{
await fileManager.DeleteFile(siteId, relativePath);
return Ok();
}
catch (Exception ex)
{
return BadRequest(ex.Message);
}
}
}
public class UpdateFileRequest
{
public string NewContent { get; set; } = null!;
}

View File

@@ -76,7 +76,8 @@ public static class ServiceCollectionExtensions
services.AddScoped<GeoIpService>(); services.AddScoped<GeoIpService>();
services.AddScoped<PublicationSiteService>(); services.AddScoped<PublicationSiteService>();
services.AddScoped<PublicationSiteManager>();
return services; return services;
} }
} }