Add Git overlay, operation tools, and virtualized preview so large folders stay responsive.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-24 15:04:04 +02:00
parent 9bf451932f
commit a3c54bbb03
127 changed files with 14748 additions and 633 deletions

View File

@@ -1,6 +1,7 @@
using Explorer.Application;
using Explorer.Domain;
using Explorer.Domain.Abstractions;
using Explorer.Plugin.Abstractions;
using Explorer.Storage.Sqlite;
using Microsoft.Extensions.Logging.Abstractions;
@@ -28,11 +29,61 @@ public class BrowseServiceTests
await store.Entries.UpsertAsync(root);
var listing = await browse.ListThisPcAsync();
var item = Assert.Single(listing.Items);
var item = Assert.Single(listing.Items, i => i.FullPath.StartsWith(@"C:\", StringComparison.OrdinalIgnoreCase));
Assert.Equal(1_073_741_824, item.SizeBytes);
Assert.Equal(250_000_000, item.FreeSpaceBytes);
Assert.Equal(1_000_000_000, item.CapacityBytes);
Assert.True(item.IsDirectory);
Assert.Contains(listing.Items, i => i.FullPath == LocationRoots.RecycleBin);
}
[Fact]
public async Task This_pc_lists_untracked_network_drives_as_importable()
{
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 = @"C:\",
DisplayName = "C:",
VolumeSerial = 1,
CapacityBytes = 1_000_000_000,
FreeBytes = 250_000_000
},
new VolumeFingerprint
{
Kind = SourceKind.Smb,
RootPath = @"Z:\",
DisplayName = "Z: (Network)",
Filesystem = "SMB",
CapacityBytes = 2_000,
FreeBytes = 500
}
]
};
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),
new CloudPlaceStore(env),
new UiPreferencesStore(env));
var listing = await browse.ListThisPcAsync();
Assert.Contains(listing.Items, i => i.FullPath.StartsWith(@"C:\", StringComparison.OrdinalIgnoreCase) && !i.AvailableToImport);
var imported = Assert.Single(listing.Items, i => i.AvailableToImport);
Assert.Equal(@"Z:\", imported.FullPath);
Assert.Contains("(Windows)", imported.DisplayName);
await store.DisposeAsync();
}
[Fact]
@@ -72,7 +123,95 @@ public class BrowseServiceTests
Assert.Equal(10, loose.SizeBytes);
}
private static async Task<(BrowseService Browse, SqliteIndexStore Store)> CreateAsync(string root, string display)
[Fact]
public async Task Recycle_bin_listing_uses_catalog_summary()
{
var (browse, _) = await CreateAsync(@"C:\", "C:", new FakeRecycleBin(new RecycleBinSummary(3, 4096)));
var listing = browse.ListRecycleBin();
Assert.Equal(LocationRoots.RecycleBin, listing.Path);
Assert.Contains("3", listing.Error);
Assert.Contains("4 KB", listing.Error, StringComparison.OrdinalIgnoreCase);
var item = Assert.Single(listing.Items);
Assert.True(item.Location.IsRecycleBin);
Assert.Equal(4096, item.SizeBytes);
var redirected = await browse.ListAsync(@"C:\$RECYCLE.BIN");
Assert.Equal(LocationRoots.RecycleBin, redirected.Path);
}
[Fact]
public async Task Recycle_bin_folder_stays_hidden_when_protected_locations_are_shown()
{
var (browse, _) = await CreateAsync(
@"C:\",
"C:",
enumerator: new RecycleFolderEnumerator(),
configurePrefs: store => store.Save(UiPreferences.Default with { ShowProtectedSystemLocations = true }));
var listing = await browse.ListAsync(@"C:\");
Assert.DoesNotContain(listing.Items, i => i.Name.Equals("$RECYCLE.BIN", StringComparison.OrdinalIgnoreCase));
Assert.Contains(listing.Items, i => i.Name == "Movies");
}
[Fact]
public async Task Cloud_quota_overrides_volume_space_and_null_is_safe()
{
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 env = new BrowseEnv(Path.GetDirectoryName(db)!);
var volumes = new BrowseVolumes();
var sources = new SourceManager(store, volumes, env, new SystemClock(), NullLogger<SourceManager>.Instance);
await sources.InitializeAsync();
var places = new CloudPlaceStore(env);
var cloudRoot = Path.Combine(env.DataDirectory, "OneDrive");
Directory.CreateDirectory(cloudRoot);
places.Add("onedrive", cloudRoot, "OneDrive");
var quotaProvider = new QuotaProvider(cloudRoot, new ProviderQuota(cloudRoot, UsedBytes: 20, TotalBytes: 100, Label: "OneDrive"));
var browse = new BrowseService(
new BrowseEnumerator(),
volumes,
store,
sources,
new StorageProviderRegistry([quotaProvider], NullLogger<StorageProviderRegistry>.Instance),
places,
new UiPreferencesStore(env));
var listing = await browse.ListCloudAsync();
var item = Assert.Single(listing.Items);
Assert.Equal(80, item.FreeSpaceBytes);
Assert.Equal(100, item.CapacityBytes);
var nullSafe = new BrowseService(
new BrowseEnumerator(),
volumes,
store,
sources,
new StorageProviderRegistry([new QuotaProvider(cloudRoot, null)], NullLogger<StorageProviderRegistry>.Instance),
places,
new UiPreferencesStore(env));
var fallback = await nullSafe.ListCloudAsync();
Assert.Null(Assert.Single(fallback.Items).FreeSpaceBytes);
var throwing = new BrowseService(
new BrowseEnumerator(),
volumes,
store,
sources,
new StorageProviderRegistry(
[new QuotaProvider(cloudRoot, null, throwOnQuota: true)],
NullLogger<StorageProviderRegistry>.Instance),
places,
new UiPreferencesStore(env));
var safe = await throwing.ListCloudAsync();
Assert.Null(Assert.Single(safe.Items).FreeSpaceBytes);
await store.DisposeAsync();
}
private static async Task<(BrowseService Browse, SqliteIndexStore Store)> CreateAsync(
string root,
string display,
IRecycleBinCatalog? recycle = null,
IFileSystemEnumerator? enumerator = null,
Action<UiPreferencesStore>? configurePrefs = null)
{
var db = Path.Combine(Path.GetTempPath(), "ew-browse", Guid.NewGuid().ToString("N"), "index.db");
var store = new SqliteIndexStore(db, NullLogger<SqliteIndexStore>.Instance);
@@ -95,14 +234,17 @@ public class BrowseServiceTests
var env = new BrowseEnv(Path.GetDirectoryName(db)!);
var sources = new SourceManager(store, volumes, env, new SystemClock(), NullLogger<SourceManager>.Instance);
await sources.InitializeAsync();
var prefs = new UiPreferencesStore(env);
configurePrefs?.Invoke(prefs);
var browse = new BrowseService(
new BrowseEnumerator(),
enumerator ?? new BrowseEnumerator(),
volumes,
store,
sources,
new StorageProviderRegistry([], NullLogger<StorageProviderRegistry>.Instance),
new CloudPlaceStore(env),
new UiPreferencesStore(env));
prefs,
recycle: recycle);
return (browse, store);
}
}
@@ -125,6 +267,50 @@ file sealed class BrowseEnumerator : IFileSystemEnumerator
}
}
file sealed class RecycleFolderEnumerator : 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, "$RECYCLE.BIN"),
Name = "$RECYCLE.BIN",
IsDirectory = true,
Attributes = AttributeFlags.Directory | AttributeFlags.Hidden | AttributeFlags.System
},
new FileSystemItem { FullPath = Path.Combine(directoryPath, "Movies"), Name = "Movies", IsDirectory = true }
];
}
}
file sealed class FakeRecycleBin(RecycleBinSummary summary) : IRecycleBinCatalog
{
public RecycleBinSummary? TrySummarize(string? rootPath = null) => summary;
}
file sealed class QuotaProvider(string root, ProviderQuota? quota, bool throwOnQuota = false) : IStorageProvider
{
public ProviderManifest Manifest { get; } = new("onedrive", "OneDrive", "1", ProviderIsolation.InProcess);
public ProviderCapability GetCapabilities() => ProviderCapability.Quota;
public bool TryMatchRoot(string path) => CloudPath.IsUnder(root, path);
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)
=> throwOnQuota
? throw new InvalidOperationException("quota failed")
: Task.FromResult(quota);
}
file sealed class BrowseVolumes : IVolumeService
{
public List<VolumeFingerprint> Online { get; set; } = [];