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:
2026-08-22 12:43:05 +02:00
commit e9aba73552
130 changed files with 15110 additions and 0 deletions

View File

@@ -0,0 +1,180 @@
namespace Explorer.Domain.Abstractions;
public interface IIndexStore
{
Task OpenAsync(CancellationToken cancellationToken = default);
Task CloseAsync();
Task<string> QuickCheckAsync(CancellationToken cancellationToken = default);
ISourceStore Sources { get; }
IEntryStore Entries { get; }
IExcludeStore Excludes { get; }
IScanJobStore ScanJobs { get; }
ITransferStore Transfers { get; }
ISearchStore Search { get; }
IAnalysisStore Analysis { get; }
IHistoryStore History { get; }
IHashStore Hashes { get; }
Task RunWriteAsync(Func<IIndexStore, Task> work, CancellationToken cancellationToken = default);
Task<T> RunWriteAsync<T>(Func<IIndexStore, Task<T>> work, CancellationToken cancellationToken = default);
}
public interface ISourceStore
{
Task<IReadOnlyList<Source>> GetAllAsync(CancellationToken cancellationToken = default);
Task<Source?> GetAsync(long id, CancellationToken cancellationToken = default);
Task<Source?> GetByStableKeyAsync(string key, CancellationToken cancellationToken = default);
Task<long> UpsertAsync(Source source, CancellationToken cancellationToken = default);
Task UpdateStatusAsync(long id, SourceStatus status, string? error, CancellationToken cancellationToken = default);
Task UpdateUsnAsync(long id, long journalId, long nextUsn, CancellationToken cancellationToken = default);
Task UpdateIndexedAsync(long id, DateTimeOffset utc, long generation, CancellationToken cancellationToken = default);
Task SetLastSeenAsync(long id, string rootPath, DateTimeOffset utc, CancellationToken cancellationToken = default);
}
public interface IEntryStore
{
Task<IndexEntry?> GetAsync(long id, CancellationToken cancellationToken = default);
Task<IndexEntry?> GetRootAsync(long sourceId, CancellationToken cancellationToken = default);
Task<IndexEntry?> GetByPathAsync(long sourceId, string pathRel, CancellationToken cancellationToken = default);
Task<IndexEntry?> GetByParentNameAsync(long sourceId, long? parentId, string nameNorm, CancellationToken cancellationToken = default);
Task<IndexEntry?> GetByFileIdAsync(long sourceId, long fileId, CancellationToken cancellationToken = default);
Task<IReadOnlyList<IndexEntry>> GetChildrenAsync(long sourceId, long? parentId, EntryStatus? status = EntryStatus.Present, CancellationToken cancellationToken = default);
Task UpsertBatchAsync(IReadOnlyList<IndexEntry> entries, CancellationToken cancellationToken = default);
Task<long> UpsertAsync(IndexEntry entry, CancellationToken cancellationToken = default);
Task UpdateAggregatesAsync(long id, long aggregateSize, int files, int dirs, CancellationToken cancellationToken = default);
Task ApplySizeDeltaToAncestorsAsync(long? parentId, long sizeDelta, int fileDelta, int dirDelta, CancellationToken cancellationToken = default);
Task MarkMissingAsDeletedAsync(long sourceId, long generation, DateTimeOffset utc, string? pathRelPrefix, CancellationToken cancellationToken = default);
Task MarkSourceOfflineAsync(long sourceId, CancellationToken cancellationToken = default);
Task MarkSourceOnlinePresentAsync(long sourceId, CancellationToken cancellationToken = default);
Task TombstoneAsync(long id, DateTimeOffset utc, CancellationToken cancellationToken = default);
Task DeleteExpiredTombstonesAsync(DateTimeOffset cutoffUtc, CancellationToken cancellationToken = default);
Task RenameSubtreePathAsync(long sourceId, string oldPathRel, string newPathRel, CancellationToken cancellationToken = default);
Task<long> CountPresentAsync(long sourceId, CancellationToken cancellationToken = default);
}
public interface IExcludeStore
{
Task<IReadOnlyList<ExcludeRule>> GetAllAsync(CancellationToken cancellationToken = default);
Task<long> AddAsync(ExcludeRule rule, CancellationToken cancellationToken = default);
Task RemoveAsync(long id, CancellationToken cancellationToken = default);
Task EnsureDefaultsAsync(IReadOnlyList<ExcludeRule> defaults, CancellationToken cancellationToken = default);
}
public interface IScanJobStore
{
Task<long> InsertAsync(ScanJob job, CancellationToken cancellationToken = default);
Task UpdateAsync(ScanJob job, CancellationToken cancellationToken = default);
Task InterruptRunningAsync(CancellationToken cancellationToken = default);
Task AddErrorAsync(ScanError error, CancellationToken cancellationToken = default);
Task<ScanJob?> GetAsync(long id, CancellationToken cancellationToken = default);
Task<IReadOnlyList<ScanJob>> GetRecentAsync(int take, CancellationToken cancellationToken = default);
}
public interface ITransferStore
{
Task<long> InsertAsync(TransferJob job, CancellationToken cancellationToken = default);
Task UpdateAsync(TransferJob job, CancellationToken cancellationToken = default);
}
public interface ISearchStore
{
Task<IReadOnlyList<IndexEntry>> SearchAsync(SearchRequest request, CancellationToken cancellationToken = default);
}
public sealed class SearchRequest
{
public string? Name { get; init; }
public string? Extension { get; init; }
public bool? IsDirectory { get; init; }
public long? MinSize { get; init; }
public long? MaxSize { get; init; }
public DateTimeOffset? CreatedAfter { get; init; }
public DateTimeOffset? CreatedBefore { get; init; }
public DateTimeOffset? ModifiedAfter { get; init; }
public DateTimeOffset? ModifiedBefore { get; init; }
public IReadOnlyList<long>? SourceIds { get; init; }
public string? PathRelPrefix { get; init; }
public bool DirectChildrenOnly { get; init; }
public bool IncludeOffline { get; init; } = true;
public bool IncludeDeleted { get; init; }
public int Skip { get; init; }
public int Take { get; init; } = 500;
}
public interface IAnalysisStore
{
Task EnsureReadyAsync(CancellationToken cancellationToken = default);
Task<long> GetIndexStampAsync(CancellationToken cancellationToken = default);
Task<IReadOnlyList<IndexEntry>> GetDirectoryRootsAsync(CancellationToken cancellationToken = default);
Task<IReadOnlyList<IndexEntry>> LargestDirectoriesAsync(long? sourceId, long? parentId, int take, CancellationToken cancellationToken = default);
Task<IReadOnlyList<IndexEntry>> LargestFilesAsync(long? sourceId, string? pathRelPrefix, int take, CancellationToken cancellationToken = default);
Task<IReadOnlyList<ExtensionUsage>> UsageByExtensionAsync(long? sourceId, string? pathRelPrefix, int take, CancellationToken cancellationToken = default);
Task<IReadOnlyList<SourceUsage>> UsageBySourceAsync(CancellationToken cancellationToken = default);
Task<IReadOnlyList<IndexEntry>> ChildrenBySizeAsync(long parentId, int take, CancellationToken cancellationToken = default);
}
public sealed class ExtensionUsage
{
public required string Extension { get; init; }
public long TotalSize { get; init; }
public long FileCount { get; init; }
}
public sealed class SourceUsage
{
public long SourceId { get; init; }
public required string DisplayName { get; init; }
public long TotalSize { get; init; }
public long FileCount { get; init; }
public SourceStatus Status { get; init; }
}
public interface IHistoryStore
{
Task CaptureSourceSnapshotAsync(long sourceId, DateTimeOffset utc, CancellationToken cancellationToken = default);
Task CaptureDirectorySnapshotsAsync(long sourceId, DateTimeOffset utc, long minSize, int topN, CancellationToken cancellationToken = default);
Task<IReadOnlyList<SourceSnapshot>> GetSourceHistoryAsync(long sourceId, CancellationToken cancellationToken = default);
}
public sealed class SourceSnapshot
{
public DateTimeOffset CapturedUtc { get; init; }
public long TotalSize { get; init; }
public long FileCount { get; init; }
public long DirCount { get; init; }
}
public interface IHashStore
{
Task EnqueueSizeCollisionsAsync(long? sourceId, CancellationToken cancellationToken = default);
Task<IReadOnlyList<HashWorkItem>> DequeueAsync(int take, CancellationToken cancellationToken = default);
Task CompletePartialAsync(long entryId, byte[] hash, CancellationToken cancellationToken = default);
Task CompleteFullAsync(long entryId, byte[] hash, CancellationToken cancellationToken = default);
Task MarkUniquePartialAsync(long entryId, CancellationToken cancellationToken = default);
Task MarkErrorAsync(long entryId, CancellationToken cancellationToken = default);
Task MarkSkippedAsync(long entryId, CancellationToken cancellationToken = default);
Task<bool> HasPartialCollisionAsync(long entryId, long sizeBytes, CancellationToken cancellationToken = default);
Task<IReadOnlyList<DuplicateGroup>> GetDuplicateGroupsAsync(long? sourceId, string? pathPrefix, int take, CancellationToken cancellationToken = default);
}
public sealed class HashWorkItem
{
public long EntryId { get; init; }
public long SizeBytes { get; init; }
public required string State { get; init; }
public long SourceId { get; init; }
public required string PathRel { get; init; }
public string? RootPath { get; init; }
public long? FileId { get; init; }
public int Attributes { get; init; }
public CloudAvailability? CloudAvailability { get; init; }
}
public sealed class DuplicateGroup
{
public long SizeBytes { get; init; }
public byte[]? Hash { get; init; }
public IReadOnlyList<IndexEntry> Entries { get; init; } = [];
public bool SameFileId { get; init; }
}