using SharepointToolbox.Core.Models; using System.IO; using System.Text; namespace SharepointToolbox.Services.Export; /// /// Exports StorageNode tree to a self-contained HTML file with collapsible subfolder rows. /// Port of PS Export-StorageToHTML (PS lines 1621-1780). /// Uses a toggle(i) JS pattern where each collapsible row has id="sf-{i}". /// public class StorageHtmlExportService { private int _togIdx; public string BuildHtml(IReadOnlyList nodes, ReportBranding? branding = null) { _togIdx = 0; var sb = new StringBuilder(); sb.AppendLine(""" SharePoint Storage Metrics """); sb.Append(BrandingHtmlHelper.BuildBrandingHeader(branding)); sb.AppendLine("""

SharePoint Storage Metrics

"""); // Summary cards var rootNodes0 = nodes.Where(n => n.IndentLevel == 0).ToList(); long siteTotal0 = rootNodes0.Sum(n => n.TotalSizeBytes); long versionTotal0 = rootNodes0.Sum(n => n.VersionSizeBytes); long fileTotal0 = rootNodes0.Sum(n => n.TotalFileCount); sb.AppendLine($"""
{FormatSize(siteTotal0)}
Total Size
{FormatSize(versionTotal0)}
Version Size
{fileTotal0:N0}
Files
"""); sb.AppendLine(""" """); foreach (var node in nodes) { RenderNode(sb, node); } sb.AppendLine("""
Library / Folder Site Files Total Size Version Size Last Modified
"""); sb.AppendLine($"

Generated: {DateTime.Now:yyyy-MM-dd HH:mm}

"); sb.AppendLine(""); return sb.ToString(); } /// /// Builds an HTML report including a file-type breakdown chart section. /// public string BuildHtml(IReadOnlyList nodes, IReadOnlyList fileTypeMetrics, ReportBranding? branding = null) { _togIdx = 0; var sb = new StringBuilder(); sb.AppendLine(""" SharePoint Storage Metrics """); sb.Append(BrandingHtmlHelper.BuildBrandingHeader(branding)); sb.AppendLine("""

SharePoint Storage Metrics

"""); // ── Summary cards ── var rootNodes = nodes.Where(n => n.IndentLevel == 0).ToList(); long siteTotal = rootNodes.Sum(n => n.TotalSizeBytes); long versionTotal = rootNodes.Sum(n => n.VersionSizeBytes); long fileTotal = rootNodes.Sum(n => n.TotalFileCount); sb.AppendLine("
"); sb.AppendLine($"
{FormatSize(siteTotal)}
Total Size
"); sb.AppendLine($"
{FormatSize(versionTotal)}
Version Size
"); sb.AppendLine($"
{fileTotal:N0}
Files
"); sb.AppendLine($"
{rootNodes.Count}
Libraries
"); sb.AppendLine("
"); // ── File type chart section ── if (fileTypeMetrics.Count > 0) { var maxSize = fileTypeMetrics.Max(m => m.TotalSizeBytes); var totalSize = fileTypeMetrics.Sum(m => m.TotalSizeBytes); var totalFiles = fileTypeMetrics.Sum(m => m.FileCount); sb.AppendLine("
"); sb.AppendLine($"

Storage by File Type ({totalFiles:N0} files, {FormatSize(totalSize)})

"); var colors = new[] { "#0078d4", "#2b88d8", "#106ebe", "#005a9e", "#004578", "#00bcf2", "#009e49", "#8cbd18", "#ffb900", "#d83b01" }; int idx = 0; foreach (var m in fileTypeMetrics.Take(15)) { double pct = maxSize > 0 ? m.TotalSizeBytes * 100.0 / maxSize : 0; string color = colors[idx % colors.Length]; string label = string.IsNullOrEmpty(m.Extension) ? "(no ext)" : m.Extension; sb.AppendLine($"""
{HtmlEncode(label)}
{FormatSize(m.TotalSizeBytes)} · {m.FileCount:N0} files
"""); idx++; } sb.AppendLine("
"); } // ── Storage table ── sb.AppendLine("

Library Details

"); sb.AppendLine(""" """); foreach (var node in nodes) { RenderNode(sb, node); } sb.AppendLine("""
Library / Folder Site Files Total Size Version Size Last Modified
"""); sb.AppendLine($"

Generated: {DateTime.Now:yyyy-MM-dd HH:mm}

"); sb.AppendLine(""); return sb.ToString(); } public async Task WriteAsync(IReadOnlyList nodes, string filePath, CancellationToken ct, ReportBranding? branding = null) { var html = BuildHtml(nodes, branding); await File.WriteAllTextAsync(filePath, html, Encoding.UTF8, ct); } public async Task WriteAsync(IReadOnlyList nodes, IReadOnlyList fileTypeMetrics, string filePath, CancellationToken ct, ReportBranding? branding = null) { var html = BuildHtml(nodes, fileTypeMetrics, branding); await File.WriteAllTextAsync(filePath, html, Encoding.UTF8, ct); } // ── Private rendering ──────────────────────────────────────────────────── private void RenderNode(StringBuilder sb, StorageNode node) { bool hasChildren = node.Children.Count > 0; int myIdx = hasChildren ? ++_togIdx : 0; string nameCell = hasChildren ? $"{HtmlEncode(node.Name)}" : $"{HtmlEncode(node.Name)}"; string lastMod = node.LastModified.HasValue ? node.LastModified.Value.ToString("yyyy-MM-dd") : string.Empty; sb.AppendLine($""" {nameCell} {HtmlEncode(node.SiteTitle)} {node.TotalFileCount:N0} {FormatSize(node.TotalSizeBytes)} {FormatSize(node.VersionSizeBytes)} {lastMod} """); if (hasChildren) { sb.AppendLine($""); sb.AppendLine(""); foreach (var child in node.Children) { RenderChildNode(sb, child); } sb.AppendLine("
"); sb.AppendLine(""); } } private void RenderChildNode(StringBuilder sb, StorageNode node) { bool hasChildren = node.Children.Count > 0; int myIdx = hasChildren ? ++_togIdx : 0; string indent = $"margin-left:{(node.IndentLevel + 1) * 16}px"; string nameCell = hasChildren ? $"{HtmlEncode(node.Name)}" : $"{HtmlEncode(node.Name)}"; string lastMod = node.LastModified.HasValue ? node.LastModified.Value.ToString("yyyy-MM-dd") : string.Empty; sb.AppendLine($""" {nameCell} {HtmlEncode(node.SiteTitle)} {node.TotalFileCount:N0} {FormatSize(node.TotalSizeBytes)} {FormatSize(node.VersionSizeBytes)} {lastMod} """); if (hasChildren) { sb.AppendLine($""); sb.AppendLine(""); foreach (var child in node.Children) { RenderChildNode(sb, child); } sb.AppendLine("
"); sb.AppendLine(""); } } private static string FormatSize(long bytes) { if (bytes >= 1_073_741_824L) return $"{bytes / 1_073_741_824.0:F2} GB"; if (bytes >= 1_048_576L) return $"{bytes / 1_048_576.0:F2} MB"; if (bytes >= 1024L) return $"{bytes / 1024.0:F2} KB"; return $"{bytes} B"; } private static string HtmlEncode(string value) => System.Net.WebUtility.HtmlEncode(value ?? string.Empty); }