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>
This commit is contained in:
242
.planning/phases/09-storage-visualization/09-02-PLAN.md
Normal file
242
.planning/phases/09-storage-visualization/09-02-PLAN.md
Normal file
@@ -0,0 +1,242 @@
|
||||
---
|
||||
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<FileTypeMetric> 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>
|
||||
Reference in New Issue
Block a user