feat(03-02): implement StorageService CSOM StorageMetrics scan engine
- Add StorageService implementing IStorageService - Load Folder.StorageMetrics, TimeLastModified, Name, ServerRelativeUrl in one CSOM round-trip per folder - CollectStorageAsync returns one StorageNode per document library at IndentLevel=0 - With FolderDepth>0, CollectSubfoldersAsync recurses into child folders - All CSOM calls use ExecuteQueryRetryHelper.ExecuteQueryRetryAsync (3 call sites) - System/hidden lists skipped (Hidden=true or BaseType != DocumentLibrary) - Forms/ and _-prefixed system folders skipped during subfolder recursion - ct.ThrowIfCancellationRequested() called at top of every recursive step
This commit is contained in:
156
SharepointToolbox/Services/StorageService.cs
Normal file
156
SharepointToolbox/Services/StorageService.cs
Normal file
@@ -0,0 +1,156 @@
|
||||
using Microsoft.SharePoint.Client;
|
||||
using SharepointToolbox.Core.Helpers;
|
||||
using SharepointToolbox.Core.Models;
|
||||
|
||||
namespace SharepointToolbox.Services;
|
||||
|
||||
/// <summary>
|
||||
/// CSOM-based storage metrics scanner.
|
||||
/// Port of PowerShell Collect-FolderStorage / Get-PnPFolderStorageMetric pattern.
|
||||
/// </summary>
|
||||
public class StorageService : IStorageService
|
||||
{
|
||||
public async Task<IReadOnlyList<StorageNode>> CollectStorageAsync(
|
||||
ClientContext ctx,
|
||||
StorageScanOptions options,
|
||||
IProgress<OperationProgress> progress,
|
||||
CancellationToken ct)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
// Load web-level metadata in one round-trip
|
||||
ctx.Load(ctx.Web,
|
||||
w => w.Title,
|
||||
w => w.Url,
|
||||
w => w.ServerRelativeUrl,
|
||||
w => w.Lists.Include(
|
||||
l => l.Title,
|
||||
l => l.Hidden,
|
||||
l => l.BaseType,
|
||||
l => l.RootFolder.ServerRelativeUrl));
|
||||
await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(ctx, progress, ct);
|
||||
|
||||
string webSrl = ctx.Web.ServerRelativeUrl.TrimEnd('/');
|
||||
string siteTitle = ctx.Web.Title;
|
||||
|
||||
var result = new List<StorageNode>();
|
||||
var libs = ctx.Web.Lists
|
||||
.Where(l => !l.Hidden && l.BaseType == BaseType.DocumentLibrary)
|
||||
.ToList();
|
||||
|
||||
int idx = 0;
|
||||
foreach (var lib in libs)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
idx++;
|
||||
progress.Report(new OperationProgress(idx, libs.Count,
|
||||
$"Loading storage metrics: {lib.Title} ({idx}/{libs.Count})"));
|
||||
|
||||
var libNode = await LoadFolderNodeAsync(
|
||||
ctx, lib.RootFolder.ServerRelativeUrl, lib.Title,
|
||||
siteTitle, lib.Title, 0, progress, ct);
|
||||
|
||||
if (options.FolderDepth > 0)
|
||||
{
|
||||
await CollectSubfoldersAsync(
|
||||
ctx, lib.RootFolder.ServerRelativeUrl,
|
||||
libNode, 1, options.FolderDepth,
|
||||
siteTitle, lib.Title, progress, ct);
|
||||
}
|
||||
|
||||
result.Add(libNode);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// -- Private helpers -----------------------------------------------------
|
||||
|
||||
private static async Task<StorageNode> LoadFolderNodeAsync(
|
||||
ClientContext ctx,
|
||||
string serverRelativeUrl,
|
||||
string name,
|
||||
string siteTitle,
|
||||
string library,
|
||||
int indentLevel,
|
||||
IProgress<OperationProgress> progress,
|
||||
CancellationToken ct)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
Folder folder = ctx.Web.GetFolderByServerRelativeUrl(serverRelativeUrl);
|
||||
ctx.Load(folder,
|
||||
f => f.StorageMetrics,
|
||||
f => f.TimeLastModified,
|
||||
f => f.ServerRelativeUrl,
|
||||
f => f.Name);
|
||||
await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(ctx, progress, ct);
|
||||
|
||||
DateTime? lastMod = folder.StorageMetrics.LastModified > DateTime.MinValue
|
||||
? folder.StorageMetrics.LastModified
|
||||
: folder.TimeLastModified > DateTime.MinValue
|
||||
? folder.TimeLastModified
|
||||
: (DateTime?)null;
|
||||
|
||||
return new StorageNode
|
||||
{
|
||||
Name = name,
|
||||
Url = ctx.Url.TrimEnd('/') + serverRelativeUrl,
|
||||
SiteTitle = siteTitle,
|
||||
Library = library,
|
||||
TotalSizeBytes = folder.StorageMetrics.TotalSize,
|
||||
FileStreamSizeBytes = folder.StorageMetrics.TotalFileStreamSize,
|
||||
TotalFileCount = folder.StorageMetrics.TotalFileCount,
|
||||
LastModified = lastMod,
|
||||
IndentLevel = indentLevel,
|
||||
Children = new List<StorageNode>()
|
||||
};
|
||||
}
|
||||
|
||||
private static async Task CollectSubfoldersAsync(
|
||||
ClientContext ctx,
|
||||
string parentServerRelativeUrl,
|
||||
StorageNode parentNode,
|
||||
int currentDepth,
|
||||
int maxDepth,
|
||||
string siteTitle,
|
||||
string library,
|
||||
IProgress<OperationProgress> progress,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (currentDepth > maxDepth) return;
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
// Load direct child folders of this folder
|
||||
Folder parentFolder = ctx.Web.GetFolderByServerRelativeUrl(parentServerRelativeUrl);
|
||||
ctx.Load(parentFolder,
|
||||
f => f.Folders.Include(
|
||||
sf => sf.Name,
|
||||
sf => sf.ServerRelativeUrl));
|
||||
await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(ctx, progress, ct);
|
||||
|
||||
foreach (Folder subFolder in parentFolder.Folders)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
// Skip SharePoint system folders
|
||||
if (subFolder.Name.Equals("Forms", StringComparison.OrdinalIgnoreCase) ||
|
||||
subFolder.Name.StartsWith("_", StringComparison.Ordinal))
|
||||
continue;
|
||||
|
||||
var childNode = await LoadFolderNodeAsync(
|
||||
ctx, subFolder.ServerRelativeUrl, subFolder.Name,
|
||||
siteTitle, library, currentDepth, progress, ct);
|
||||
|
||||
if (currentDepth < maxDepth)
|
||||
{
|
||||
await CollectSubfoldersAsync(
|
||||
ctx, subFolder.ServerRelativeUrl, childNode,
|
||||
currentDepth + 1, maxDepth,
|
||||
siteTitle, library, progress, ct);
|
||||
}
|
||||
|
||||
parentNode.Children.Add(childNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user