Files
Sharepoint-Toolbox/.planning/phases/09-storage-visualization/09-02-PLAN.md
Dev a63a698282 docs(09-storage-visualization): create phase plan — 4 plans in 4 waves
Wave 1: LiveCharts2 NuGet + FileTypeMetric model + IStorageService extension
Wave 2: StorageService file-type enumeration implementation
Wave 3: ViewModel chart properties + View XAML + localization
Wave 4: Unit tests for chart ViewModel behavior

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 15:16:16 +02:00

243 lines
9.5 KiB
Markdown

---
phase: 09-storage-visualization
plan: 02
type: execute
wave: 2
depends_on:
- "09-01"
files_modified:
- SharepointToolbox/Services/StorageService.cs
autonomous: true
requirements:
- VIZZ-02
must_haves:
truths:
- "CollectFileTypeMetricsAsync enumerates files from all non-hidden document libraries"
- "Files are grouped by extension with summed size and count"
- "Results are sorted by TotalSizeBytes descending"
- "Existing CollectStorageAsync method is not modified"
artifacts:
- path: "SharepointToolbox/Services/StorageService.cs"
provides: "Implementation of CollectFileTypeMetricsAsync"
contains: "CollectFileTypeMetricsAsync"
key_links:
- from: "SharepointToolbox/Services/StorageService.cs"
to: "SharepointToolbox/Core/Models/FileTypeMetric.cs"
via: "Groups CSOM file data into FileTypeMetric records"
pattern: "new FileTypeMetric"
- from: "SharepointToolbox/Services/StorageService.cs"
to: "SharepointToolbox/Core/Helpers/ExecuteQueryRetryHelper.cs"
via: "Throttle-safe query execution"
pattern: "ExecuteQueryRetryHelper\\.ExecuteQueryRetryAsync"
---
<objective>
Implement CollectFileTypeMetricsAsync in StorageService -- enumerate files across all non-hidden document libraries using CSOM CamlQuery, aggregate by file extension, and return sorted FileTypeMetric list.
Purpose: Provides the data layer for chart visualization (VIZZ-02). The ViewModel will call this after the main storage scan completes.
Output: Updated StorageService.cs with CollectFileTypeMetricsAsync implementation
</objective>
<execution_context>
@C:/Users/dev/.claude/get-shit-done/workflows/execute-plan.md
@C:/Users/dev/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/09-storage-visualization/09-01-SUMMARY.md
<interfaces>
<!-- From Plan 09-01 -->
From SharepointToolbox/Core/Models/FileTypeMetric.cs:
```csharp
public record FileTypeMetric(string Extension, long TotalSizeBytes, int FileCount)
{
public string DisplayLabel => string.IsNullOrEmpty(Extension)
? "No Extension"
: Extension.TrimStart('.').ToUpperInvariant();
}
```
From SharepointToolbox/Services/IStorageService.cs:
```csharp
public interface IStorageService
{
Task<IReadOnlyList<StorageNode>> CollectStorageAsync(
ClientContext ctx, StorageScanOptions options,
IProgress<OperationProgress> progress, CancellationToken ct);
Task<IReadOnlyList<FileTypeMetric>> CollectFileTypeMetricsAsync(
ClientContext ctx,
IProgress<OperationProgress> progress,
CancellationToken ct);
}
```
From SharepointToolbox/Core/Helpers/ExecuteQueryRetryHelper.cs:
```csharp
public static class ExecuteQueryRetryHelper
{
public static async Task ExecuteQueryRetryAsync(
ClientContext ctx,
IProgress<OperationProgress>? progress = null,
CancellationToken ct = default);
}
```
<!-- Existing StorageService structure (DO NOT modify existing methods) -->
From SharepointToolbox/Services/StorageService.cs:
```csharp
public class StorageService : IStorageService
{
public async Task<IReadOnlyList<StorageNode>> CollectStorageAsync(...) { ... }
private static async Task<StorageNode> LoadFolderNodeAsync(...) { ... }
private static async Task CollectSubfoldersAsync(...) { ... }
}
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Implement CollectFileTypeMetricsAsync in StorageService</name>
<files>SharepointToolbox/Services/StorageService.cs</files>
<action>
Add the `CollectFileTypeMetricsAsync` method to the existing `StorageService` class. Do NOT modify any existing methods (CollectStorageAsync, LoadFolderNodeAsync, CollectSubfoldersAsync). Add the new method after the existing `CollectStorageAsync` method.
Add this method to the `StorageService` class:
```csharp
public async Task<IReadOnlyList<FileTypeMetric>> CollectFileTypeMetricsAsync(
ClientContext ctx,
IProgress<OperationProgress> progress,
CancellationToken ct)
{
ct.ThrowIfCancellationRequested();
// Load all non-hidden document libraries
ctx.Load(ctx.Web,
w => w.Lists.Include(
l => l.Title,
l => l.Hidden,
l => l.BaseType,
l => l.ItemCount));
await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(ctx, progress, ct);
var libs = ctx.Web.Lists
.Where(l => !l.Hidden && l.BaseType == BaseType.DocumentLibrary)
.ToList();
// Accumulate file sizes by extension across all libraries
var extensionMap = new Dictionary<string, (long totalSize, int count)>(StringComparer.OrdinalIgnoreCase);
int libIdx = 0;
foreach (var lib in libs)
{
ct.ThrowIfCancellationRequested();
libIdx++;
progress.Report(new OperationProgress(libIdx, libs.Count,
$"Scanning files by type: {lib.Title} ({libIdx}/{libs.Count})"));
// Use CamlQuery to enumerate all files in the library
// Paginate with 500 items per batch to avoid list view threshold issues
var query = new CamlQuery
{
ViewXml = @"<View Scope='RecursiveAll'>
<Query>
<Where>
<Eq>
<FieldRef Name='FSObjType' />
<Value Type='Integer'>0</Value>
</Eq>
</Where>
</Query>
<ViewFields>
<FieldRef Name='FileLeafRef' />
<FieldRef Name='File_x0020_Size' />
</ViewFields>
<RowLimit Paged='TRUE'>500</RowLimit>
</View>"
};
ListItemCollection items;
do
{
ct.ThrowIfCancellationRequested();
items = lib.GetItems(query);
ctx.Load(items, ic => ic.ListItemCollectionPosition,
ic => ic.Include(
i => i["FileLeafRef"],
i => i["File_x0020_Size"]));
await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(ctx, progress, ct);
foreach (var item in items)
{
string fileName = item["FileLeafRef"]?.ToString() ?? string.Empty;
string sizeStr = item["File_x0020_Size"]?.ToString() ?? "0";
if (!long.TryParse(sizeStr, out long fileSize))
fileSize = 0;
string ext = Path.GetExtension(fileName).ToLowerInvariant();
// ext is "" for extensionless files, ".docx" etc. for others
if (extensionMap.TryGetValue(ext, out var existing))
extensionMap[ext] = (existing.totalSize + fileSize, existing.count + 1);
else
extensionMap[ext] = (fileSize, 1);
}
// Move to next page
query.ListItemCollectionPosition = items.ListItemCollectionPosition;
}
while (items.ListItemCollectionPosition != null);
}
// Convert to FileTypeMetric list, sorted by size descending
return extensionMap
.Select(kvp => new FileTypeMetric(kvp.Key, kvp.Value.totalSize, kvp.Value.count))
.OrderByDescending(m => m.TotalSizeBytes)
.ToList();
}
```
Make sure to add `using System.IO;` at the top of the file if not already present (for `Path.GetExtension`).
Design notes:
- Uses `Scope='RecursiveAll'` in CamlQuery to get files from all subfolders without explicit recursion
- `FSObjType=0` filter ensures only files (not folders) are returned
- Paged query with 500-item batches avoids list view threshold (5000 default) issues
- File_x0020_Size is the internal name for file size in SharePoint
- Extensions normalized to lowercase for consistent grouping (".DOCX" and ".docx" merge)
- Dictionary uses OrdinalIgnoreCase comparer as extra safety
- Existing methods (CollectStorageAsync, LoadFolderNodeAsync, CollectSubfoldersAsync) are NOT touched
</action>
<verify>
<automated>cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet build SharepointToolbox/SharepointToolbox.csproj --no-incremental 2>&1 | tail -5</automated>
</verify>
<done>StorageService.cs implements CollectFileTypeMetricsAsync. Method enumerates files via CamlQuery with paging, groups by extension, returns IReadOnlyList&lt;FileTypeMetric&gt; sorted by TotalSizeBytes descending. Existing CollectStorageAsync is unchanged. Project compiles with 0 errors.</done>
</task>
</tasks>
<verification>
- `dotnet build SharepointToolbox/SharepointToolbox.csproj` succeeds with 0 errors
- StorageService now implements both IStorageService methods
- CollectFileTypeMetricsAsync uses paginated CamlQuery (RowLimit 500, Paged=TRUE)
- Extensions normalized to lowercase
- Results sorted by TotalSizeBytes descending
- No modifications to CollectStorageAsync, LoadFolderNodeAsync, or CollectSubfoldersAsync
</verification>
<success_criteria>
StorageService fully implements IStorageService. CollectFileTypeMetricsAsync can enumerate files by extension from any SharePoint site. The project compiles cleanly and existing storage scan behavior is unaffected.
</success_criteria>
<output>
After completion, create `.planning/phases/09-storage-visualization/09-02-SUMMARY.md`
</output>