Add settings, cloud places, and optional archive-content indexing.
Keep official clients in charge of sync while Explorer can group locations, persist UI prefs, and list zip/rar/7z members without extracting them. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -95,7 +95,9 @@ public class BrowseServiceTests
|
||||
volumes,
|
||||
store,
|
||||
sources,
|
||||
new StorageProviderRegistry([], NullLogger<StorageProviderRegistry>.Instance));
|
||||
new StorageProviderRegistry([], NullLogger<StorageProviderRegistry>.Instance),
|
||||
new CloudPlaceStore(env),
|
||||
new UiPreferencesStore(env));
|
||||
return (browse, store);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using Explorer.Application;
|
||||
using Explorer.Domain;
|
||||
using Explorer.Plugin.Abstractions;
|
||||
using Explorer.Plugin.GoogleDrive;
|
||||
using Explorer.Plugin.Nextcloud;
|
||||
using Explorer.Plugin.OneDrive;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
@@ -104,6 +106,111 @@ public class CloudProviderTests
|
||||
Assert.Equal("Dominique - Personal", merged[0].DisplayName);
|
||||
Assert.Equal(@"C:\Users\x\OneDrive - Contoso", merged[1].Path);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Cloud_path_is_under_is_prefix_safe()
|
||||
{
|
||||
Assert.True(CloudPath.IsUnder(@"F:\OneDrive - wyniger", @"F:\OneDrive - wyniger\Obsidian\Notes"));
|
||||
Assert.False(CloudPath.IsUnder(@"F:\OneDrive", @"F:\OneDrive - wyniger"));
|
||||
Assert.True(CloudPath.IsUnder(@"J:\", @"J:\My Drive\Photos"));
|
||||
Assert.Equal(@"J:\", CloudPath.NormalizePlace("j:"));
|
||||
Assert.Equal(@"J:\", CloudPath.NormalizePlace(@"J:\"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DriveFS_media_named_google_drive_becomes_a_place()
|
||||
{
|
||||
var places = DriveFsPreferenceReader.PlacesFromTables(
|
||||
[
|
||||
new("Games", @"D:\", 4),
|
||||
new("Google Drive", @"J:\", 10),
|
||||
new("MP3", @"G:\", 4)
|
||||
],
|
||||
[
|
||||
new("My Drive", @"My Drive", @"J:\My Drive", true)
|
||||
]);
|
||||
Assert.Single(places);
|
||||
Assert.Equal("googledrive", places[0].ProviderId);
|
||||
Assert.Equal("Google Drive", places[0].DisplayName);
|
||||
Assert.Equal(@"J:\", places[0].Path);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DriveFS_mirrored_folder_becomes_a_place_when_no_mount()
|
||||
{
|
||||
var places = DriveFsPreferenceReader.PlacesFromTables(
|
||||
[new("Data", @"F:\", 4)],
|
||||
[new("Photos", @"Photos", @"F:\Google Photos", false)]);
|
||||
Assert.Single(places);
|
||||
Assert.Equal("Google Drive - Photos", places[0].DisplayName);
|
||||
Assert.Equal(@"F:\Google Photos", places[0].Path);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DriveFS_local_preference_db_exposes_configured_mount()
|
||||
{
|
||||
if (!File.Exists(DriveFsPreferenceReader.DefaultDatabasePath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var places = DriveFsPreferenceReader.ReadPlaces();
|
||||
Assert.Contains(places, p => p.DisplayName == "Google Drive" && p.Path.Length >= 2);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"C:\Users\x\My Drive", "Google Drive")]
|
||||
[InlineData(@"G:\Shared drives", "Shared drives")]
|
||||
[InlineData(@"D:\Work", "Google Drive - Work")]
|
||||
public void Google_drive_place_label(string path, string expected)
|
||||
=> Assert.Equal(expected, GoogleDrivePlaceDiscovery.BuildLabel(path, null));
|
||||
|
||||
[Fact]
|
||||
public void Nextcloud_parses_account_folders_from_cfg()
|
||||
{
|
||||
const string cfg = """
|
||||
[Accounts]
|
||||
0\url=https://cloud.example.com
|
||||
0\displayName=Dominique Wyniger
|
||||
0\Folders\1\localPath=F:/OneDrive - wyniger/Obsidian/Notes/
|
||||
0\Folders\1\targetPath=/Notizen/Notes
|
||||
0\Folders\1\virtualFilesMode=off
|
||||
""";
|
||||
var places = NextcloudPlaceDiscovery.ParseCfg(cfg);
|
||||
Assert.Single(places);
|
||||
Assert.Equal("nextcloud", places[0].ProviderId);
|
||||
Assert.Equal(@"F:\OneDrive - wyniger\Obsidian\Notes", places[0].Path);
|
||||
Assert.Equal("Nextcloud - Notes", places[0].DisplayName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OneDrive_does_not_claim_google_drive_paths()
|
||||
{
|
||||
Assert.False(OneDriveStorageProvider.LooksLikeOneDrive(@"G:\My Drive\Photos"));
|
||||
Assert.True(GoogleDriveStorageProvider.LooksLikeGoogleDrive(@"G:\My Drive\Photos"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Discover_skips_missing_default_profile_folders()
|
||||
{
|
||||
var profile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
||||
var google = GoogleDrivePlaceDiscovery.Discover();
|
||||
var nextcloud = NextcloudPlaceDiscovery.Discover();
|
||||
AssertMissingGuess(google, Path.Combine(profile, "Google Drive"));
|
||||
AssertMissingGuess(google, Path.Combine(profile, "My Drive"));
|
||||
AssertMissingGuess(nextcloud, Path.Combine(profile, "Nextcloud"));
|
||||
AssertMissingGuess(nextcloud, Path.Combine(profile, "ownCloud"));
|
||||
}
|
||||
|
||||
private static void AssertMissingGuess(IReadOnlyList<ProviderPlace> places, string path)
|
||||
{
|
||||
if (Directory.Exists(path))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Assert.DoesNotContain(places, p => p.Path.Equals(path.TrimEnd('\\'), StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
|
||||
file static class CloudFilesNativeStates
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
<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.GoogleDrive\Explorer.Plugin.GoogleDrive.csproj" />
|
||||
<ProjectReference Include="..\..\src\Explorer.Plugin.Nextcloud\Explorer.Plugin.Nextcloud.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" />
|
||||
|
||||
@@ -90,6 +90,68 @@ public class SourceManagerTests
|
||||
var found = await mgr.FindByPathAsync(@"Z:\");
|
||||
Assert.Equal(source.Id, found!.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Forget_removes_disconnected_source_and_index_rows()
|
||||
{
|
||||
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);
|
||||
await mgr.InitializeAsync();
|
||||
var source = (await store.Sources.GetAllAsync()).Single();
|
||||
await store.Entries.UpsertAsync(new IndexEntry
|
||||
{
|
||||
SourceId = source.Id,
|
||||
Name = "Photos",
|
||||
NameNorm = "photos",
|
||||
IsDirectory = true,
|
||||
PathRel = "Photos",
|
||||
LastSeenUtc = DateTimeOffset.UtcNow
|
||||
});
|
||||
|
||||
Assert.False(mgr.CanForget(source));
|
||||
Assert.False(await mgr.ForgetDisconnectedAsync(@"Z:\"));
|
||||
|
||||
volumes.Online = [];
|
||||
await mgr.RefreshOnlineStateAsync();
|
||||
source = (await store.Sources.GetAllAsync()).Single();
|
||||
Assert.True(mgr.CanForget(source));
|
||||
Assert.True(await mgr.ForgetDisconnectedAsync(@"Z:\"));
|
||||
Assert.Empty(await store.Sources.GetAllAsync());
|
||||
Assert.Null(await store.Entries.GetRootAsync(source.Id));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Forget_removes_unc_from_recents()
|
||||
{
|
||||
var dir = Path.Combine(Path.GetTempPath(), "ew-app", Guid.NewGuid().ToString("N"));
|
||||
var db = Path.Combine(dir, "index.db");
|
||||
await using var store = new SqliteIndexStore(db, NullLogger<SqliteIndexStore>.Instance);
|
||||
var volumes = new FakeVolumes();
|
||||
var env = new FakeEnv(dir);
|
||||
File.WriteAllLines(Path.Combine(dir, "recents.txt"), [@"\\old-server\share"]);
|
||||
var mgr = new SourceManager(store, volumes, env, new SystemClock(), NullLogger<SourceManager>.Instance);
|
||||
await mgr.InitializeAsync();
|
||||
Assert.Single(await store.Sources.GetAllAsync());
|
||||
Assert.True(await mgr.ForgetDisconnectedAsync(@"\\old-server\share"));
|
||||
Assert.Empty(await store.Sources.GetAllAsync());
|
||||
Assert.Empty(File.ReadAllLines(Path.Combine(dir, "recents.txt")));
|
||||
}
|
||||
}
|
||||
|
||||
file sealed class FakeVolumes : IVolumeService
|
||||
|
||||
69
tests/Explorer.Application.Tests/UiPreferencesStoreTests.cs
Normal file
69
tests/Explorer.Application.Tests/UiPreferencesStoreTests.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
using Explorer.Application;
|
||||
using Explorer.Domain.Abstractions;
|
||||
|
||||
namespace Explorer.Application.Tests;
|
||||
|
||||
public class UiPreferencesStoreTests
|
||||
{
|
||||
[Fact]
|
||||
public void Parse_reads_theme_and_independent_group_flags()
|
||||
{
|
||||
var prefs = UiPreferencesStore.Parse(
|
||||
[
|
||||
"theme=Light",
|
||||
"group-network=true",
|
||||
"group-cloud=false",
|
||||
"index-archives=true"
|
||||
]);
|
||||
Assert.Equal("Light", prefs.Theme);
|
||||
Assert.True(prefs.GroupNetworkPlaces);
|
||||
Assert.False(prefs.GroupCloudPlaces);
|
||||
Assert.True(prefs.IndexArchiveContents);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_defaults_missing_keys()
|
||||
{
|
||||
var prefs = UiPreferencesStore.Parse(["theme=dark"]);
|
||||
Assert.Equal("Dark", prefs.Theme);
|
||||
Assert.False(prefs.GroupNetworkPlaces);
|
||||
Assert.False(prefs.GroupCloudPlaces);
|
||||
Assert.False(prefs.IndexArchiveContents);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Load_and_save_roundtrip()
|
||||
{
|
||||
var dir = Path.Combine(Path.GetTempPath(), "ew-ui-prefs", Guid.NewGuid().ToString("N"));
|
||||
try
|
||||
{
|
||||
var store = new UiPreferencesStore(new PrefsEnv(dir));
|
||||
store.Save(new UiPreferences("Light", true, false, true));
|
||||
var loaded = store.Load();
|
||||
Assert.Equal("Light", loaded.Theme);
|
||||
Assert.True(loaded.GroupNetworkPlaces);
|
||||
Assert.False(loaded.GroupCloudPlaces);
|
||||
Assert.True(loaded.IndexArchiveContents);
|
||||
}
|
||||
finally
|
||||
{
|
||||
try { Directory.Delete(dir, true); } catch { /* ignore */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
file sealed class PrefsEnv : IAppEnvironment
|
||||
{
|
||||
public PrefsEnv(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