Files
Dev fe19249f82 feat(08-04): add simplified export overloads to CsvExportService
- Add BuildCsv(IReadOnlyList<SimplifiedPermissionEntry>) overload with SimplifiedLabels and RiskLevel columns
- Add WriteAsync overload for simplified entries
- Original PermissionEntry methods unchanged

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 14:12:18 +02:00

122 lines
5.0 KiB
C#

using System.IO;
using System.Text;
using SharepointToolbox.Core.Models;
namespace SharepointToolbox.Services.Export;
/// <summary>
/// Exports permission entries to CSV format.
/// Ports PowerShell Merge-PermissionRows + Export-Csv functionality.
/// </summary>
public class CsvExportService
{
private const string Header =
"\"Object\",\"Title\",\"URL\",\"HasUniquePermissions\",\"Users\",\"UserLogins\",\"Type\",\"Permissions\",\"GrantedThrough\"";
/// <summary>
/// Builds a CSV string from the supplied permission entries.
/// Merges rows with identical (Users, PermissionLevels, GrantedThrough) by pipe-joining URLs and Titles.
/// </summary>
public string BuildCsv(IReadOnlyList<PermissionEntry> entries)
{
var sb = new StringBuilder();
sb.AppendLine(Header);
// Merge: group by (Users, PermissionLevels, GrantedThrough) — port of PS Merge-PermissionRows
var merged = entries
.GroupBy(e => (e.Users, e.PermissionLevels, e.GrantedThrough))
.Select(g => new
{
ObjectType = g.First().ObjectType,
Title = string.Join(" | ", g.Select(e => e.Title).Distinct()),
Url = string.Join(" | ", g.Select(e => e.Url).Distinct()),
HasUnique = g.First().HasUniquePermissions,
Users = g.Key.Users,
UserLogins = g.First().UserLogins,
PrincipalType = g.First().PrincipalType,
Permissions = g.Key.PermissionLevels,
GrantedThrough = g.Key.GrantedThrough
});
foreach (var row in merged)
sb.AppendLine(string.Join(",", new[]
{
Csv(row.ObjectType), Csv(row.Title), Csv(row.Url),
Csv(row.HasUnique.ToString()), Csv(row.Users), Csv(row.UserLogins),
Csv(row.PrincipalType), Csv(row.Permissions), Csv(row.GrantedThrough)
}));
return sb.ToString();
}
/// <summary>
/// Writes the CSV to the specified file path using UTF-8 with BOM (for Excel compatibility).
/// </summary>
public async Task WriteAsync(IReadOnlyList<PermissionEntry> entries, string filePath, CancellationToken ct)
{
var csv = BuildCsv(entries);
await File.WriteAllTextAsync(filePath, csv, new UTF8Encoding(encoderShouldEmitUTF8Identifier: true), ct);
}
/// <summary>
/// Header for simplified CSV export — includes "SimplifiedLabels" and "RiskLevel" columns.
/// </summary>
private const string SimplifiedHeader =
"\"Object\",\"Title\",\"URL\",\"HasUniquePermissions\",\"Users\",\"UserLogins\",\"Type\",\"Permissions\",\"SimplifiedLabels\",\"RiskLevel\",\"GrantedThrough\"";
/// <summary>
/// Builds a CSV string from simplified permission entries.
/// Includes SimplifiedLabels and RiskLevel columns after raw Permissions.
/// Uses the same merge logic as the standard BuildCsv.
/// </summary>
public string BuildCsv(IReadOnlyList<SimplifiedPermissionEntry> entries)
{
var sb = new StringBuilder();
sb.AppendLine(SimplifiedHeader);
var merged = entries
.GroupBy(e => (e.Users, e.PermissionLevels, e.GrantedThrough))
.Select(g => new
{
ObjectType = g.First().ObjectType,
Title = string.Join(" | ", g.Select(e => e.Title).Distinct()),
Url = string.Join(" | ", g.Select(e => e.Url).Distinct()),
HasUnique = g.First().HasUniquePermissions,
Users = g.Key.Users,
UserLogins = g.First().UserLogins,
PrincipalType = g.First().PrincipalType,
Permissions = g.Key.PermissionLevels,
SimplifiedLabels = g.First().SimplifiedLabels,
RiskLevel = g.First().RiskLevel.ToString(),
GrantedThrough = g.Key.GrantedThrough
});
foreach (var row in merged)
sb.AppendLine(string.Join(",", new[]
{
Csv(row.ObjectType), Csv(row.Title), Csv(row.Url),
Csv(row.HasUnique.ToString()), Csv(row.Users), Csv(row.UserLogins),
Csv(row.PrincipalType), Csv(row.Permissions), Csv(row.SimplifiedLabels),
Csv(row.RiskLevel), Csv(row.GrantedThrough)
}));
return sb.ToString();
}
/// <summary>
/// Writes simplified CSV to the specified file path.
/// </summary>
public async Task WriteAsync(IReadOnlyList<SimplifiedPermissionEntry> entries, string filePath, CancellationToken ct)
{
var csv = BuildCsv(entries);
await File.WriteAllTextAsync(filePath, csv, new UTF8Encoding(encoderShouldEmitUTF8Identifier: true), ct);
}
/// <summary>RFC 4180 CSV field escaping: wrap in double quotes, double internal quotes.</summary>
private static string Csv(string value)
{
if (string.IsNullOrEmpty(value)) return "\"\"";
return $"\"{value.Replace("\"", "\"\"")}\"";
}
}