Persist file categories on the index and classify archives and unknown types during idle maintenance.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
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 EntryClassificationServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Archive_children_override_zip_as_photos()
|
||||
{
|
||||
await using var store = await OpenStore();
|
||||
var source = await AddSource(store, @"C:\media");
|
||||
var zip = await Upsert(store, new IndexEntry
|
||||
{
|
||||
SourceId = source.Id,
|
||||
Name = "photos.zip",
|
||||
NameNorm = "photos.zip",
|
||||
Extension = "zip",
|
||||
PathRel = "photos.zip",
|
||||
LastSeenUtc = DateTimeOffset.UtcNow
|
||||
});
|
||||
await Upsert(store, Child(source.Id, zip.Id, "a.jpg", "jpg"));
|
||||
await Upsert(store, Child(source.Id, zip.Id, "b.jpg", "jpg"));
|
||||
await Upsert(store, Child(source.Id, zip.Id, "c.jpg", "jpg"));
|
||||
|
||||
var service = Create(store, new MemoryEnumerator());
|
||||
service.Resume();
|
||||
Assert.True(await service.ProcessPendingAsync());
|
||||
var loaded = await store.Entries.GetAsync(zip.Id);
|
||||
Assert.Equal(FileCategory.Photos, loaded!.Category);
|
||||
Assert.Equal(CategorySources.ArchiveContents, loaded.CategorySource);
|
||||
Assert.False(await service.ProcessPendingAsync());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Signature_classifies_jpeg_without_extension()
|
||||
{
|
||||
var root = Path.Combine(Path.GetTempPath(), "ew-classify", Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(root);
|
||||
var file = Path.Combine(root, "pic.bin");
|
||||
File.WriteAllBytes(file, [0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01]);
|
||||
await using var store = await OpenStore();
|
||||
var source = await AddSource(store, root);
|
||||
var entry = await Upsert(store, new IndexEntry
|
||||
{
|
||||
SourceId = source.Id,
|
||||
Name = "pic.bin",
|
||||
NameNorm = "pic.bin",
|
||||
Extension = "bin",
|
||||
PathRel = "pic.bin",
|
||||
LastSeenUtc = DateTimeOffset.UtcNow
|
||||
});
|
||||
Assert.Equal(FileCategory.Unknown, entry.Category);
|
||||
|
||||
var enumerator = new MemoryEnumerator();
|
||||
enumerator.Add(new FileSystemItem { FullPath = file, Name = "pic.bin" });
|
||||
var service = Create(store, enumerator);
|
||||
service.Resume();
|
||||
Assert.True(await service.ProcessPendingAsync());
|
||||
var loaded = await store.Entries.GetAsync(entry.Id);
|
||||
Assert.Equal(FileCategory.Photos, loaded!.Category);
|
||||
Assert.Equal(CategorySources.Mime, loaded.CategorySource);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Pause_skips_work()
|
||||
{
|
||||
await using var store = await OpenStore();
|
||||
var source = await AddSource(store, @"C:\media");
|
||||
var zip = await Upsert(store, new IndexEntry
|
||||
{
|
||||
SourceId = source.Id,
|
||||
Name = "photos.zip",
|
||||
NameNorm = "photos.zip",
|
||||
Extension = "zip",
|
||||
PathRel = "photos.zip",
|
||||
LastSeenUtc = DateTimeOffset.UtcNow
|
||||
});
|
||||
await Upsert(store, Child(source.Id, zip.Id, "a.jpg", "jpg"));
|
||||
var service = Create(store, new MemoryEnumerator());
|
||||
Assert.True(service.IsPaused);
|
||||
Assert.False(await service.ProcessPendingAsync());
|
||||
var loaded = await store.Entries.GetAsync(zip.Id);
|
||||
Assert.Equal(FileCategory.Archive, loaded!.Category);
|
||||
}
|
||||
|
||||
private static EntryClassificationService Create(IIndexStore store, IFileSystemEnumerator enumerator)
|
||||
=> new(
|
||||
store,
|
||||
enumerator,
|
||||
new NeverHydrate(),
|
||||
NullLogger<EntryClassificationService>.Instance);
|
||||
|
||||
private static async Task<SqliteIndexStore> OpenStore()
|
||||
{
|
||||
var db = Path.Combine(Path.GetTempPath(), "ew-classify", Guid.NewGuid().ToString("N"), "index.db");
|
||||
var store = new SqliteIndexStore(db, NullLogger<SqliteIndexStore>.Instance);
|
||||
await store.OpenAsync();
|
||||
return store;
|
||||
}
|
||||
|
||||
private static async Task<Source> AddSource(IIndexStore store, string root)
|
||||
{
|
||||
var source = new Source
|
||||
{
|
||||
StableKey = Guid.NewGuid().ToString("N"),
|
||||
Kind = SourceKind.NtfsLocal,
|
||||
DisplayName = "Test",
|
||||
LastRootPath = root,
|
||||
Status = SourceStatus.Online
|
||||
};
|
||||
source.Id = await store.Sources.UpsertAsync(source);
|
||||
return source;
|
||||
}
|
||||
|
||||
private static async Task<IndexEntry> Upsert(IIndexStore store, IndexEntry entry)
|
||||
{
|
||||
entry.Id = await store.Entries.UpsertAsync(entry);
|
||||
return (await store.Entries.GetAsync(entry.Id))!;
|
||||
}
|
||||
|
||||
private static IndexEntry Child(long sourceId, long parentId, string name, string ext)
|
||||
=> new()
|
||||
{
|
||||
SourceId = sourceId,
|
||||
ParentId = parentId,
|
||||
Name = name,
|
||||
NameNorm = name,
|
||||
Extension = ext,
|
||||
PathRel = name,
|
||||
LastSeenUtc = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
private sealed class NeverHydrate : IHydrationGuard
|
||||
{
|
||||
public bool WouldHydrateOnRead(FileSystemItem item) => false;
|
||||
public bool WouldHydrateOnRead(int attributes, CloudAvailability? availability) => false;
|
||||
public Task<bool> WouldHydrateOnReadAsync(string path, CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult(false);
|
||||
}
|
||||
|
||||
private sealed class MemoryEnumerator : IFileSystemEnumerator
|
||||
{
|
||||
private readonly Dictionary<string, FileSystemItem> _items = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public void Add(FileSystemItem item) => _items[item.FullPath] = item;
|
||||
|
||||
public IEnumerable<FileSystemItem> EnumerateChildren(string directoryPath) => [];
|
||||
public FileSystemItem? GetItem(string path)
|
||||
=> _items.TryGetValue(path, out var item) ? item : null;
|
||||
public IReadOnlyList<FileSystemItem> EnumerateChildrenSafe(string directoryPath, out string? error)
|
||||
{
|
||||
error = null;
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user