Initial commit

This commit is contained in:
2026-06-02 10:51:14 +02:00
committed by kawa
commit d19092c84e
182 changed files with 13757 additions and 0 deletions
+227
View File
@@ -0,0 +1,227 @@
using Microsoft.SharePoint.Client;
using PnP.Framework.Sites;
using Serilog;
using SharepointToolbox.Web.Core.Helpers;
using SharepointToolbox.Web.Core.Models;
using SharepointToolbox.Web.Services.Audit;
using ModelSiteTemplate = SharepointToolbox.Web.Core.Models.SiteTemplate;
using SpWeb = Microsoft.SharePoint.Client.Web;
namespace SharepointToolbox.Web.Services;
public class TemplateService : ITemplateService
{
private readonly IAuditService _audit;
public TemplateService(IAuditService audit) { _audit = audit; }
private static readonly HashSet<string> SystemListNames = new(StringComparer.OrdinalIgnoreCase)
{
"Style Library","Form Templates","Site Assets","Site Pages","Composed Looks",
"Master Page Gallery","Web Part Gallery","Theme Gallery","Solution Gallery",
"List Template Gallery","Converted Forms","Customized Reports",
"Content type publishing error log","TaxonomyHiddenList","appdata","appfiles"
};
public async Task<ModelSiteTemplate> CaptureTemplateAsync(
ClientContext ctx, SiteTemplateOptions options,
IProgress<OperationProgress> progress, CancellationToken ct)
{
progress.Report(new OperationProgress(0, 0, "Loading site properties..."));
var web = ctx.Web;
ctx.Load(web, w => w.Title, w => w.Description, w => w.Language,
w => w.SiteLogoUrl, w => w.WebTemplate, w => w.Configuration, w => w.ServerRelativeUrl);
await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(ctx, progress, ct);
var siteType = (web.WebTemplate == "GROUP" || web.WebTemplate == "GROUP#0") ? "Team" : "Communication";
var template = new ModelSiteTemplate
{
Name = string.Empty, SourceUrl = ctx.Url, CapturedAt = DateTime.UtcNow,
SiteType = siteType, Options = options,
};
if (options.CaptureSettings)
template.Settings = new TemplateSettings { Title = web.Title, Description = web.Description, Language = (int)web.Language };
if (options.CaptureLogo)
template.Logo = new TemplateLogo { LogoUrl = web.SiteLogoUrl ?? string.Empty };
if (options.CaptureLibraries || options.CaptureFolders)
{
progress.Report(new OperationProgress(0, 0, "Enumerating libraries..."));
var lists = ctx.LoadQuery(web.Lists
.Include(l => l.Title, l => l.Hidden, l => l.BaseType, l => l.BaseTemplate, l => l.RootFolder)
.Where(l => !l.Hidden));
await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(ctx, progress, ct);
var filtered = lists.Where(l => !SystemListNames.Contains(l.Title))
.Where(l => l.BaseType == BaseType.DocumentLibrary || l.BaseType == BaseType.GenericList).ToList();
for (int i = 0; i < filtered.Count; i++)
{
ct.ThrowIfCancellationRequested();
var list = filtered[i];
progress.Report(new OperationProgress(i + 1, filtered.Count, $"Capturing library: {list.Title}"));
var libInfo = new TemplateLibraryInfo { Name = list.Title, BaseType = list.BaseType.ToString(), BaseTemplate = (int)list.BaseTemplate };
if (options.CaptureFolders)
{
ctx.Load(list.RootFolder, f => f.ServerRelativeUrl);
await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(ctx, progress, ct);
libInfo.Folders = await EnumerateLibraryFoldersAsync(ctx, list, ct);
}
template.Libraries.Add(libInfo);
}
}
if (options.CapturePermissionGroups)
{
progress.Report(new OperationProgress(0, 0, "Capturing permission groups..."));
var groups = web.SiteGroups;
ctx.Load(groups, gs => gs.Include(g => g.Title, g => g.Description));
await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(ctx, progress, ct);
var roleAssignments = web.RoleAssignments;
ctx.Load(roleAssignments, ras => ras.Include(ra => ra.Member.Title, ra => ra.RoleDefinitionBindings.Include(rd => rd.Name)));
await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(ctx, progress, ct);
foreach (var group in groups)
{
ct.ThrowIfCancellationRequested();
var roles = roleAssignments.Where(ra => ra.Member.Title == group.Title)
.SelectMany(ra => ra.RoleDefinitionBindings.Select(rd => rd.Name)).ToList();
template.PermissionGroups.Add(new TemplatePermissionGroup { Name = group.Title, Description = group.Description ?? string.Empty, RoleDefinitions = roles });
}
}
progress.Report(new OperationProgress(1, 1, "Template capture complete."));
return template;
}
public async Task<string> ApplyTemplateAsync(
ClientContext adminCtx, ModelSiteTemplate template,
string newSiteTitle, string newSiteAlias,
IProgress<OperationProgress> progress, CancellationToken ct)
{
progress.Report(new OperationProgress(0, 0, $"Creating {template.SiteType} site: {newSiteTitle}..."));
string siteUrl;
if (template.SiteType.Equals("Team", StringComparison.OrdinalIgnoreCase))
{
var info = new TeamSiteCollectionCreationInformation { DisplayName = newSiteTitle, Alias = newSiteAlias, Description = template.Settings?.Description ?? string.Empty, IsPublic = false };
using var siteCtx = await adminCtx.CreateSiteAsync(info);
siteCtx.Load(siteCtx.Web, w => w.Url);
await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(siteCtx, progress, ct);
siteUrl = siteCtx.Web.Url;
}
else
{
var tenantHost = new Uri(adminCtx.Url).Host;
var info = new CommunicationSiteCollectionCreationInformation { Title = newSiteTitle, Url = $"https://{tenantHost}/sites/{newSiteAlias}", Description = template.Settings?.Description ?? string.Empty };
using var siteCtx = await adminCtx.CreateSiteAsync(info);
siteCtx.Load(siteCtx.Web, w => w.Url);
await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(siteCtx, progress, ct);
siteUrl = siteCtx.Web.Url;
}
var newCtx = new ClientContext(siteUrl) { Credentials = adminCtx.Credentials };
try
{
for (int i = 0; i < template.Libraries.Count; i++)
{
ct.ThrowIfCancellationRequested();
var lib = template.Libraries[i];
progress.Report(new OperationProgress(i + 1, template.Libraries.Count, $"Creating library: {lib.Name}"));
try
{
var listInfo = new ListCreationInformation { Title = lib.Name, TemplateType = lib.BaseTemplate };
var newList = newCtx.Web.Lists.Add(listInfo);
await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(newCtx, progress, ct);
if (lib.Folders.Count > 0) await CreateFoldersRecursiveAsync(newCtx, newList, lib.Folders, progress, ct);
}
catch (Exception ex) { Log.Warning("Failed to create library {Name}: {Error}", lib.Name, ex.Message); }
}
foreach (var group in template.PermissionGroups)
{
ct.ThrowIfCancellationRequested();
try
{
var groupInfo = new GroupCreationInformation { Title = group.Name, Description = group.Description };
var newGroup = newCtx.Web.SiteGroups.Add(groupInfo);
foreach (var roleName in group.RoleDefinitions)
{
try
{
var roleDef = newCtx.Web.RoleDefinitions.GetByName(roleName);
var bindings = new RoleDefinitionBindingCollection(newCtx) { roleDef };
newCtx.Web.RoleAssignments.Add(newGroup, bindings);
}
catch (Exception ex) { Log.Warning("Failed to assign role {Role}: {Error}", roleName, ex.Message); }
}
await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(newCtx, progress, ct);
}
catch (Exception ex) { Log.Warning("Failed to create group {Name}: {Error}", group.Name, ex.Message); }
}
}
finally { newCtx.Dispose(); }
progress.Report(new OperationProgress(1, 1, $"Template applied. Site: {siteUrl}"));
await _audit.LogAsync("ApplyTemplate", adminCtx.Url, new[] { siteUrl },
$"Template '{template.Name}' applied to new site '{newSiteTitle}'");
return siteUrl;
}
private static async Task<List<TemplateFolderInfo>> EnumerateLibraryFoldersAsync(ClientContext ctx, List list, CancellationToken ct)
{
var rootUrl = list.RootFolder.ServerRelativeUrl.TrimEnd('/');
var folders = new List<(string Relative, string Parent)>();
await foreach (var item in SharePointPaginationHelper.GetItemsInFolderAsync(
ctx, list, rootUrl, recursive: true,
viewFields: new[] { "FSObjType", "FileLeafRef", "FileRef", "FileDirRef" }, ct: ct))
{
if (item["FSObjType"]?.ToString() != "1") continue;
var name = item["FileLeafRef"]?.ToString() ?? string.Empty;
var fileRef = (item["FileRef"]?.ToString() ?? string.Empty).TrimEnd('/');
var dirRef = (item["FileDirRef"]?.ToString() ?? string.Empty).TrimEnd('/');
if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(fileRef)) continue;
if (name.StartsWith("_") || name.Equals("Forms", StringComparison.OrdinalIgnoreCase)) continue;
var rel = fileRef.StartsWith(rootUrl, StringComparison.OrdinalIgnoreCase) ? fileRef[rootUrl.Length..].TrimStart('/') : name;
var parentRel = dirRef.StartsWith(rootUrl, StringComparison.OrdinalIgnoreCase) ? dirRef[rootUrl.Length..].TrimStart('/') : string.Empty;
folders.Add((rel, parentRel));
}
var nodes = folders.ToDictionary(f => f.Relative,
f => new TemplateFolderInfo { Name = Path.GetFileName(f.Relative), RelativePath = f.Relative, Children = new List<TemplateFolderInfo>() },
StringComparer.OrdinalIgnoreCase);
var roots = new List<TemplateFolderInfo>();
foreach (var (rel, parent) in folders)
{
if (!nodes.TryGetValue(rel, out var node)) continue;
if (!string.IsNullOrEmpty(parent) && nodes.TryGetValue(parent, out var p)) p.Children.Add(node);
else roots.Add(node);
}
return roots;
}
private static async Task CreateFoldersRecursiveAsync(ClientContext ctx, List list, List<TemplateFolderInfo> folders, IProgress<OperationProgress> progress, CancellationToken ct)
{
ctx.Load(list.RootFolder, f => f.ServerRelativeUrl);
await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(ctx, progress, ct);
await CreateSubFoldersRecursiveAsync(ctx, list.RootFolder.ServerRelativeUrl.TrimEnd('/'), folders, progress, ct);
}
private static async Task CreateSubFoldersRecursiveAsync(ClientContext ctx, string parentUrl, List<TemplateFolderInfo> folders, IProgress<OperationProgress> progress, CancellationToken ct)
{
foreach (var folder in folders)
{
ct.ThrowIfCancellationRequested();
try
{
ctx.Web.Folders.Add($"{parentUrl}/{folder.Name}");
await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(ctx, progress, ct);
if (folder.Children.Count > 0)
await CreateSubFoldersRecursiveAsync(ctx, $"{parentUrl}/{folder.Name}", folder.Children, progress, ct);
}
catch (Exception ex) { Log.Warning("Failed to create folder {Path}: {Error}", folder.RelativePath, ex.Message); }
}
}
}