Some checks failed
Release SharePoint Toolbox v2 / release (push) Failing after 14s
v1.1 shipped with 4 phases (25 plans), 10/10 requirements complete: - Global site selection (toolbar picker, all tabs consume) - User access audit (Graph people-picker, direct/group/inherited) - Simplified permissions (plain-language labels, risk levels, detail toggle) - Storage visualization (LiveCharts2 pie/donut + bar charts) Post-phase polish: centralized site selection (removed per-tab pickers), claims prefix stripping, StorageMetrics backfill, chart tooltip fix, summary stats in app + HTML exports. 205 tests passing, 10,484 LOC. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
99 lines
3.6 KiB
C#
99 lines
3.6 KiB
C#
using SharepointToolbox.Core.Models;
|
|
using System.IO;
|
|
using System.Text;
|
|
|
|
namespace SharepointToolbox.Services.Export;
|
|
|
|
/// <summary>
|
|
/// Exports a flat list of StorageNode objects to a UTF-8 BOM CSV.
|
|
/// Compatible with Microsoft Excel (BOM signals UTF-8 encoding).
|
|
/// </summary>
|
|
public class StorageCsvExportService
|
|
{
|
|
public string BuildCsv(IReadOnlyList<StorageNode> 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<StorageNode> nodes, string filePath, CancellationToken ct)
|
|
{
|
|
var csv = BuildCsv(nodes);
|
|
await File.WriteAllTextAsync(filePath, csv, new UTF8Encoding(encoderShouldEmitUTF8Identifier: true), ct);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Builds a CSV with library details followed by a file-type breakdown section.
|
|
/// </summary>
|
|
public string BuildCsv(IReadOnlyList<StorageNode> nodes, IReadOnlyList<FileTypeMetric> fileTypeMetrics)
|
|
{
|
|
var sb = new StringBuilder();
|
|
|
|
// Library details
|
|
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));
|
|
}
|
|
|
|
// File type breakdown
|
|
if (fileTypeMetrics.Count > 0)
|
|
{
|
|
sb.AppendLine();
|
|
sb.AppendLine("File Type,Size (MB),File Count");
|
|
foreach (var m in fileTypeMetrics)
|
|
{
|
|
string label = string.IsNullOrEmpty(m.Extension) ? "(no extension)" : m.Extension;
|
|
sb.AppendLine(string.Join(",", Csv(label), FormatMb(m.TotalSizeBytes), m.FileCount.ToString()));
|
|
}
|
|
}
|
|
|
|
return sb.ToString();
|
|
}
|
|
|
|
public async Task WriteAsync(IReadOnlyList<StorageNode> nodes, IReadOnlyList<FileTypeMetric> fileTypeMetrics, string filePath, CancellationToken ct)
|
|
{
|
|
var csv = BuildCsv(nodes, fileTypeMetrics);
|
|
await File.WriteAllTextAsync(filePath, csv, new UTF8Encoding(encoderShouldEmitUTF8Identifier: true), ct);
|
|
}
|
|
|
|
// ── Helpers ───────────────────────────────────────────────────────────────
|
|
|
|
private static string FormatMb(long bytes)
|
|
=> (bytes / (1024.0 * 1024.0)).ToString("F2");
|
|
|
|
/// <summary>RFC 4180 CSV field quoting.</summary>
|
|
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;
|
|
}
|
|
}
|