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>
243 lines
9.5 KiB
Markdown
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<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>
|