Merge pull request 'Add report logos and configurable folder scan depth' (#2) from feat/report-logos-and-scan-depth into main
Reviewed-on: #2
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
using Microsoft.Graph;
|
||||
using Microsoft.Graph.Models;
|
||||
using Microsoft.Kiota.Abstractions;
|
||||
using SharepointToolbox.Web.Core.Models;
|
||||
using AppGraphClientFactory = SharepointToolbox.Web.Infrastructure.Auth.GraphClientFactory;
|
||||
|
||||
namespace SharepointToolbox.Web.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Delegated Graph implementation of <see cref="ISiteDiscoveryService"/>.
|
||||
/// Uses the <c>/sites?search=*</c> endpoint, paging through every result.
|
||||
/// Requires the delegated <c>Sites.Read.All</c> scope.
|
||||
/// </summary>
|
||||
public class SiteDiscoveryService : ISiteDiscoveryService
|
||||
{
|
||||
private readonly AppGraphClientFactory _graphClientFactory;
|
||||
|
||||
public SiteDiscoveryService(AppGraphClientFactory graphClientFactory)
|
||||
{
|
||||
_graphClientFactory = graphClientFactory;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<SiteInfo>> SearchSitesAsync(
|
||||
TenantProfile profile,
|
||||
string? query = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var graphClient = await _graphClientFactory.CreateClientAsync(profile);
|
||||
// "*" is the Graph convention for "return all sites".
|
||||
var search = string.IsNullOrWhiteSpace(query) ? "*" : query!;
|
||||
|
||||
// The typed Sites.GetAsync maps its Search property to OData "$search",
|
||||
// which routes "*" through KQL and fails ("'*' is not valid at position 0").
|
||||
// The all-sites wildcard only works via the bare, non-OData "search"
|
||||
// query parameter, so build the request manually.
|
||||
var requestInfo = new RequestInformation
|
||||
{
|
||||
HttpMethod = Method.GET,
|
||||
UrlTemplate = "{+baseurl}/sites{?search,%24top}",
|
||||
PathParameters = new Dictionary<string, object>
|
||||
{
|
||||
{ "baseurl", graphClient.RequestAdapter.BaseUrl ?? "https://graph.microsoft.com/v1.0" }
|
||||
},
|
||||
};
|
||||
requestInfo.QueryParameters.Add("search", search);
|
||||
requestInfo.QueryParameters.Add("%24top", 999);
|
||||
requestInfo.Headers.Add("Accept", "application/json");
|
||||
|
||||
var response = await graphClient.RequestAdapter.SendAsync<SiteCollectionResponse>(
|
||||
requestInfo, SiteCollectionResponse.CreateFromDiscriminatorValue, cancellationToken: ct);
|
||||
|
||||
if (response is null) return Array.Empty<SiteInfo>();
|
||||
|
||||
var results = new List<SiteInfo>();
|
||||
var iter = PageIterator<Site, SiteCollectionResponse>.CreatePageIterator(
|
||||
graphClient, response,
|
||||
site =>
|
||||
{
|
||||
if (ct.IsCancellationRequested) return false;
|
||||
var url = site.WebUrl ?? string.Empty;
|
||||
if (string.IsNullOrEmpty(url)) return true;
|
||||
// Skip OneDrive personal sites — not useful for these scans.
|
||||
if (url.Contains("/personal/", StringComparison.OrdinalIgnoreCase)) return true;
|
||||
var title = site.DisplayName ?? site.Name ?? url;
|
||||
results.Add(new SiteInfo(url, title));
|
||||
return true;
|
||||
});
|
||||
await iter.IterateAsync(ct);
|
||||
|
||||
return results
|
||||
.GroupBy(s => s.Url, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(g => g.First())
|
||||
.OrderBy(s => s.Title, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user