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:
145
tests/Explorer.Application.Tests/BrowseServiceTests.cs
Normal file
145
tests/Explorer.Application.Tests/BrowseServiceTests.cs
Normal 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; }
|
||||
}
|
||||
Reference in New Issue
Block a user