Files
Swarm/DysonNetwork.Zone/Publication/SiteManagerController.cs

261 lines
8.4 KiB
C#

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);
}
}
[HttpPost("deploy")]
[Authorize]
[Consumes("multipart/form-data")]
public async Task<IActionResult> Deploy(Guid siteId, IFormFile? zipFile)
{
var check = await CheckAccess(siteId);
if (check != null) return check;
if (zipFile == null || zipFile.Length == 0)
return BadRequest("No file provided.");
if (Path.GetExtension(zipFile.FileName).ToLowerInvariant() != ".zip")
return BadRequest("Only .zip files are allowed for deployment.");
// Define size limits
const long maxZipFileSize = 52428800; // 50MB for the zip file itself
const long maxTotalSiteSizeAfterExtract = 104857600; // 100MB total size after extraction
if (zipFile.Length > maxZipFileSize)
return BadRequest($"Zip file size exceeds {maxZipFileSize / (1024 * 1024)}MB limit.");
try
{
// For now, we'll only check the zip file size.
// A more robust solution might involve extracting to a temp location
// and checking the uncompressed size before moving, but that's more complex.
// Get current site size before deployment
long currentTotal = await fileManager.GetTotalSiteSize(siteId);
// This is a rough check. The actual uncompressed size might be much larger.
// Consider adding a more sophisticated check if this is a concern.
if (currentTotal + zipFile.Length * 3 > maxTotalSiteSizeAfterExtract) // Heuristic: assume 3x expansion
return BadRequest($"Deployment would exceed total site size limit of {maxTotalSiteSizeAfterExtract / (1024 * 1024)}MB.");
await fileManager.DeployZip(siteId, zipFile);
return Ok("Deployment successful.");
}
catch (Exception ex)
{
return BadRequest($"Deployment failed: {ex.Message}");
}
}
[HttpDelete("purge")]
[Authorize]
public async Task<IActionResult> Purge(Guid siteId)
{
var check = await CheckAccess(siteId);
if (check != null) return check;
try
{
await fileManager.PurgeSite(siteId);
return Ok("Site content purged successfully.");
}
catch (Exception ex)
{
return BadRequest($"Purge failed: {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;
string fullPath;
try
{
fullPath = fileManager.GetValidatedFullPath(siteId, relativePath);
}
catch (ArgumentException)
{
return BadRequest("Invalid path");
}
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
if (request.NewContent.Length > maxFileSize)
return BadRequest("New content exceeds 1MB limit");
long oldSize = 0;
try
{
var fullPath = fileManager.GetValidatedFullPath(siteId, relativePath);
if (System.IO.File.Exists(fullPath))
oldSize = new FileInfo(fullPath).Length;
var currentTotal = await fileManager.GetTotalSiteSize(siteId);
if (currentTotal - oldSize + request.NewContent.Length > maxTotalSize)
return BadRequest("Site total size would exceed 25MB limit");
await fileManager.UpdateFile(siteId, relativePath, request.NewContent);
return Ok();
}
catch (ArgumentException)
{
return BadRequest("Invalid path");
}
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!;
}