using Microsoft.SharePoint.Client; using Serilog; using SharepointToolbox.Core.Helpers; using SharepointToolbox.Core.Models; namespace SharepointToolbox.Services; public class FolderStructureService : IFolderStructureService { public async Task> CreateFoldersAsync( ClientContext ctx, string libraryTitle, IReadOnlyList rows, IProgress 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); } /// /// Builds unique folder paths from CSV rows, sorted parent-first to ensure /// parent folders are created before children. /// internal static IReadOnlyList BuildUniquePaths(IReadOnlyList rows) { var paths = new HashSet(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(); } }