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,145 @@
using Explorer.Application;
using Explorer.Domain;
using Explorer.Domain.Abstractions;
using Explorer.Storage.Sqlite;
using Microsoft.Extensions.Logging.Abstractions;
namespace Explorer.Application.Tests;
public class BrowseServiceTests
{
[Fact]
public async Task This_pc_shows_indexed_root_aggregate_size()
{
var (browse, store) = await CreateAsync(@"C:\", "C: (930.6 GB)");
var source = (await store.Sources.GetAllAsync()).Single();
source.LastIndexedUtc = DateTimeOffset.UtcNow;
await store.Sources.UpsertAsync(source);
var root = new IndexEntry
{
SourceId = source.Id,
Name = "C:",
NameNorm = "c:",
IsDirectory = true,
PathRel = "",
LastSeenUtc = DateTimeOffset.UtcNow,
AggregateSize = 1_073_741_824
};
await store.Entries.UpsertAsync(root);
var listing = await browse.ListThisPcAsync();
var item = Assert.Single(listing.Items);
Assert.Equal(1_073_741_824, item.SizeBytes);
Assert.True(item.IsDirectory);
}
[Fact]
public async Task Live_listing_overlays_folder_aggregate_from_index()
{
var (browse, store) = await CreateAsync(@"C:\", "C:");
var source = (await store.Sources.GetAllAsync()).Single();
source.LastIndexedUtc = DateTimeOffset.UtcNow;
await store.Sources.UpsertAsync(source);
var root = new IndexEntry
{
SourceId = source.Id,
Name = "C:",
NameNorm = "c:",
IsDirectory = true,
PathRel = "",
LastSeenUtc = DateTimeOffset.UtcNow
};
root.Id = await store.Entries.UpsertAsync(root);
await store.Entries.UpsertAsync(new IndexEntry
{
SourceId = source.Id,
ParentId = root.Id,
Name = "Movies",
NameNorm = "movies",
IsDirectory = true,
PathRel = "Movies",
LastSeenUtc = DateTimeOffset.UtcNow,
AggregateSize = 200
});
var listing = await browse.ListAsync(@"C:\");
var movies = Assert.Single(listing.Items, i => i.Name == "Movies");
Assert.Equal(200, movies.SizeBytes);
var loose = Assert.Single(listing.Items, i => i.Name == "loose.txt");
Assert.Equal(10, loose.SizeBytes);
}
private static async Task<(BrowseService Browse, SqliteIndexStore Store)> CreateAsync(string root, string display)
{
var db = Path.Combine(Path.GetTempPath(), "ew-browse", Guid.NewGuid().ToString("N"), "index.db");
var store = new SqliteIndexStore(db, NullLogger<SqliteIndexStore>.Instance);
await store.OpenAsync();
var volumes = new BrowseVolumes
{
Online =
[
new VolumeFingerprint
{
Kind = SourceKind.NtfsLocal,
RootPath = root,
DisplayName = display,
VolumeSerial = 1
}
]
};
var env = new BrowseEnv(Path.GetDirectoryName(db)!);
var sources = new SourceManager(store, volumes, env, new SystemClock(), NullLogger<SourceManager>.Instance);
await sources.InitializeAsync();
var browse = new BrowseService(
new BrowseEnumerator(),
volumes,
store,
sources,
new StorageProviderRegistry([], NullLogger<StorageProviderRegistry>.Instance));
return (browse, store);
}
}
file sealed class BrowseEnumerator : IFileSystemEnumerator
{
public IEnumerable<FileSystemItem> EnumerateChildren(string directoryPath)
=> EnumerateChildrenSafe(directoryPath, out _);
public FileSystemItem? GetItem(string path) => null;
public IReadOnlyList<FileSystemItem> EnumerateChildrenSafe(string directoryPath, out string? error)
{
error = null;
return
[
new FileSystemItem { FullPath = Path.Combine(directoryPath, "Movies"), Name = "Movies", IsDirectory = true },
new FileSystemItem { FullPath = Path.Combine(directoryPath, "loose.txt"), Name = "loose.txt", SizeBytes = 10 }
];
}
}
file sealed class BrowseVolumes : IVolumeService
{
public List<VolumeFingerprint> Online { get; set; } = [];
public IReadOnlyList<VolumeFingerprint> EnumerateOnlineVolumes() => Online;
public VolumeFingerprint? Probe(string path)
=> Online.FirstOrDefault(v => path.StartsWith(v.RootPath.TrimEnd('\\'), StringComparison.OrdinalIgnoreCase));
public bool IsPathReachable(string path)
=> Online.Any(v => path.StartsWith(v.RootPath.TrimEnd('\\'), StringComparison.OrdinalIgnoreCase));
}
file sealed class BrowseEnv : IAppEnvironment
{
public BrowseEnv(string dir)
{
DataDirectory = dir;
Directory.CreateDirectory(dir);
DatabasePath = Path.Combine(dir, "index.db");
LogDirectory = Path.Combine(dir, "logs");
Directory.CreateDirectory(LogDirectory);
}
public string DataDirectory { get; }
public string DatabasePath { get; }
public string LogDirectory { get; }
}

View File

@@ -0,0 +1,154 @@
using Explorer.Application;
using Explorer.Domain;
using Explorer.Plugin.Abstractions;
using Explorer.Plugin.OneDrive;
using Microsoft.Extensions.Logging.Abstractions;
namespace Explorer.Application.Tests;
public class CloudProviderTests
{
[Fact]
public async Task Enrich_passes_through_when_provider_throws()
{
var registry = new StorageProviderRegistry([new ThrowingProvider()], NullLogger<StorageProviderRegistry>.Instance);
var item = new FileSystemItem { FullPath = @"C:\a.txt", Name = "a.txt", SizeBytes = 10 };
var result = await registry.EnrichAsync([item]);
Assert.Single(result);
Assert.Null(result[0].Cloud);
Assert.Equal("a.txt", result[0].Name);
}
[Fact]
public async Task Enrich_maps_provider_state()
{
var registry = new StorageProviderRegistry([new StubProvider()], NullLogger<StorageProviderRegistry>.Instance);
var item = new FileSystemItem { FullPath = @"C:\OneDrive\photo.jpg", Name = "photo.jpg", SizeBytes = 100 };
var result = await registry.EnrichAsync([item]);
Assert.Equal(Explorer.Domain.CloudAvailability.OnlineOnly, result[0].Cloud?.Availability);
Assert.Equal(8, result[0].AllocatedSizeBytes);
Assert.True(result[0].Cloud?.MayHydrateOnRead);
}
[Fact]
public void Hydration_guard_skips_online_only_and_recall_attributes()
{
var registry = new StorageProviderRegistry([], NullLogger<StorageProviderRegistry>.Instance);
var guard = new HydrationGuard(registry);
Assert.True(guard.WouldHydrateOnRead(AttributeFlags.RecallOnDataAccess, null));
Assert.True(guard.WouldHydrateOnRead(0, Explorer.Domain.CloudAvailability.OnlineOnly));
Assert.False(guard.WouldHydrateOnRead(0, Explorer.Domain.CloudAvailability.LocallyAvailable));
Assert.True(guard.WouldHydrateOnRead(new FileSystemItem
{
FullPath = @"C:\x",
Name = "x",
Cloud = new CloudPresence("onedrive", Explorer.Domain.CloudAvailability.OnlineOnly, 1, 0, true)
}));
}
[Theory]
[InlineData(OneDrivePlaceholderState.RecallOnDataAccess, null, Plugin.Abstractions.CloudAvailability.OnlineOnly)]
[InlineData(OneDrivePlaceholderState.Pinned, null, Plugin.Abstractions.CloudAvailability.Pinned)]
[InlineData(0, CloudFilesNativeStates.Invalid, Plugin.Abstractions.CloudAvailability.Error)]
[InlineData(0, CloudFilesNativeStates.PlaceholderInSync, Plugin.Abstractions.CloudAvailability.LocallyAvailable)]
public void OneDrive_maps_attributes_to_availability(int attributes, uint? cf, Plugin.Abstractions.CloudAvailability expected)
{
Assert.Equal(expected, OneDrivePlaceholderState.MapAvailability(attributes, cf));
var state = OneDrivePlaceholderState.FromLocalSignals("onedrive", @"C:\OneDrive\f", attributes, 0, 50, 4, cf);
Assert.Equal(expected, state.Availability);
if (expected == Plugin.Abstractions.CloudAvailability.OnlineOnly)
{
Assert.True(state.MayHydrateOnRead);
}
}
[Fact]
public void Disabled_provider_is_ignored()
{
var registry = new StorageProviderRegistry([new StubProvider()], NullLogger<StorageProviderRegistry>.Instance);
registry.SetEnabled("onedrive", false);
Assert.Null(registry.Find(@"C:\OneDrive\a"));
Assert.False(registry.HasCapability(@"C:\OneDrive\a", ProviderCapability.Pin));
Assert.Empty(registry.GetPlaces());
}
[Fact]
public void GetPlaces_fail_open_when_provider_throws()
{
var registry = new StorageProviderRegistry([new ThrowingPlacesProvider()], NullLogger<StorageProviderRegistry>.Instance);
Assert.Empty(registry.GetPlaces());
}
[Theory]
[InlineData("Personal", @"C:\Users\x\OneDrive", null, "Dominique", "Dominique - Personal")]
[InlineData("Business1", @"C:\Users\x\OneDrive - Contoso", "Contoso", "Dominique", "Dominique - Contoso")]
[InlineData("Business1", @"C:\Users\x\OneDrive - Contoso", null, null, "OneDrive - Contoso")]
[InlineData("Personal", @"C:\Users\x\OneDrive", null, null, "OneDrive")]
public void OneDrive_place_label(string key, string folder, string? display, string? user, string expected)
=> Assert.Equal(expected, OneDrivePlaceDiscovery.BuildLabel(key, folder, display, user));
[Fact]
public void Cloud_place_store_merges_manual_without_duplicates()
{
var discovered = new ProviderPlace[]
{
new("onedrive", "Dominique - Personal", @"C:\Users\x\OneDrive")
};
var manual = new ProviderPlace[]
{
new("onedrive", "Work", @"C:\Users\x\OneDrive - Contoso"),
new("onedrive", "Dup", @"C:\Users\x\OneDrive")
};
var merged = CloudPlaceStore.Merge(discovered, manual);
Assert.Equal(2, merged.Count);
Assert.Equal("Dominique - Personal", merged[0].DisplayName);
Assert.Equal(@"C:\Users\x\OneDrive - Contoso", merged[1].Path);
}
}
file static class CloudFilesNativeStates
{
public const uint PlaceholderInSync = 0x1 | 0x8;
public const uint Invalid = 0xFFFFFFFF;
}
file sealed class ThrowingPlacesProvider : IStorageProvider
{
public ProviderManifest Manifest { get; } = new("boom", "Boom", "1", ProviderIsolation.InProcess);
public ProviderCapability GetCapabilities() => ProviderCapability.CloudState;
public bool TryMatchRoot(string path) => false;
public IReadOnlyList<ProviderPlace> GetPlaces() => throw new InvalidOperationException("places failed");
public Task<IReadOnlyList<ProviderItemState>> GetItemStatesAsync(IReadOnlyList<string> paths, CancellationToken cancellationToken = default)
=> Task.FromResult<IReadOnlyList<ProviderItemState>>([]);
public Task<ProviderActionResult> TryInvokeAsync(ProviderActionRequest request, CancellationToken cancellationToken = default)
=> Task.FromResult(new ProviderActionResult(ProviderActionStatus.Unsupported));
public Task<ProviderQuota?> TryGetQuotaAsync(string rootPath, CancellationToken cancellationToken = default)
=> Task.FromResult<ProviderQuota?>(null);
}
file sealed class ThrowingProvider : IStorageProvider
{
public ProviderManifest Manifest { get; } = new("boom", "Boom", "1", ProviderIsolation.InProcess);
public ProviderCapability GetCapabilities() => ProviderCapability.CloudState;
public bool TryMatchRoot(string path) => throw new InvalidOperationException("match failed");
public Task<IReadOnlyList<ProviderItemState>> GetItemStatesAsync(IReadOnlyList<string> paths, CancellationToken cancellationToken = default)
=> throw new InvalidOperationException("state failed");
public Task<ProviderActionResult> TryInvokeAsync(ProviderActionRequest request, CancellationToken cancellationToken = default)
=> throw new InvalidOperationException("invoke failed");
public Task<ProviderQuota?> TryGetQuotaAsync(string rootPath, CancellationToken cancellationToken = default)
=> throw new InvalidOperationException("quota failed");
}
file sealed class StubProvider : IStorageProvider
{
public ProviderManifest Manifest { get; } = new("onedrive", "OneDrive", "1", ProviderIsolation.InProcess);
public ProviderCapability GetCapabilities() => ProviderCapability.CloudState | ProviderCapability.Pin | ProviderCapability.Dehydrate;
public bool TryMatchRoot(string path) => path.Contains("OneDrive", StringComparison.OrdinalIgnoreCase);
public Task<IReadOnlyList<ProviderItemState>> GetItemStatesAsync(IReadOnlyList<string> paths, CancellationToken cancellationToken = default)
=> Task.FromResult<IReadOnlyList<ProviderItemState>>(paths.Select(p => new ProviderItemState(
"onedrive", p, Plugin.Abstractions.CloudAvailability.OnlineOnly, 100, 8, true, true, "Online-only", null)).ToList());
public Task<ProviderActionResult> TryInvokeAsync(ProviderActionRequest request, CancellationToken cancellationToken = default)
=> Task.FromResult(new ProviderActionResult(ProviderActionStatus.Succeeded));
public Task<ProviderQuota?> TryGetQuotaAsync(string rootPath, CancellationToken cancellationToken = default)
=> Task.FromResult<ProviderQuota?>(null);
}

View File

@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0-windows</TargetFramework>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Explorer.Application\Explorer.Application.csproj" />
<ProjectReference Include="..\..\src\Explorer.Domain\Explorer.Domain.csproj" />
<ProjectReference Include="..\..\src\Explorer.Plugin.Abstractions\Explorer.Plugin.Abstractions.csproj" />
<ProjectReference Include="..\..\src\Explorer.Plugin.OneDrive\Explorer.Plugin.OneDrive.csproj" />
<ProjectReference Include="..\..\src\Explorer.Indexing\Explorer.Indexing.csproj" />
<ProjectReference Include="..\..\src\Explorer.Storage.Sqlite\Explorer.Storage.Sqlite.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,49 @@
using Explorer.Application;
using Explorer.Domain.Abstractions;
namespace Explorer.Application.Tests;
public class PathHistoryStoreTests
{
[Fact]
public void Remember_moves_existing_path_to_front()
{
var next = PathHistoryStore.Remember(["D:\\Photos", "C:\\"], @"c:\");
Assert.Equal(@"c:\", next[0]);
Assert.Equal(2, next.Count);
Assert.Equal(@"D:\Photos", next[1]);
}
[Fact]
public void Load_and_save_roundtrip()
{
var dir = Path.Combine(Path.GetTempPath(), "ew-path-history", Guid.NewGuid().ToString("N"));
try
{
var store = new PathHistoryStore(new HistoryEnv(dir));
store.Save(["C:\\Work", "This PC", " "]);
var loaded = store.Load();
Assert.Equal(["C:\\Work"], loaded);
}
finally
{
try { Directory.Delete(dir, true); } catch { /* ignore */ }
}
}
}
file sealed class HistoryEnv : IAppEnvironment
{
public HistoryEnv(string dir)
{
DataDirectory = dir;
Directory.CreateDirectory(dir);
DatabasePath = Path.Combine(dir, "index.db");
LogDirectory = Path.Combine(dir, "logs");
Directory.CreateDirectory(LogDirectory);
}
public string DataDirectory { get; }
public string DatabasePath { get; }
public string LogDirectory { get; }
}

View File

@@ -0,0 +1,123 @@
using Explorer.Application;
using Explorer.Domain;
using Explorer.Domain.Abstractions;
using Explorer.Storage.Sqlite;
using Microsoft.Extensions.Logging.Abstractions;
namespace Explorer.Application.Tests;
public class SourceManagerTests
{
[Fact]
public async Task Removable_drive_keeps_stable_key_when_letter_changes()
{
var db = Path.Combine(Path.GetTempPath(), "ew-app", Guid.NewGuid().ToString("N"), "index.db");
await using var store = new SqliteIndexStore(db, NullLogger<SqliteIndexStore>.Instance);
await store.OpenAsync();
var volumes = new FakeVolumes();
volumes.Online =
[
new VolumeFingerprint
{
Kind = SourceKind.Removable,
VolumeGuid = @"\\?\Volume{same}\",
RootPath = @"E:\",
DisplayName = "Stick E",
VolumeSerial = 9,
CapacityBytes = 64
}
];
var env = new FakeEnv(Path.GetDirectoryName(db)!);
var mgr = new SourceManager(store, volumes, env, new SystemClock(), NullLogger<SourceManager>.Instance);
await mgr.InitializeAsync();
var first = (await store.Sources.GetAllAsync()).Single();
var key = first.StableKey;
volumes.Online =
[
new VolumeFingerprint
{
Kind = SourceKind.Removable,
VolumeGuid = @"\\?\Volume{same}\",
RootPath = @"G:\",
DisplayName = "Stick G",
VolumeSerial = 9,
CapacityBytes = 64
}
];
await mgr.RefreshOnlineStateAsync();
var again = (await store.Sources.GetAllAsync()).Single();
Assert.Equal(key, again.StableKey);
Assert.Equal(@"G:\", again.LastRootPath);
}
[Fact]
public async Task Ensure_for_mapped_letter_creates_source()
{
var db = Path.Combine(Path.GetTempPath(), "ew-app", Guid.NewGuid().ToString("N"), "index.db");
await using var store = new SqliteIndexStore(db, NullLogger<SqliteIndexStore>.Instance);
await store.OpenAsync();
var volumes = new FakeVolumes
{
Online =
[
new VolumeFingerprint
{
Kind = SourceKind.Smb,
RootPath = @"Z:\",
DisplayName = "Z: (Network)",
Filesystem = "SMB"
}
]
};
var env = new FakeEnv(Path.GetDirectoryName(db)!);
var mgr = new SourceManager(store, volumes, env, new SystemClock(), NullLogger<SourceManager>.Instance);
volumes.Online = [];
await mgr.InitializeAsync();
volumes.Online =
[
new VolumeFingerprint
{
Kind = SourceKind.Smb,
RootPath = @"Z:\",
DisplayName = "Z: (Network)",
Filesystem = "SMB"
}
];
var source = await mgr.EnsureForPathAsync(@"Z:\Photos");
Assert.NotNull(source);
Assert.Equal(@"Z:\", source!.LastRootPath);
var found = await mgr.FindByPathAsync(@"Z:\");
Assert.Equal(source.Id, found!.Id);
}
}
file sealed class FakeVolumes : IVolumeService
{
public List<VolumeFingerprint> Online { get; set; } = [];
public IReadOnlyList<VolumeFingerprint> EnumerateOnlineVolumes() => Online;
public VolumeFingerprint? Probe(string path)
{
var root = Path.GetPathRoot(path)?.TrimEnd('\\');
return Online.FirstOrDefault(v =>
v.RootPath.TrimEnd('\\').Equals(root, StringComparison.OrdinalIgnoreCase)
|| v.RootPath.Equals(path, StringComparison.OrdinalIgnoreCase));
}
public bool IsPathReachable(string path) => Online.Any(v => path.StartsWith(v.RootPath.TrimEnd('\\'), StringComparison.OrdinalIgnoreCase));
}
file sealed class FakeEnv : IAppEnvironment
{
public FakeEnv(string dir)
{
DataDirectory = dir;
Directory.CreateDirectory(dir);
DatabasePath = Path.Combine(dir, "index.db");
LogDirectory = Path.Combine(dir, "logs");
Directory.CreateDirectory(LogDirectory);
}
public string DataDirectory { get; }
public string DatabasePath { get; }
public string LogDirectory { get; }
}