Files
Sharepoint-Toolbox/SharepointToolbox/Services/Export/DuplicatesCsvExportService.cs
T
Dev 12dd1de9f2 chore: release v2.4
- Add theme system (Dark/Light palettes, ModernTheme, ThemeManager)
- Add InputDialog, Spinner common view
- Add DuplicatesCsvExportService
- Refresh views, dialogs, and view models across tabs
- Update localization strings (en/fr)
- Tweak services (transfer, permissions, search, user access, ownership elevation, bulk operations)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 10:50:03 +02:00

113 lines
4.0 KiB
C#

using System.IO;
using System.Text;
using SharepointToolbox.Core.Models;
using SharepointToolbox.Localization;
namespace SharepointToolbox.Services.Export;
/// <summary>
/// Exports DuplicateGroup list to CSV. Each duplicate item becomes one row;
/// the Group column ties copies together and a Copies column gives the group size.
/// Header row is built at write-time so culture switches are honoured.
/// </summary>
public class DuplicatesCsvExportService
{
/// <summary>Writes the CSV to <paramref name="filePath"/> with UTF-8 BOM (Excel-compatible).</summary>
public async Task WriteAsync(
IReadOnlyList<DuplicateGroup> groups,
string filePath,
CancellationToken ct)
{
var csv = BuildCsv(groups);
await ExportFileWriter.WriteCsvAsync(filePath, csv, ct);
}
/// <summary>
/// Writes one or more CSVs depending on <paramref name="splitMode"/>.
/// Single → <paramref name="basePath"/> as-is. BySite → one file per site,
/// filenames derived from <paramref name="basePath"/> with a site suffix.
/// </summary>
public Task WriteAsync(
IReadOnlyList<DuplicateGroup> groups,
string basePath,
ReportSplitMode splitMode,
CancellationToken ct)
=> ReportSplitHelper.WritePartitionedAsync(
groups, basePath, splitMode,
PartitionBySite,
(part, path, c) => WriteAsync(part, path, c),
ct);
internal static IEnumerable<(string Label, IReadOnlyList<DuplicateGroup> Partition)> PartitionBySite(
IReadOnlyList<DuplicateGroup> groups)
{
return groups
.GroupBy(g =>
{
var first = g.Items.FirstOrDefault();
return (Url: first?.SiteUrl ?? string.Empty, Title: first?.SiteTitle ?? string.Empty);
})
.Select(g => (
Label: ReportSplitHelper.DeriveSiteLabel(g.Key.Url, g.Key.Title),
Partition: (IReadOnlyList<DuplicateGroup>)g.ToList()));
}
/// <summary>
/// Builds the CSV payload. Emits a header summary (group count, generated
/// timestamp), then one row per duplicate item with its group index and
/// group size. CSV fields are escaped via <see cref="CsvSanitizer.Escape"/>.
/// </summary>
public string BuildCsv(IReadOnlyList<DuplicateGroup> groups)
{
var T = TranslationSource.Instance;
var sb = new StringBuilder();
// Summary
sb.AppendLine($"\"{T["report.title.duplicates_short"]}\"");
sb.AppendLine($"\"{T["report.text.duplicate_groups_found"]}\",\"{groups.Count}\"");
sb.AppendLine($"\"{T["report.text.generated"]}\",\"{DateTime.Now:yyyy-MM-dd HH:mm:ss}\"");
sb.AppendLine();
// Header
sb.AppendLine(string.Join(",", new[]
{
Csv(T["report.col.number"]),
Csv(T["report.col.group"]),
Csv(T["report.text.copies"]),
Csv(T["report.col.site"]),
Csv(T["report.col.name"]),
Csv(T["report.col.library"]),
Csv(T["report.col.path"]),
Csv(T["report.col.size_bytes"]),
Csv(T["report.col.created"]),
Csv(T["report.col.modified"]),
}));
foreach (var g in groups)
{
int i = 0;
foreach (var item in g.Items)
{
i++;
sb.AppendLine(string.Join(",", new[]
{
Csv(i.ToString()),
Csv(g.Name),
Csv(g.Items.Count.ToString()),
Csv(item.SiteTitle),
Csv(item.Name),
Csv(item.Library),
Csv(item.Path),
Csv(item.SizeBytes?.ToString() ?? string.Empty),
Csv(item.Created?.ToString("yyyy-MM-dd") ?? string.Empty),
Csv(item.Modified?.ToString("yyyy-MM-dd") ?? string.Empty),
}));
}
}
return sb.ToString();
}
private static string Csv(string value) => CsvSanitizer.Escape(value);
}