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:
@@ -161,6 +161,107 @@ public class ScannerTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Full_scan_indexes_archive_members_without_double_counting_folder_size()
|
||||
{
|
||||
var root = CreateTree();
|
||||
var zipPath = Path.Combine(root, "pack.zip");
|
||||
CreateZip(zipPath, ("docs/inner.txt", new string('x', 40)));
|
||||
try
|
||||
{
|
||||
await using var store = await OpenStore();
|
||||
var source = await AddSource(store, root);
|
||||
var indexer = CreateIndexer(store, enabled: true);
|
||||
var scanner = new FilesystemScanner(store, new IoEnumerator(), new StorageProviderRegistry([], NullLogger<StorageProviderRegistry>.Instance), NullLogger<FilesystemScanner>.Instance, indexer);
|
||||
await scanner.ScanAsync(source, ScanKind.Full, null, null, CancellationToken.None);
|
||||
|
||||
var inner = await store.Entries.GetByPathAsync(source.Id, @"pack.zip\docs\inner.txt");
|
||||
Assert.NotNull(inner);
|
||||
Assert.Equal(40, inner.SizeBytes);
|
||||
Assert.False(inner.IsDirectory);
|
||||
|
||||
var zip = await store.Entries.GetByPathAsync(source.Id, "pack.zip");
|
||||
Assert.NotNull(zip);
|
||||
Assert.False(zip.IsDirectory);
|
||||
Assert.Equal(new FileInfo(zipPath).Length, zip.SizeBytes);
|
||||
|
||||
var scanRoot = await store.Entries.GetRootAsync(source.Id);
|
||||
Assert.Equal(100 + 50 + zip.SizeBytes, scanRoot!.AggregateSize);
|
||||
}
|
||||
finally
|
||||
{
|
||||
TryDelete(root);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Full_scan_skips_archive_contents_when_setting_is_off()
|
||||
{
|
||||
var root = CreateTree();
|
||||
CreateZip(Path.Combine(root, "pack.zip"), ("inner.txt", "hello"));
|
||||
try
|
||||
{
|
||||
await using var store = await OpenStore();
|
||||
var source = await AddSource(store, root);
|
||||
var scanner = new FilesystemScanner(store, new IoEnumerator(), new StorageProviderRegistry([], NullLogger<StorageProviderRegistry>.Instance), NullLogger<FilesystemScanner>.Instance);
|
||||
await scanner.ScanAsync(source, ScanKind.Full, null, null, CancellationToken.None);
|
||||
Assert.Null(await store.Entries.GetByPathAsync(source.Id, @"pack.zip\inner.txt"));
|
||||
Assert.NotNull(await store.Entries.GetByPathAsync(source.Id, "pack.zip"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
TryDelete(root);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Folder_reconcile_tombs_archive_members_when_zip_is_removed()
|
||||
{
|
||||
var root = CreateTree();
|
||||
var zipPath = Path.Combine(root, "pack.zip");
|
||||
CreateZip(zipPath, ("inner.txt", "hello"));
|
||||
try
|
||||
{
|
||||
await using var store = await OpenStore();
|
||||
var source = await AddSource(store, root);
|
||||
var indexer = CreateIndexer(store, enabled: true);
|
||||
var scanner = new FilesystemScanner(store, new IoEnumerator(), new StorageProviderRegistry([], NullLogger<StorageProviderRegistry>.Instance), NullLogger<FilesystemScanner>.Instance, indexer);
|
||||
await scanner.ScanAsync(source, ScanKind.Full, null, null, CancellationToken.None);
|
||||
File.Delete(zipPath);
|
||||
var reconciler = new FolderReconciler(store, new IoEnumerator(), new StorageProviderRegistry([], NullLogger<StorageProviderRegistry>.Instance), indexer);
|
||||
await reconciler.ReconcileAsync(source, "", CancellationToken.None);
|
||||
var zip = await store.Entries.GetByPathAsync(source.Id, "pack.zip");
|
||||
var inner = await store.Entries.GetByPathAsync(source.Id, @"pack.zip\inner.txt");
|
||||
Assert.Equal(EntryStatus.Deleted, zip!.Status);
|
||||
Assert.Equal(EntryStatus.Deleted, inner!.Status);
|
||||
}
|
||||
finally
|
||||
{
|
||||
TryDelete(root);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Archive_catalog_lists_zip_entry_sizes_without_extracting()
|
||||
{
|
||||
var dir = Path.Combine(Path.GetTempPath(), "ew-scan", Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(dir);
|
||||
var zipPath = Path.Combine(dir, "pack.zip");
|
||||
CreateZip(zipPath, ("docs/inner.txt", new string('z', 25)));
|
||||
try
|
||||
{
|
||||
var members = new ArchiveCatalog().TryList(zipPath);
|
||||
var inner = Assert.Single(members, m => m.RelativePath.Equals(@"docs\inner.txt", StringComparison.OrdinalIgnoreCase));
|
||||
Assert.False(inner.IsDirectory);
|
||||
Assert.Equal(25, inner.SizeBytes);
|
||||
Assert.DoesNotContain(Directory.GetFiles(dir, "*", SearchOption.AllDirectories), p => p.EndsWith("inner.txt", StringComparison.OrdinalIgnoreCase) && !p.EndsWith(".zip", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
finally
|
||||
{
|
||||
TryDelete(dir);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Folder_reconcile_tombs_removed_file()
|
||||
{
|
||||
@@ -218,4 +319,46 @@ public class ScannerTests
|
||||
{
|
||||
try { Directory.Delete(root, true); } catch { /* ignore */ }
|
||||
}
|
||||
|
||||
private static void CreateZip(string zipPath, params (string Name, string Content)[] files)
|
||||
{
|
||||
using var zip = System.IO.Compression.ZipFile.Open(zipPath, System.IO.Compression.ZipArchiveMode.Create);
|
||||
foreach (var (name, content) in files)
|
||||
{
|
||||
var entry = zip.CreateEntry(name);
|
||||
using var stream = entry.Open();
|
||||
using var writer = new StreamWriter(stream);
|
||||
writer.Write(content);
|
||||
}
|
||||
}
|
||||
|
||||
private static ArchiveContentsIndexer CreateIndexer(IIndexStore store, bool enabled)
|
||||
{
|
||||
var dir = Path.Combine(Path.GetTempPath(), "ew-scan-prefs", Guid.NewGuid().ToString("N"));
|
||||
var prefs = new UiPreferencesStore(new ScanPrefsEnv(dir));
|
||||
prefs.Save(new UiPreferences("Dark", false, false, enabled));
|
||||
return new ArchiveContentsIndexer(
|
||||
store,
|
||||
new IoEnumerator(),
|
||||
new HydrationGuard(new StorageProviderRegistry([], NullLogger<StorageProviderRegistry>.Instance)),
|
||||
new ArchiveCatalog(),
|
||||
prefs,
|
||||
NullLogger<ArchiveContentsIndexer>.Instance);
|
||||
}
|
||||
}
|
||||
|
||||
file sealed class ScanPrefsEnv : IAppEnvironment
|
||||
{
|
||||
public ScanPrefsEnv(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