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:
106
src/Explorer.Indexing/FolderReconciler.cs
Normal file
106
src/Explorer.Indexing/FolderReconciler.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user