--- phase: 03 plan: 03 title: Storage Export Services — CSV and Collapsible-Tree HTML status: pending wave: 2 depends_on: - 03-02 files_modified: - SharepointToolbox/Services/Export/StorageCsvExportService.cs - SharepointToolbox/Services/Export/StorageHtmlExportService.cs autonomous: true requirements: - STOR-04 - STOR-05 must_haves: truths: - "StorageCsvExportService.BuildCsv produces a UTF-8 BOM CSV with header: Library, Site, Files, Total Size (MB), Version Size (MB), Last Modified" - "StorageCsvExportService.BuildCsv includes one row per StorageNode (flattened, respects IndentLevel for Library name prefix)" - "StorageHtmlExportService.BuildHtml produces a self-contained HTML file with inline CSS and JS — no external dependencies" - "StorageHtmlExportService.BuildHtml includes toggle(i) JS and collapsible subfolder rows (sf-{i} IDs)" - "StorageCsvExportServiceTests: all 3 tests pass" - "StorageHtmlExportServiceTests: all 3 tests pass" artifacts: - path: "SharepointToolbox/Services/Export/StorageCsvExportService.cs" provides: "CSV exporter for StorageNode list (STOR-04)" exports: ["StorageCsvExportService"] - path: "SharepointToolbox/Services/Export/StorageHtmlExportService.cs" provides: "Collapsible-tree HTML exporter for StorageNode list (STOR-05)" exports: ["StorageHtmlExportService"] key_links: - from: "StorageCsvExportService.cs" to: "StorageNode.VersionSizeBytes" via: "computed property" pattern: "VersionSizeBytes" - from: "StorageHtmlExportService.cs" to: "toggle(i) JS" via: "inline script" pattern: "toggle\\(" --- # Plan 03-03: Storage Export Services — CSV and Collapsible-Tree HTML ## Goal Replace the stub implementations in `StorageCsvExportService` and `StorageHtmlExportService` with real implementations. The CSV export produces a flat UTF-8 BOM CSV compatible with Excel. The HTML export ports the PowerShell `Export-StorageToHTML` function (PS lines 1621-1780), producing a self-contained HTML file with a collapsible tree view driven by an inline `toggle(i)` JavaScript function. ## Context Plan 03-01 created stub `BuildCsv`/`BuildHtml` methods returning `string.Empty`. This plan fills them in. The test files `StorageCsvExportServiceTests.cs` and `StorageHtmlExportServiceTests.cs` already exist and define the expected output — they currently fail because of the stubs. Pattern reference: Phase 2 `CsvExportService` uses UTF-8 BOM + RFC 4180 quoting. The same `Csv()` helper pattern is applied here. `StorageHtmlExportService` uses a `_togIdx` counter reset at the start of each `BuildHtml` call (per the PS pattern) to generate unique IDs for collapsible rows. ## Tasks ### Task 1: Implement StorageCsvExportService **File:** `SharepointToolbox/Services/Export/StorageCsvExportService.cs` **Action:** Modify (replace stub with full implementation) **Why:** STOR-04 — user can export storage metrics to CSV. ```csharp using SharepointToolbox.Core.Models; using System.Text; namespace SharepointToolbox.Services.Export; /// /// Exports a flat list of StorageNode objects to a UTF-8 BOM CSV. /// Compatible with Microsoft Excel (BOM signals UTF-8 encoding). /// public class StorageCsvExportService { public string BuildCsv(IReadOnlyList nodes) { var sb = new StringBuilder(); // Header sb.AppendLine("Library,Site,Files,Total Size (MB),Version Size (MB),Last Modified"); foreach (var node in nodes) { sb.AppendLine(string.Join(",", Csv(node.Name), Csv(node.SiteTitle), node.TotalFileCount.ToString(), FormatMb(node.TotalSizeBytes), FormatMb(node.VersionSizeBytes), node.LastModified.HasValue ? Csv(node.LastModified.Value.ToString("yyyy-MM-dd")) : string.Empty)); } return sb.ToString(); } public async Task WriteAsync(IReadOnlyList nodes, string filePath, CancellationToken ct) { var csv = BuildCsv(nodes); // UTF-8 with BOM for Excel compatibility await File.WriteAllTextAsync(filePath, csv, new UTF8Encoding(encoderShouldEmitUTF8Identifier: true), ct); } // ── Helpers ─────────────────────────────────────────────────────────────── private static string FormatMb(long bytes) => (bytes / (1024.0 * 1024.0)).ToString("F2"); /// RFC 4180 CSV field quoting. private static string Csv(string value) { if (string.IsNullOrEmpty(value)) return string.Empty; if (value.Contains(',') || value.Contains('"') || value.Contains('\n')) return $"\"{value.Replace("\"", "\"\"")}\""; return value; } } ``` **Verification:** ```bash dotnet test C:/Users/dev/Documents/projets/Sharepoint/SharepointToolbox.Tests/SharepointToolbox.Tests.csproj --filter "FullyQualifiedName~StorageCsvExportServiceTests" -x ``` Expected: 3 tests pass ### Task 2: Implement StorageHtmlExportService **File:** `SharepointToolbox/Services/Export/StorageHtmlExportService.cs` **Action:** Modify (replace stub with full implementation) **Why:** STOR-05 — user can export storage metrics to interactive HTML with collapsible tree view. ```csharp using SharepointToolbox.Core.Models; 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) { _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 / 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) { var html = BuildHtml(nodes); 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); } ``` **Verification:** ```bash dotnet test C:/Users/dev/Documents/projets/Sharepoint/SharepointToolbox.Tests/SharepointToolbox.Tests.csproj --filter "FullyQualifiedName~StorageHtmlExportServiceTests" -x ``` Expected: 3 tests pass ## Verification ```bash dotnet test C:/Users/dev/Documents/projets/Sharepoint/SharepointToolbox.Tests/SharepointToolbox.Tests.csproj --filter "FullyQualifiedName~StorageCsvExportServiceTests|FullyQualifiedName~StorageHtmlExportServiceTests" -x ``` Expected: 6 tests pass, 0 fail ## Commit Message feat(03-03): implement StorageCsvExportService and StorageHtmlExportService ## Output After completion, create `.planning/phases/03-storage/03-03-SUMMARY.md`