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,295 @@
using Explorer.Domain;
using Explorer.Domain.Abstractions;
using Microsoft.Extensions.Logging;
namespace Explorer.Application;
public sealed class SourceManager
{
private readonly IIndexStore _store;
private readonly IVolumeService _volumes;
private readonly IAppEnvironment _env;
private readonly IClock _clock;
private readonly ILogger<SourceManager> _logger;
public SourceManager(
IIndexStore store,
IVolumeService volumes,
IAppEnvironment env,
IClock clock,
ILogger<SourceManager> logger)
{
_store = store;
_volumes = volumes;
_env = env;
_clock = clock;
_logger = logger;
}
public async Task InitializeAsync(CancellationToken cancellationToken = default)
{
await _store.OpenAsync(cancellationToken).ConfigureAwait(false);
await _store.ScanJobs.InterruptRunningAsync(cancellationToken).ConfigureAwait(false);
await _store.Excludes.EnsureDefaultsAsync(DefaultExcludes.Create(), cancellationToken).ConfigureAwait(false);
await RefreshOnlineStateAsync(cancellationToken).ConfigureAwait(false);
}
public async Task<IReadOnlyList<Source>> RefreshOnlineStateAsync(CancellationToken cancellationToken = default)
{
var known = (await _store.Sources.GetAllAsync(cancellationToken).ConfigureAwait(false)).ToList();
var online = _volumes.EnumerateOnlineVolumes();
var seenIds = new HashSet<long>();
foreach (var fp in online)
{
var match = VolumeIdentityMatcher.Match(fp, known);
Source source;
if (match.Source is not null && !match.Ambiguous)
{
source = match.Source;
source.LastRootPath = fp.RootPath;
source.DisplayName = fp.DisplayName ?? source.DisplayName;
source.Label = fp.Label ?? source.Label;
source.Filesystem = fp.Filesystem ?? source.Filesystem;
source.CapacityBytes = fp.CapacityBytes ?? source.CapacityBytes;
source.VolumeGuid = fp.VolumeGuid ?? source.VolumeGuid;
source.VolumeSerial = fp.VolumeSerial ?? source.VolumeSerial;
source.Kind = fp.Kind;
source.LastSeenUtc = _clock.UtcNow;
source.Status = source.Status == SourceStatus.Scanning ? SourceStatus.Scanning : SourceStatus.Online;
source.LastError = null;
await _store.Sources.UpsertAsync(source, cancellationToken).ConfigureAwait(false);
if (source.IsIndexed)
{
await _store.Entries.MarkSourceOnlinePresentAsync(source.Id, cancellationToken).ConfigureAwait(false);
}
}
else
{
source = new Source
{
StableKey = Guid.NewGuid().ToString("N"),
Kind = fp.Kind,
DisplayName = fp.DisplayName ?? fp.RootPath,
VolumeGuid = fp.VolumeGuid,
VolumeSerial = fp.VolumeSerial,
Filesystem = fp.Filesystem,
Label = fp.Label,
CapacityBytes = fp.CapacityBytes,
DeviceInstanceId = fp.DeviceInstanceId,
LastRootPath = fp.RootPath,
Status = SourceStatus.Online,
LastSeenUtc = _clock.UtcNow
};
source.Id = await _store.Sources.UpsertAsync(source, cancellationToken).ConfigureAwait(false);
known.Add(source);
}
seenIds.Add(source.Id);
}
foreach (var source in known)
{
if (seenIds.Contains(source.Id))
{
continue;
}
var reachable = source.LastRootPath is not null && _volumes.IsPathReachable(source.LastRootPath);
if (reachable)
{
continue;
}
if (source.Status != SourceStatus.Offline)
{
await _store.Sources.UpdateStatusAsync(source.Id, SourceStatus.Offline, null, cancellationToken)
.ConfigureAwait(false);
await _store.Entries.MarkSourceOfflineAsync(source.Id, cancellationToken).ConfigureAwait(false);
}
}
foreach (var unc in LoadRecents())
{
if (known.Any(s => s.LastRootPath is not null
&& PathRules.CanonicalUncRoot(s.LastRootPath)
.Equals(PathRules.CanonicalUncRoot(unc), StringComparison.OrdinalIgnoreCase)))
{
continue;
}
await AddUncAsync(unc, cancellationToken).ConfigureAwait(false);
}
return await _store.Sources.GetAllAsync(cancellationToken).ConfigureAwait(false);
}
public async Task<Source> AddUncAsync(string path, CancellationToken cancellationToken = default)
{
var root = PathRules.CanonicalUncRoot(path);
var known = await _store.Sources.GetAllAsync(cancellationToken).ConfigureAwait(false);
var fp = new VolumeFingerprint
{
Kind = SourceKind.Smb,
RootPath = root,
DisplayName = root,
Filesystem = "SMB"
};
var match = VolumeIdentityMatcher.Match(fp, known);
if (match.Source is not null)
{
var existing = match.Source;
existing.LastRootPath = root;
existing.LastSeenUtc = _clock.UtcNow;
existing.Status = _volumes.IsPathReachable(root) ? SourceStatus.Online : SourceStatus.Offline;
await _store.Sources.UpsertAsync(existing, cancellationToken).ConfigureAwait(false);
RememberUnc(root);
return existing;
}
var source = new Source
{
StableKey = Guid.NewGuid().ToString("N"),
Kind = SourceKind.Smb,
DisplayName = root,
Filesystem = "SMB",
LastRootPath = root,
Status = _volumes.IsPathReachable(root) ? SourceStatus.Online : SourceStatus.Offline,
LastSeenUtc = _clock.UtcNow
};
source.Id = await _store.Sources.UpsertAsync(source, cancellationToken).ConfigureAwait(false);
RememberUnc(root);
return source;
}
public async Task<Source?> FindByPathAsync(string path, CancellationToken cancellationToken = default)
{
var sources = await _store.Sources.GetAllAsync(cancellationToken).ConfigureAwait(false);
var normalized = PathRules.FromExtended(path);
Source? best = null;
var bestLen = -1;
foreach (var source in sources)
{
if (source.LastRootPath is null)
{
continue;
}
var root = PathRules.FromExtended(source.LastRootPath).TrimEnd('\\');
var candidate = normalized.TrimEnd('\\');
if (candidate.Equals(root, StringComparison.OrdinalIgnoreCase)
|| candidate.StartsWith(root + "\\", StringComparison.OrdinalIgnoreCase)
|| (root.Length == 2 && root[1] == ':' && candidate.StartsWith(root, StringComparison.OrdinalIgnoreCase)))
{
if (root.Length > bestLen)
{
best = source;
bestLen = root.Length;
}
}
}
return best;
}
public async Task<Source?> EnsureForPathAsync(string path, CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(path) || path == "This PC")
{
return null;
}
var existing = await FindByPathAsync(path, cancellationToken).ConfigureAwait(false);
if (existing is not null)
{
existing.LastSeenUtc = _clock.UtcNow;
existing.Status = _volumes.IsPathReachable(existing.LastRootPath ?? path)
? (existing.Status == SourceStatus.Scanning ? SourceStatus.Scanning : SourceStatus.Online)
: SourceStatus.Offline;
await _store.Sources.UpsertAsync(existing, cancellationToken).ConfigureAwait(false);
return existing;
}
var fp = _volumes.Probe(path);
if (fp is null)
{
return null;
}
var known = await _store.Sources.GetAllAsync(cancellationToken).ConfigureAwait(false);
var match = VolumeIdentityMatcher.Match(fp, known);
if (match.Source is not null && !match.Ambiguous)
{
var source = match.Source;
source.LastRootPath = fp.RootPath;
source.DisplayName = fp.DisplayName ?? source.DisplayName;
source.Kind = fp.Kind;
source.Filesystem = fp.Filesystem ?? source.Filesystem;
source.Label = fp.Label ?? source.Label;
source.LastSeenUtc = _clock.UtcNow;
source.Status = _volumes.IsPathReachable(fp.RootPath) ? SourceStatus.Online : SourceStatus.Offline;
await _store.Sources.UpsertAsync(source, cancellationToken).ConfigureAwait(false);
return source;
}
if (fp.Kind == SourceKind.Smb && PathRules.IsUnc(fp.RootPath))
{
return await AddUncAsync(fp.RootPath, cancellationToken).ConfigureAwait(false);
}
var created = new Source
{
StableKey = Guid.NewGuid().ToString("N"),
Kind = fp.Kind,
DisplayName = fp.DisplayName ?? fp.RootPath,
VolumeGuid = fp.VolumeGuid,
VolumeSerial = fp.VolumeSerial,
Filesystem = fp.Filesystem,
Label = fp.Label,
CapacityBytes = fp.CapacityBytes,
LastRootPath = fp.RootPath,
Status = _volumes.IsPathReachable(fp.RootPath) ? SourceStatus.Online : SourceStatus.Offline,
LastSeenUtc = _clock.UtcNow
};
created.Id = await _store.Sources.UpsertAsync(created, cancellationToken).ConfigureAwait(false);
return created;
}
private IReadOnlyList<string> LoadRecents()
{
var file = Path.Combine(_env.DataDirectory, "recents.txt");
if (!File.Exists(file))
{
return [];
}
try
{
return File.ReadAllLines(file)
.Where(l => !string.IsNullOrWhiteSpace(l))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
}
catch (Exception ex)
{
_logger.LogDebug(ex, "Failed reading recents");
return [];
}
}
private void RememberUnc(string root)
{
try
{
var file = Path.Combine(_env.DataDirectory, "recents.txt");
var lines = LoadRecents().ToList();
lines.RemoveAll(l => l.Equals(root, StringComparison.OrdinalIgnoreCase));
lines.Insert(0, root);
File.WriteAllLines(file, lines.Take(30));
}
catch (Exception ex)
{
_logger.LogDebug(ex, "Failed writing recents");
}
}
}