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,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<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.Domain\Explorer.Domain.csproj" />
<ProjectReference Include="..\..\src\Explorer.Search\Explorer.Search.csproj" />
<ProjectReference Include="..\..\src\Explorer.Storage.Sqlite\Explorer.Storage.Sqlite.csproj" />
<ProjectReference Include="..\..\src\Explorer.Application\Explorer.Application.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,47 @@
using Explorer.Domain;
using Explorer.Search;
using Explorer.Storage.Sqlite;
using Microsoft.Extensions.Logging.Abstractions;
namespace Explorer.Search.Tests;
public class SearchServiceTests
{
[Fact]
public async Task Glob_and_size_and_directory_filters()
{
var db = Path.Combine(Path.GetTempPath(), "ew-search", Guid.NewGuid().ToString("N"), "index.db");
await using var store = new SqliteIndexStore(db, NullLogger<SqliteIndexStore>.Instance);
await store.OpenAsync();
var source = new Source { StableKey = "k", DisplayName = "d", Kind = SourceKind.NtfsLocal, LastRootPath = @"C:\d", Status = SourceStatus.Online };
source.Id = await store.Sources.UpsertAsync(source);
var root = new IndexEntry { SourceId = source.Id, Name = "d", NameNorm = "d", 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 = "clip.mkv", NameNorm = "clip.mkv", Extension = "mkv",
SizeBytes = 20_000_000, PathRel = "clip.mkv", LastSeenUtc = DateTimeOffset.UtcNow
});
await store.Entries.UpsertAsync(new IndexEntry
{
SourceId = source.Id, ParentId = root.Id, Name = "notes.txt", NameNorm = "notes.txt", Extension = "txt",
SizeBytes = 10, PathRel = "notes.txt", LastSeenUtc = DateTimeOffset.UtcNow
});
await store.Entries.UpsertAsync(new IndexEntry
{
SourceId = source.Id, ParentId = root.Id, Name = "Folder", NameNorm = "folder", IsDirectory = true,
PathRel = "Folder", LastSeenUtc = DateTimeOffset.UtcNow
});
var search = new SearchService(store);
var mkv = await search.SearchAsync(new SearchQuery { Text = "*.mkv", SourceIds = [source.Id] });
Assert.Single(mkv);
var big = await search.SearchAsync(new SearchQuery { MinSize = 1_000_000, SourceIds = [source.Id] });
Assert.Single(big);
var dirs = await search.SearchAsync(new SearchQuery { IsDirectory = true, SourceIds = [source.Id] });
Assert.Contains(dirs, e => e.Name == "Folder");
var both = await search.SearchAsync(new SearchQuery { SourceIds = [source.Id], IsDirectory = null });
Assert.Contains(both, e => e.Name == "Folder");
Assert.Contains(both, e => e.Name == "notes.txt");
}
}