Files
Sharepoint-Toolbox/SharepointToolbox/Services/SiteListService.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

91 lines
3.2 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Online.SharePoint.TenantAdministration;
using Microsoft.SharePoint.Client;
using SharepointToolbox.Core.Models;
namespace SharepointToolbox.Services;
public class SiteListService : ISiteListService
{
private readonly SessionManager _sessionManager;
public SiteListService(SessionManager sessionManager)
{
_sessionManager = sessionManager;
}
public async Task<IReadOnlyList<SiteInfo>> GetSitesAsync(
TenantProfile profile,
IProgress<OperationProgress> progress,
CancellationToken ct)
{
ct.ThrowIfCancellationRequested();
progress.Report(OperationProgress.Indeterminate("Loading sites..."));
// Obtain the already-authenticated context for the tenant URL, then clone it to
// the admin URL. Cloning reuses the existing token — no second interactive login.
ClientContext ctx;
try
{
ctx = await _sessionManager.GetOrCreateContextAsync(profile, ct);
}
catch (ServerException ex) when (ex.Message.Contains("Access denied", StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException(
"Site listing requires SharePoint administrator permissions. Connect with an admin account.", ex);
}
var adminUrl = DeriveAdminUrl(profile.TenantUrl);
using var adminCtx = ctx.Clone(adminUrl);
var results = new List<SiteInfo>();
var tenant = new Tenant(adminCtx);
SPOSitePropertiesEnumerable? batch = null;
// Paginate through all site collections using GetSitePropertiesFromSharePointByFilters.
// StartIndex = null on the first call; subsequent calls use NextStartIndexFromSharePoint.
while (batch == null || batch.NextStartIndexFromSharePoint != null)
{
ct.ThrowIfCancellationRequested();
var filter = new SPOSitePropertiesEnumerableFilter
{
IncludePersonalSite = PersonalSiteFilter.UseServerDefault,
StartIndex = batch?.NextStartIndexFromSharePoint,
IncludeDetail = true
};
batch = tenant.GetSitePropertiesFromSharePointByFilters(filter);
adminCtx.Load(batch);
await adminCtx.ExecuteQueryAsync();
foreach (var s in batch)
{
if (s.Status == "Active"
&& !s.Url.Contains("-my.sharepoint.com", StringComparison.OrdinalIgnoreCase))
{
results.Add(new SiteInfo(s.Url, s.Title)
{
StorageUsedMb = s.StorageUsage,
StorageQuotaMb = s.StorageMaximumLevel,
Template = s.Template ?? string.Empty
});
}
}
}
return results.OrderBy(s => s.Url).ToList();
}
internal static string DeriveAdminUrl(string tenantUrl)
=> Regex.Replace(tenantUrl.TrimEnd('/'),
@"(https://[^.]+)(\.sharepoint\.com)",
"$1-admin$2",
RegexOptions.IgnoreCase);
}