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
+127
View File
@@ -0,0 +1,127 @@
using Microsoft.SharePoint.Client;
using Microsoft.SharePoint.Client.Search.Query;
using Serilog;
using SharepointToolbox.Web.Core.Helpers;
using SharepointToolbox.Web.Core.Models;
namespace SharepointToolbox.Web.Services;
public class SystemGroupTargetResolver : ISystemGroupTargetResolver
{
private readonly Dictionary<string, SystemGroupTarget?> _cache = new(StringComparer.OrdinalIgnoreCase);
public async Task<SystemGroupTarget?> ResolveAsync(
ClientContext ctx, SystemGroupClassification classification, CancellationToken ct)
{
ct.ThrowIfCancellationRequested();
var key = BuildCacheKey(ctx.Url, classification);
if (key is not null && _cache.TryGetValue(key, out var cached)) return cached;
SystemGroupTarget? result = null;
try
{
result = classification.Kind switch
{
SystemGroupKind.LimitedAccessWeb => await ResolveWebAsync(ctx, classification.WebId!.Value, ct),
SystemGroupKind.LimitedAccessList => await ResolveListAsync(ctx, classification.ListId!.Value, ct),
SystemGroupKind.SharingLink => await ResolveItemAsync(ctx, classification.ItemUniqueId!.Value, classification.LinkType, ct),
_ => null
};
}
catch (OperationCanceledException) { throw; }
catch (Exception ex) { Log.Debug("System group resolve failed for {Kind}: {Error}", classification.Kind, ex.Message); }
if (key is not null) _cache[key] = result;
return result;
}
private static string? BuildCacheKey(string siteUrl, SystemGroupClassification c) => c.Kind switch
{
SystemGroupKind.LimitedAccessWeb => $"{siteUrl}|web|{c.WebId}",
SystemGroupKind.LimitedAccessList => $"{siteUrl}|list|{c.ListId}",
SystemGroupKind.SharingLink => $"{siteUrl}|item|{c.ItemUniqueId}",
_ => null
};
private static async Task<SystemGroupTarget?> ResolveWebAsync(ClientContext ctx, Guid webId, CancellationToken ct)
{
var web = ctx.Site.OpenWebById(webId);
ctx.Load(web, w => w.Title, w => w.Url);
await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(ctx, null, ct);
return new SystemGroupTarget(SystemGroupKind.LimitedAccessWeb, web.Title, web.Url);
}
private static async Task<SystemGroupTarget?> ResolveListAsync(ClientContext ctx, Guid listId, CancellationToken ct)
{
var list = ctx.Web.Lists.GetById(listId);
ctx.Load(list, l => l.Title, l => l.DefaultViewUrl);
await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(ctx, null, ct);
return new SystemGroupTarget(SystemGroupKind.LimitedAccessList, list.Title, BuildAbsoluteUrl(ctx.Url, list.DefaultViewUrl));
}
private static async Task<SystemGroupTarget?> ResolveItemAsync(ClientContext ctx, Guid itemUniqueId, string? linkType, CancellationToken ct)
{
try
{
var file = ctx.Web.GetFileById(itemUniqueId);
ctx.Load(file, f => f.Name, f => f.ServerRelativeUrl, f => f.Exists);
await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(ctx, null, ct);
if (file.Exists) return new SystemGroupTarget(SystemGroupKind.SharingLink, file.Name, BuildAbsoluteUrl(ctx.Url, file.ServerRelativeUrl), linkType);
}
catch (ServerException ex) { Log.Debug("File by ID not found: {Error}", ex.Message); }
try
{
var folder = ctx.Web.GetFolderById(itemUniqueId);
ctx.Load(folder, f => f.Name, f => f.ServerRelativeUrl);
await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(ctx, null, ct);
return new SystemGroupTarget(SystemGroupKind.SharingLink, folder.Name, BuildAbsoluteUrl(ctx.Url, folder.ServerRelativeUrl), linkType);
}
catch (ServerException ex) { Log.Debug("Folder by ID not found: {Error}", ex.Message); }
return await TryResolveViaSearchAsync(ctx, itemUniqueId, linkType, ct);
}
private static async Task<SystemGroupTarget?> TryResolveViaSearchAsync(ClientContext ctx, Guid itemUniqueId, string? linkType, CancellationToken ct)
{
ct.ThrowIfCancellationRequested();
try
{
var kq = new KeywordQuery(ctx) { QueryText = $"UniqueId:{{{itemUniqueId}}}", RowLimit = 1, TrimDuplicates = false };
kq.SelectProperties.Add("Path"); kq.SelectProperties.Add("Title");
var executor = new SearchExecutor(ctx);
var result = executor.ExecuteQuery(kq);
await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(ctx, null, ct);
var table = result.Value.FirstOrDefault(t => t.TableType == KnownTableTypes.RelevantResults);
if (table is null || table.RowCount == 0) return null;
var row = ToDict(table.ResultRows.First());
var path = row.TryGetValue("Path", out var p) ? p?.ToString() : null;
var title = row.TryGetValue("Title", out var t) ? t?.ToString() : null;
if (string.IsNullOrEmpty(path)) return null;
var leaf = !string.IsNullOrWhiteSpace(title) ? title! : Uri.UnescapeDataString(path.TrimEnd('/').Split('/').Last());
return new SystemGroupTarget(SystemGroupKind.SharingLink, $"{leaf} (via index)", path, linkType);
}
catch (OperationCanceledException) { throw; }
catch (Exception ex) { Log.Debug("UniqueId search fallback failed: {Error}", ex.Message); return null; }
}
private static IDictionary<string, object> ToDict(object rawRow)
{
if (rawRow is IDictionary<string, object> generic) return generic;
var dict = new Dictionary<string, object>();
if (rawRow is System.Collections.IDictionary legacy)
foreach (System.Collections.DictionaryEntry e in legacy)
dict[e.Key.ToString()!] = e.Value ?? string.Empty;
return dict;
}
private static string BuildAbsoluteUrl(string contextUrl, string? serverRelative)
{
if (string.IsNullOrEmpty(serverRelative)) return contextUrl;
if (Uri.TryCreate(serverRelative, UriKind.Absolute, out _)) return serverRelative;
var uri = new Uri(contextUrl);
return $"{uri.Scheme}://{uri.Host}{serverRelative}";
}
}