Add Explorer Workbench with hierarchical, off-UI Storage analysis.

Storage queries run in the background with cancellation and covering indexes so switching views no longer freezes the UI.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-22 12:43:05 +02:00
commit e9aba73552
130 changed files with 15110 additions and 0 deletions

View File

@@ -0,0 +1,106 @@
using Explorer.Application;
using Explorer.Domain;
using Explorer.Domain.Abstractions;
using Microsoft.Extensions.Logging;
namespace Explorer.Indexing;
public sealed class FolderReconciler
{
private readonly IIndexStore _store;
private readonly IFileSystemEnumerator _enumerator;
private readonly StorageProviderRegistry _providers;
public FolderReconciler(IIndexStore store, IFileSystemEnumerator enumerator, StorageProviderRegistry providers)
{
_store = store;
_enumerator = enumerator;
_providers = providers;
}
public async Task ReconcileAsync(Source source, string pathRel, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(source.LastRootPath))
{
return;
}
var full = PathRules.Combine(source.LastRootPath, pathRel);
var parent = await _store.Entries.GetByPathAsync(source.Id, pathRel, cancellationToken).ConfigureAwait(false);
if (parent is null)
{
return;
}
var live = await _providers.EnrichAsync(_enumerator.EnumerateChildrenSafe(full, out _), cancellationToken)
.ConfigureAwait(false);
var indexed = await _store.Entries.GetChildrenAsync(source.Id, parent.Id, EntryStatus.Present, cancellationToken)
.ConfigureAwait(false);
var now = DateTimeOffset.UtcNow;
var liveNames = new HashSet<string>(live.Select(i => NameNormalizer.Normalize(i.Name)), StringComparer.Ordinal);
await _store.RunWriteAsync(async s =>
{
foreach (var item in live)
{
var rel = PathRules.MakeRelative(source.LastRootPath, item.FullPath);
var entry = new IndexEntry
{
SourceId = source.Id,
ParentId = parent.Id,
Name = item.Name,
NameNorm = NameNormalizer.Normalize(item.Name),
Extension = item.IsDirectory ? null : NameNormalizer.Extension(item.Name),
IsDirectory = item.IsDirectory,
SizeBytes = item.IsDirectory ? 0 : item.SizeBytes,
AggregateSize = item.IsDirectory ? 0 : item.SizeBytes,
CreatedUtc = item.CreatedUtc,
ModifiedUtc = item.ModifiedUtc,
LastSeenUtc = now,
LastIndexedUtc = now,
Attributes = item.Attributes,
FileId = item.FileId,
ReparseTag = item.ReparseTag,
Status = EntryStatus.Present,
PathRel = rel,
ScanGeneration = source.ScanGeneration,
AllocatedSizeBytes = item.AllocatedSizeBytes ?? item.Cloud?.AllocatedSizeBytes,
CloudAvailability = item.Cloud?.Availability
};
var existing = indexed.FirstOrDefault(e => e.NameNorm == entry.NameNorm);
var oldSize = existing is { IsDirectory: false } ? existing.SizeBytes : 0;
var id = await s.Entries.UpsertAsync(entry, cancellationToken).ConfigureAwait(false);
if (!item.IsDirectory)
{
var delta = item.SizeBytes - oldSize;
if (existing is null)
{
await s.Entries.ApplySizeDeltaToAncestorsAsync(parent.Id, item.SizeBytes, 1, 0, cancellationToken)
.ConfigureAwait(false);
}
else if (delta != 0)
{
await s.Entries.ApplySizeDeltaToAncestorsAsync(parent.Id, delta, 0, 0, cancellationToken)
.ConfigureAwait(false);
}
}
else if (existing is null)
{
await s.Entries.ApplySizeDeltaToAncestorsAsync(parent.Id, 0, 0, 1, cancellationToken)
.ConfigureAwait(false);
}
_ = id;
}
foreach (var old in indexed)
{
if (!liveNames.Contains(old.NameNorm))
{
await s.Entries.TombstoneAsync(old.Id, now, cancellationToken).ConfigureAwait(false);
}
}
}, cancellationToken).ConfigureAwait(false);
}
}