using DysonNetwork.Shared.Models; using System.IO.Compression; 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.ContentRootPath, configuration["Sites:BasePath"]!.TrimStart('/') ); private string GetFullPath(Guid siteId, string relativePath) { // Treat paths starting with separator as relative to site root relativePath = relativePath.TrimStart('/', '\\'); var fullPath = Path.Combine(_basePath, siteId.ToString(), relativePath); var normalizedPath = Path.GetFullPath(fullPath); var siteDirFull = Path.Combine(_basePath, siteId.ToString()); var normalizedSiteDir = Path.GetFullPath(siteDirFull); if (!normalizedPath.StartsWith(normalizedSiteDir + Path.DirectorySeparatorChar) && !normalizedPath.Equals(normalizedSiteDir)) { throw new ArgumentException("Path escapes site directory"); } return normalizedPath; } 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> ListFiles(Guid siteId, string relativePath = "") { await EnsureSiteDirectory(siteId); var targetDir = GetFullPath(siteId, relativePath); if (!Directory.Exists(targetDir)) throw new DirectoryNotFoundException("Directory not found"); var entries = (from file in Directory.GetFiles(targetDir) 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(targetDir) 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 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 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 GetValidatedFullPath(Guid siteId, string relativePath) { return GetFullPath(siteId, relativePath); } public string GetSiteDirectory(Guid siteId) { return Path.Combine(_basePath, siteId.ToString()); } 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); } public async Task PurgeSite(Guid siteId) { await EnsureSiteDirectory(siteId); // This ensures site exists and is self-managed var siteDirectory = Path.Combine(_basePath, siteId.ToString()); if (Directory.Exists(siteDirectory)) { Directory.Delete(siteDirectory, true); // true for recursive delete Directory.CreateDirectory(siteDirectory); // Recreate empty directory } } public async Task DeployZip(Guid siteId, IFormFile zipFile) { await EnsureSiteDirectory(siteId); var siteDirectory = Path.Combine(_basePath, siteId.ToString()); // Create a temporary file for the uploaded zip var tempFilePath = Path.GetTempFileName(); try { await using (var stream = new FileStream(tempFilePath, FileMode.Create)) { await zipFile.CopyToAsync(stream); } // Extract the zip file to the site's directory, overwriting existing files await ZipFile.ExtractToDirectoryAsync(tempFilePath, siteDirectory, true); } finally { // Clean up the temporary file if (File.Exists(tempFilePath)) { File.Delete(tempFilePath); } } } }