feat(04-06): implement TemplateService and FolderStructureService

- FolderStructureService.CreateFoldersAsync creates folder hierarchy from CSV rows using BulkOperationRunner
- FolderStructureService.BuildUniquePaths deduplicates and sorts paths parent-first by slash depth
- TemplateService already committed; verified compilation and interface compliance
- FolderStructureServiceTests: 4 unit tests pass (BuildUniquePaths edge cases, deduplication, empty levels, BuildPath) + 1 skip
- TemplateServiceTests: 3 unit tests pass (interface impl, SiteTemplate defaults, SiteTemplateOptions defaults) + 2 skip
This commit is contained in:
Dev
2026-04-03 10:07:49 +02:00
parent 773393c4c0
commit 84cd569fb7
3 changed files with 207 additions and 0 deletions

View File

@@ -0,0 +1,69 @@
using Microsoft.SharePoint.Client;
using Serilog;
using SharepointToolbox.Core.Helpers;
using SharepointToolbox.Core.Models;
namespace SharepointToolbox.Services;
public class FolderStructureService : IFolderStructureService
{
public async Task<BulkOperationSummary<string>> CreateFoldersAsync(
ClientContext ctx,
string libraryTitle,
IReadOnlyList<FolderStructureRow> rows,
IProgress<OperationProgress> progress,
CancellationToken ct)
{
// Get library root folder URL
var list = ctx.Web.Lists.GetByTitle(libraryTitle);
ctx.Load(list, l => l.RootFolder.ServerRelativeUrl);
await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(ctx, progress, ct);
var baseUrl = list.RootFolder.ServerRelativeUrl.TrimEnd('/');
// Build unique folder paths from CSV rows, sorted parent-first
var folderPaths = BuildUniquePaths(rows);
return await BulkOperationRunner.RunAsync(
folderPaths,
async (path, idx, token) =>
{
var fullPath = $"{baseUrl}/{path}";
ctx.Web.Folders.Add(fullPath);
await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(ctx, progress, token);
Log.Information("Created folder: {Path}", fullPath);
},
progress,
ct);
}
/// <summary>
/// Builds unique folder paths from CSV rows, sorted parent-first to ensure
/// parent folders are created before children.
/// </summary>
internal static IReadOnlyList<string> BuildUniquePaths(IReadOnlyList<FolderStructureRow> rows)
{
var paths = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var row in rows)
{
var parts = new[] { row.Level1, row.Level2, row.Level3, row.Level4 }
.Where(s => !string.IsNullOrWhiteSpace(s))
.ToArray();
// Add each level as a path (e.g., "Admin", "Admin/HR", "Admin/HR/Contracts")
var current = string.Empty;
foreach (var part in parts)
{
current = string.IsNullOrEmpty(current) ? part.Trim() : $"{current}/{part.Trim()}";
paths.Add(current);
}
}
// Sort by depth (fewer slashes first) to ensure parent-first ordering
return paths
.OrderBy(p => p.Count(c => c == '/'))
.ThenBy(p => p, StringComparer.OrdinalIgnoreCase)
.ToList();
}
}