diff --git a/SharepointToolbox/Services/StorageService.cs b/SharepointToolbox/Services/StorageService.cs
new file mode 100644
index 0000000..402d6c9
--- /dev/null
+++ b/SharepointToolbox/Services/StorageService.cs
@@ -0,0 +1,156 @@
+using Microsoft.SharePoint.Client;
+using SharepointToolbox.Core.Helpers;
+using SharepointToolbox.Core.Models;
+
+namespace SharepointToolbox.Services;
+
+///
+/// CSOM-based storage metrics scanner.
+/// Port of PowerShell Collect-FolderStorage / Get-PnPFolderStorageMetric pattern.
+///
+public class StorageService : IStorageService
+{
+ public async Task> CollectStorageAsync(
+ ClientContext ctx,
+ StorageScanOptions options,
+ IProgress 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();
+ 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 LoadFolderNodeAsync(
+ ClientContext ctx,
+ string serverRelativeUrl,
+ string name,
+ string siteTitle,
+ string library,
+ int indentLevel,
+ IProgress 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()
+ };
+ }
+
+ private static async Task CollectSubfoldersAsync(
+ ClientContext ctx,
+ string parentServerRelativeUrl,
+ StorageNode parentNode,
+ int currentDepth,
+ int maxDepth,
+ string siteTitle,
+ string library,
+ IProgress 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);
+ }
+ }
+}