Initial commit
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using SharepointToolbox.Web.Core.Models;
|
||||
using SharepointToolbox.Web.Localization;
|
||||
|
||||
namespace SharepointToolbox.Web.Services.Export;
|
||||
|
||||
/// <summary>
|
||||
/// Exports permission entries to CSV format.
|
||||
/// Ports PowerShell Merge-PermissionRows + Export-Csv functionality.
|
||||
/// </summary>
|
||||
public class CsvExportService
|
||||
{
|
||||
private static string BuildHeader()
|
||||
{
|
||||
var T = TranslationSource.Instance;
|
||||
return $"\"{T["report.col.object"]}\",\"{T["report.col.title"]}\",\"{T["report.col.url"]}\",\"HasUniquePermissions\",\"Users\",\"UserLogins\",\"Type\",\"Permissions\",\"GrantedThrough\",\"TargetLabel\",\"TargetUrl\",\"SharingLinkType\"";
|
||||
}
|
||||
|
||||
/// <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(BuildHeader());
|
||||
|
||||
// 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,
|
||||
TargetLabel = g.First().TargetLabel ?? string.Empty,
|
||||
TargetUrl = g.First().TargetUrl ?? string.Empty,
|
||||
SharingLinkType = g.First().SharingLinkType ?? string.Empty
|
||||
});
|
||||
|
||||
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),
|
||||
Csv(row.TargetLabel), Csv(row.TargetUrl), Csv(row.SharingLinkType)
|
||||
}));
|
||||
|
||||
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 ExportFileWriter.WriteCsvAsync(filePath, csv, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Header for simplified CSV export — includes "SimplifiedLabels" and "RiskLevel" columns.
|
||||
/// </summary>
|
||||
private static string BuildSimplifiedHeader()
|
||||
{
|
||||
var T = TranslationSource.Instance;
|
||||
return $"\"{T["report.col.object"]}\",\"{T["report.col.title"]}\",\"{T["report.col.url"]}\",\"HasUniquePermissions\",\"Users\",\"UserLogins\",\"Type\",\"Permissions\",\"SimplifiedLabels\",\"RiskLevel\",\"GrantedThrough\",\"TargetLabel\",\"TargetUrl\",\"SharingLinkType\"";
|
||||
}
|
||||
|
||||
/// <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(BuildSimplifiedHeader());
|
||||
|
||||
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,
|
||||
TargetLabel = g.First().TargetLabel ?? string.Empty,
|
||||
TargetUrl = g.First().TargetUrl ?? string.Empty,
|
||||
SharingLinkType = g.First().SharingLinkType ?? string.Empty
|
||||
});
|
||||
|
||||
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),
|
||||
Csv(row.TargetLabel), Csv(row.TargetUrl), Csv(row.SharingLinkType)
|
||||
}));
|
||||
|
||||
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 ExportFileWriter.WriteCsvAsync(filePath, csv, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes permission entries with optional per-site partitioning.
|
||||
/// Single → writes one file at <paramref name="basePath"/>.
|
||||
/// BySite → one file per site-collection URL, suffixed on the base path.
|
||||
/// </summary>
|
||||
public Task WriteAsync(
|
||||
IReadOnlyList<PermissionEntry> entries,
|
||||
string basePath,
|
||||
ReportSplitMode splitMode,
|
||||
CancellationToken ct)
|
||||
=> ReportSplitHelper.WritePartitionedAsync(
|
||||
entries, basePath, splitMode,
|
||||
PartitionBySite,
|
||||
(part, path, c) => WriteAsync(part, path, c),
|
||||
ct);
|
||||
|
||||
/// <summary>Simplified-entry split variant.</summary>
|
||||
public Task WriteAsync(
|
||||
IReadOnlyList<SimplifiedPermissionEntry> entries,
|
||||
string basePath,
|
||||
ReportSplitMode splitMode,
|
||||
CancellationToken ct)
|
||||
=> ReportSplitHelper.WritePartitionedAsync(
|
||||
entries, basePath, splitMode,
|
||||
PartitionBySite,
|
||||
(part, path, c) => WriteAsync(part, path, c),
|
||||
ct);
|
||||
|
||||
internal static IEnumerable<(string Label, IReadOnlyList<PermissionEntry> Partition)> PartitionBySite(
|
||||
IReadOnlyList<PermissionEntry> entries)
|
||||
{
|
||||
return entries
|
||||
.GroupBy(e => ReportSplitHelper.DeriveSiteCollectionUrl(e.Url), StringComparer.OrdinalIgnoreCase)
|
||||
.Select(g => (
|
||||
Label: ReportSplitHelper.DeriveSiteLabel(g.Key),
|
||||
Partition: (IReadOnlyList<PermissionEntry>)g.ToList()));
|
||||
}
|
||||
|
||||
internal static IEnumerable<(string Label, IReadOnlyList<SimplifiedPermissionEntry> Partition)> PartitionBySite(
|
||||
IReadOnlyList<SimplifiedPermissionEntry> entries)
|
||||
{
|
||||
return entries
|
||||
.GroupBy(e => ReportSplitHelper.DeriveSiteCollectionUrl(e.Url), StringComparer.OrdinalIgnoreCase)
|
||||
.Select(g => (
|
||||
Label: ReportSplitHelper.DeriveSiteLabel(g.Key),
|
||||
Partition: (IReadOnlyList<SimplifiedPermissionEntry>)g.ToList()));
|
||||
}
|
||||
|
||||
/// <summary>RFC 4180 CSV field escaping with formula-injection guard.</summary>
|
||||
private static string Csv(string value) => CsvSanitizer.Escape(value);
|
||||
}
|
||||
Reference in New Issue
Block a user