Files
Sharepoint-Toolbox/SharepointToolbox/Services/Export/StorageCsvExportService.cs
T
2026-05-06 09:17:40 +02:00

228 lines
9.6 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using SharepointToolbox.Core.Models;
using SharepointToolbox.Localization;
using System.Globalization;
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
{
/// <summary>
/// Builds a single-section CSV: header row plus one row per
/// <see cref="StorageNode"/> with library, site, file count, total size
/// (MB), version size (MB), and last-modified date.
/// </summary>
public string BuildCsv(IReadOnlyList<StorageNode> nodes)
{
// Pre-size: ~110 chars/row + header avoids most StringBuilder growth.
var sb = new StringBuilder(128 + nodes.Count * 110);
WriteCsv(sb, nodes);
return sb.ToString();
}
private static void WriteCsv(StringBuilder sb, IReadOnlyList<StorageNode> nodes)
{
var T = TranslationSource.Instance;
// Hoist resource lookups out of the row loop: ResourceManager.GetString
// is a culture-aware dictionary probe — caching once per export saves
// O(rows × columns) lookups on large tenants.
string colLibrary = T["report.col.library"];
string colKind = T["stor.col.kind"];
string colSite = T["report.col.site"];
string colFiles = T["report.stat.files"];
string colTotalMb = T["report.col.total_size_mb"];
string colVerMb = T["report.col.version_size_mb"];
string colLastMod = T["report.col.last_modified"];
sb.Append(colLibrary).Append(',')
.Append(colKind).Append(',')
.Append(colSite).Append(',')
.Append(colFiles).Append(',')
.Append(colTotalMb).Append(',')
.Append(colVerMb).Append(',')
.AppendLine(colLastMod);
var kindLabels = BuildKindLabelCache();
foreach (var node in nodes)
{
AppendCsvField(sb, node.Name).Append(',');
AppendCsvField(sb, kindLabels[(int)node.Kind]).Append(',');
AppendCsvField(sb, node.SiteTitle).Append(',');
sb.Append(node.TotalFileCount).Append(',');
AppendMb(sb, node.TotalSizeBytes).Append(',');
AppendMb(sb, node.VersionSizeBytes).Append(',');
if (node.LastModified.HasValue)
AppendCsvField(sb, node.LastModified.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture));
sb.AppendLine();
}
}
/// <summary>Writes the library-level CSV to <paramref name="filePath"/> with UTF-8 BOM.</summary>
public async Task WriteAsync(IReadOnlyList<StorageNode> nodes, string filePath, CancellationToken ct)
{
// Stream straight to disk: skip the StringBuilder→string copy and the
// separate UTF-8 buffer that File.WriteAllTextAsync materializes.
var sb = new StringBuilder(128 + nodes.Count * 110);
WriteCsv(sb, nodes);
await ExportFileWriter.WriteCsvChunksAsync(filePath, sb, 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(192 + nodes.Count * 100 + fileTypeMetrics.Count * 40);
WriteCsv(sb, nodes, fileTypeMetrics);
return sb.ToString();
}
private static void WriteCsv(StringBuilder sb, IReadOnlyList<StorageNode> nodes, IReadOnlyList<FileTypeMetric> fileTypeMetrics)
{
var T = TranslationSource.Instance;
string colLibrary = T["report.col.library"];
string colSite = T["report.col.site"];
string colFiles = T["report.stat.files"];
string colTotalMb = T["report.col.total_size_mb"];
string colVerMb = T["report.col.version_size_mb"];
string colLastMod = T["report.col.last_modified"];
sb.Append(colLibrary).Append(',')
.Append(colSite).Append(',')
.Append(colFiles).Append(',')
.Append(colTotalMb).Append(',')
.Append(colVerMb).Append(',')
.AppendLine(colLastMod);
foreach (var node in nodes)
{
AppendCsvField(sb, node.Name).Append(',');
AppendCsvField(sb, node.SiteTitle).Append(',');
sb.Append(node.TotalFileCount).Append(',');
AppendMb(sb, node.TotalSizeBytes).Append(',');
AppendMb(sb, node.VersionSizeBytes).Append(',');
if (node.LastModified.HasValue)
AppendCsvField(sb, node.LastModified.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture));
sb.AppendLine();
}
if (fileTypeMetrics.Count > 0)
{
string colFileType = T["report.col.file_type"];
string colSizeMb = T["report.col.size_mb"];
string colFileCnt = T["report.col.file_count"];
string noExtLabel = T["report.text.no_extension"];
sb.AppendLine();
sb.Append(colFileType).Append(',')
.Append(colSizeMb).Append(',')
.AppendLine(colFileCnt);
foreach (var m in fileTypeMetrics)
{
string label = string.IsNullOrEmpty(m.Extension) ? noExtLabel : m.Extension;
AppendCsvField(sb, label).Append(',');
AppendMb(sb, m.TotalSizeBytes).Append(',');
sb.Append(m.FileCount).AppendLine();
}
}
}
/// <summary>Writes the two-section CSV (libraries + file-type breakdown) with UTF-8 BOM.</summary>
public async Task WriteAsync(IReadOnlyList<StorageNode> nodes, IReadOnlyList<FileTypeMetric> fileTypeMetrics, string filePath, CancellationToken ct)
{
var sb = new StringBuilder(192 + nodes.Count * 100 + fileTypeMetrics.Count * 40);
WriteCsv(sb, nodes, fileTypeMetrics);
await ExportFileWriter.WriteCsvChunksAsync(filePath, sb, ct);
}
/// <summary>
/// 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.
/// </summary>
public Task WriteAsync(
IReadOnlyList<StorageNode> nodes,
IReadOnlyList<FileTypeMetric> fileTypeMetrics,
string basePath,
ReportSplitMode splitMode,
CancellationToken ct)
=> ReportSplitHelper.WritePartitionedAsync(
nodes, basePath, splitMode,
PartitionBySite,
(part, path, c) => WriteAsync(part, fileTypeMetrics, path, c),
ct);
/// <summary>
/// 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.
/// </summary>
internal static IEnumerable<(string Label, IReadOnlyList<StorageNode> Partition)> PartitionBySite(
IReadOnlyList<StorageNode> nodes)
{
var buckets = new Dictionary<string, List<StorageNode>>(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<StorageNode>();
buckets[currentSite] = list;
}
list.Add(node);
}
return buckets.Select(kv => (kv.Key, (IReadOnlyList<StorageNode>)kv.Value));
}
// ── Helpers ───────────────────────────────────────────────────────────────
private static StringBuilder AppendMb(StringBuilder sb, long bytes)
=> sb.Append((bytes / (1024.0 * 1024.0)).ToString("F2", CultureInfo.InvariantCulture));
private static StringBuilder AppendCsvField(StringBuilder sb, string value)
=> sb.Append(CsvSanitizer.EscapeMinimal(value));
/// <summary>
/// Pre-resolves localized labels for every <see cref="StorageNodeKind"/>
/// once per export, indexed by the enum's int value. Avoids a
/// <c>ResourceManager.GetString</c> call per row in hot CSV loops.
/// </summary>
private static string[] BuildKindLabelCache()
{
var values = (StorageNodeKind[])Enum.GetValues(typeof(StorageNodeKind));
int max = 0;
foreach (var v in values) { int i = (int)v; if (i > max) max = i; }
var cache = new string[max + 1];
for (int i = 0; i < cache.Length; i++) cache[i] = ((StorageNodeKind)i).ToString();
foreach (var v in values) cache[(int)v] = KindLabel(v);
return cache;
}
private static string KindLabel(StorageNodeKind kind)
{
var T = TranslationSource.Instance;
return kind switch
{
StorageNodeKind.Library => T["stor.kind.library"],
StorageNodeKind.HiddenLibrary => T["stor.kind.hidden"],
StorageNodeKind.PreservationHold => T["stor.kind.preservation"],
StorageNodeKind.ListAttachments => T["stor.kind.attachments"],
StorageNodeKind.RecycleBin => T["stor.kind.recyclebin"],
StorageNodeKind.Subsite => T["stor.kind.subsite"],
_ => kind.ToString()
};
}
}