using SharepointToolbox.Core.Models; using SharepointToolbox.Localization; using System.IO; 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 { /// /// Builds a single-section CSV: header row plus one row per /// with library, site, file count, total size /// (MB), version size (MB), and last-modified date. /// public string BuildCsv(IReadOnlyList nodes) { var T = TranslationSource.Instance; var sb = new StringBuilder(); // Header sb.AppendLine($"{T["report.col.library"]},{T["report.col.site"]},{T["report.stat.files"]},{T["report.col.total_size_mb"]},{T["report.col.version_size_mb"]},{T["report.col.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(); } /// Writes the library-level CSV to with UTF-8 BOM. public async Task WriteAsync(IReadOnlyList nodes, string filePath, CancellationToken ct) { var csv = BuildCsv(nodes); await ExportFileWriter.WriteCsvAsync(filePath, csv, ct); } /// /// Builds a CSV with library details followed by a file-type breakdown section. /// public string BuildCsv(IReadOnlyList nodes, IReadOnlyList fileTypeMetrics) { var T = TranslationSource.Instance; var sb = new StringBuilder(); // Library details sb.AppendLine($"{T["report.col.library"]},{T["report.col.site"]},{T["report.stat.files"]},{T["report.col.total_size_mb"]},{T["report.col.version_size_mb"]},{T["report.col.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)); } // File type breakdown if (fileTypeMetrics.Count > 0) { sb.AppendLine(); sb.AppendLine($"{T["report.col.file_type"]},{T["report.col.size_mb"]},{T["report.col.file_count"]}"); foreach (var m in fileTypeMetrics) { string label = string.IsNullOrEmpty(m.Extension) ? T["report.text.no_extension"] : m.Extension; sb.AppendLine(string.Join(",", Csv(label), FormatMb(m.TotalSizeBytes), m.FileCount.ToString())); } } return sb.ToString(); } /// Writes the two-section CSV (libraries + file-type breakdown) with UTF-8 BOM. public async Task WriteAsync(IReadOnlyList nodes, IReadOnlyList fileTypeMetrics, string filePath, CancellationToken ct) { var csv = BuildCsv(nodes, fileTypeMetrics); await ExportFileWriter.WriteCsvAsync(filePath, csv, ct); } /// /// Writes storage metrics with optional per-site partitioning. /// Single → one file. BySite → one file per SiteTitle. File-type metrics /// are replicated across all partitions because the tenant-level scan /// does not retain per-site breakdowns. /// public Task WriteAsync( IReadOnlyList nodes, IReadOnlyList fileTypeMetrics, string basePath, ReportSplitMode splitMode, CancellationToken ct) => ReportSplitHelper.WritePartitionedAsync( nodes, basePath, splitMode, PartitionBySite, (part, path, c) => WriteAsync(part, fileTypeMetrics, path, c), ct); /// /// Splits the flat StorageNode list into per-site slices while preserving /// the DFS hierarchy (each root library followed by its indented descendants). /// Siblings sharing a SiteTitle roll up into the same partition. /// internal static IEnumerable<(string Label, IReadOnlyList Partition)> PartitionBySite( IReadOnlyList nodes) { var buckets = new Dictionary>(StringComparer.OrdinalIgnoreCase); string currentSite = string.Empty; foreach (var node in nodes) { if (node.IndentLevel == 0) currentSite = string.IsNullOrWhiteSpace(node.SiteTitle) ? ReportSplitHelper.DeriveSiteLabel(node.Url) : node.SiteTitle; if (!buckets.TryGetValue(currentSite, out var list)) { list = new List(); buckets[currentSite] = list; } list.Add(node); } return buckets.Select(kv => (kv.Key, (IReadOnlyList)kv.Value)); } // ── Helpers ─────────────────────────────────────────────────────────────── private static string FormatMb(long bytes) => (bytes / (1024.0 * 1024.0)).ToString("F2"); /// RFC 4180 CSV field quoting with formula-injection guard. private static string Csv(string value) => CsvSanitizer.EscapeMinimal(value); }