From eafaa154591b19be9841ccb27e37032808993cbf Mon Sep 17 00:00:00 2001 From: Dev Date: Thu, 2 Apr 2026 15:30:34 +0200 Subject: [PATCH] feat(03-03): implement StorageHtmlExportService MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace string.Empty stub with full BuildHtml implementation - Self-contained HTML with inline CSS and JS — no external dependencies - toggle(i) JS function with collapsible subfolder rows (sf-{i} IDs) - _togIdx counter reset at start of each BuildHtml call (per PS pattern) - RenderNode/RenderChildNode for recursive tree rendering - FormatSize helper: B/KB/MB/GB adaptive display - HtmlEncode via System.Net.WebUtility - Add explicit System.IO using (required in WPF project) --- .../Export/StorageHtmlExportService.cs | 168 +++++++++++++++++- 1 file changed, 166 insertions(+), 2 deletions(-) diff --git a/SharepointToolbox/Services/Export/StorageHtmlExportService.cs b/SharepointToolbox/Services/Export/StorageHtmlExportService.cs index 3818d42..2278c01 100644 --- a/SharepointToolbox/Services/Export/StorageHtmlExportService.cs +++ b/SharepointToolbox/Services/Export/StorageHtmlExportService.cs @@ -1,14 +1,178 @@ 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 { - public string BuildHtml(IReadOnlyList nodes) => string.Empty; // implemented in Plan 03-03 + private int _togIdx; + + public string BuildHtml(IReadOnlyList nodes) + { + _togIdx = 0; + var sb = new StringBuilder(); + + sb.AppendLine(""" + + + + + + SharePoint Storage Metrics + + + + +

SharePoint Storage Metrics

+ """); + + sb.AppendLine(""" + + + + + + + + + + + + + """); + + foreach (var node in nodes) + { + RenderNode(sb, node); + } + + sb.AppendLine(""" + +
Library / FolderSiteFilesTotal SizeVersion SizeLast 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) { var html = BuildHtml(nodes); - await System.IO.File.WriteAllTextAsync(filePath, html, System.Text.Encoding.UTF8, ct); + 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); }