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,151 @@
using Explorer.Domain;
using Explorer.Domain.Abstractions;
namespace Explorer.Application;
public sealed class BrowseService
{
private readonly IFileSystemEnumerator _enumerator;
private readonly IVolumeService _volumes;
private readonly IIndexStore _store;
private readonly SourceManager _sources;
private readonly StorageProviderRegistry _providers;
public BrowseService(
IFileSystemEnumerator enumerator,
IVolumeService volumes,
IIndexStore store,
SourceManager sources,
StorageProviderRegistry providers)
{
_enumerator = enumerator;
_volumes = volumes;
_store = store;
_sources = sources;
_providers = providers;
}
public async Task<FolderListing> ListThisPcAsync(CancellationToken cancellationToken = default)
{
var sources = await _sources.RefreshOnlineStateAsync(cancellationToken).ConfigureAwait(false);
var items = new List<FileSystemItem>();
foreach (var source in sources.OrderBy(s => s.Kind).ThenBy(s => s.DisplayName, StringComparer.CurrentCultureIgnoreCase))
{
IndexEntry? root = null;
if (source.IsIndexed)
{
root = await _store.Entries.GetRootAsync(source.Id, cancellationToken).ConfigureAwait(false);
}
items.Add(new FileSystemItem
{
FullPath = source.LastRootPath ?? source.DisplayName,
Name = source.Status == SourceStatus.Offline
? $"{source.DisplayName} (Offline)"
: source.DisplayName,
IsDirectory = true,
SizeBytes = root?.AggregateSize ?? 0,
Attributes = AttributeFlags.Directory
});
}
return new FolderListing { Path = "This PC", Items = items };
}
public async Task<FolderListing> ListAsync(string path, CancellationToken cancellationToken = default)
{
var source = await _sources.FindByPathAsync(path, cancellationToken).ConfigureAwait(false);
var reachable = _volumes.IsPathReachable(path);
if (reachable)
{
var items = _enumerator.EnumerateChildrenSafe(path, out var error);
var listing = (await _providers.EnrichAsync(items.ToList(), cancellationToken).ConfigureAwait(false)).ToList();
if (source is { IsIndexed: true })
{
listing = await OverlayFolderSizesAsync(source, path, listing, cancellationToken).ConfigureAwait(false);
}
return new FolderListing { Path = path, IsOffline = false, Items = listing, Error = error };
}
if (source is { IsIndexed: true })
{
var rel = source.LastRootPath is null ? "" : PathRules.MakeRelative(source.LastRootPath, path);
var dir = await _store.Entries.GetByPathAsync(source.Id, rel, cancellationToken).ConfigureAwait(false)
?? await _store.Entries.GetRootAsync(source.Id, cancellationToken).ConfigureAwait(false);
if (dir is null)
{
return new FolderListing { Path = path, IsOffline = true, Error = "Not available" };
}
var children = await _store.Entries.GetChildrenAsync(source.Id, dir.Id, null, cancellationToken)
.ConfigureAwait(false);
var items = children
.Where(c => c.Status is EntryStatus.Present or EntryStatus.Offline)
.Select(c => new FileSystemItem
{
FullPath = PathRules.Combine(source.LastRootPath ?? source.DisplayName, c.PathRel),
Name = c.Name,
IsDirectory = c.IsDirectory,
SizeBytes = c.IsDirectory ? c.AggregateSize : c.SizeBytes,
CreatedUtc = c.CreatedUtc,
ModifiedUtc = c.ModifiedUtc,
Attributes = c.Attributes,
FileId = c.FileId,
ReparseTag = c.ReparseTag,
AllocatedSizeBytes = c.AllocatedSizeBytes,
Cloud = c.CloudAvailability is { } availability
? new CloudPresence(null, availability, c.SizeBytes, c.AllocatedSizeBytes, availability == CloudAvailability.OnlineOnly)
: null
})
.ToList();
return new FolderListing { Path = path, IsOffline = true, Items = items };
}
return new FolderListing { Path = path, IsOffline = true, Error = "Path not found" };
}
private async Task<List<FileSystemItem>> OverlayFolderSizesAsync(
Source source,
string path,
List<FileSystemItem> listing,
CancellationToken cancellationToken)
{
var rel = PathRules.MakeRelative(source.LastRootPath ?? path, path);
var indexed = await _store.Entries.GetByPathAsync(source.Id, rel, cancellationToken).ConfigureAwait(false)
?? (string.IsNullOrEmpty(rel)
? await _store.Entries.GetRootAsync(source.Id, cancellationToken).ConfigureAwait(false)
: null);
if (indexed is null)
{
return listing;
}
var children = await _store.Entries.GetChildrenAsync(source.Id, indexed.Id, EntryStatus.Present, cancellationToken)
.ConfigureAwait(false);
var byName = children.ToDictionary(c => c.NameNorm, StringComparer.Ordinal);
return listing.Select(i =>
{
if (!i.IsDirectory || !byName.TryGetValue(NameNormalizer.Normalize(i.Name), out var e))
{
return i;
}
return new FileSystemItem
{
FullPath = i.FullPath,
Name = i.Name,
IsDirectory = true,
SizeBytes = e.AggregateSize,
CreatedUtc = i.CreatedUtc,
ModifiedUtc = i.ModifiedUtc,
Attributes = i.Attributes,
FileId = i.FileId,
ReparseTag = i.ReparseTag,
AllocatedSizeBytes = i.AllocatedSizeBytes ?? e.AllocatedSizeBytes,
Cloud = i.Cloud
};
}).ToList();
}
}