using Explorer.Domain; using Explorer.Domain.Abstractions; namespace Explorer.Analysis; public sealed class AnalysisService { private readonly IIndexStore _store; private readonly AnalysisResultCache _cache = new(); private readonly SemaphoreSlim _ready = new(1, 1); private bool _readyDone; public AnalysisService(IIndexStore store) => _store = store; public Task EnsureReadyAsync(CancellationToken cancellationToken = default) => RunOffUiAsync(async ct => { if (_readyDone) { return 0; } await _ready.WaitAsync(ct).ConfigureAwait(false); try { if (!_readyDone) { await _store.Analysis.EnsureReadyAsync(CancellationToken.None).ConfigureAwait(false); _readyDone = true; } } finally { _ready.Release(); } return 0; }, cancellationToken); public Task GetIndexStampAsync(CancellationToken cancellationToken = default) => RunOffUiAsync(ct => _store.Analysis.GetIndexStampAsync(ct), cancellationToken); public Task> GetDirectoryRootsAsync(CancellationToken cancellationToken = default) => CachedAsync("roots", ct => _store.Analysis.GetDirectoryRootsAsync(ct), cancellationToken); public Task> LargestDirectoriesAsync( long? sourceId, long? parentId, int take = AppConstants.AnalysisTopN, CancellationToken cancellationToken = default) => CachedAsync( $"dirs:{sourceId}:{parentId}:{take}", ct => _store.Analysis.LargestDirectoriesAsync(sourceId, parentId, take, ct), cancellationToken); public Task> LargestFilesAsync( long? sourceId, string? pathRelPrefix, int take = AppConstants.AnalysisTopN, CancellationToken cancellationToken = default) => CachedAsync( $"files:{sourceId}:{pathRelPrefix}:{take}", ct => _store.Analysis.LargestFilesAsync(sourceId, pathRelPrefix, take, ct), cancellationToken); public Task> UsageByExtensionAsync( long? sourceId, string? pathRelPrefix, int take = AppConstants.AnalysisTopN, CancellationToken cancellationToken = default) => CachedAsync( $"types:{sourceId}:{pathRelPrefix}:{take}", ct => _store.Analysis.UsageByExtensionAsync(sourceId, pathRelPrefix, take, ct), cancellationToken); public Task> UsageBySourceAsync(CancellationToken cancellationToken = default) => CachedAsync("sources", ct => _store.Analysis.UsageBySourceAsync(ct), cancellationToken); public Task> DrilldownAsync( long parentId, int take = AppConstants.AnalysisTopN, CancellationToken cancellationToken = default) => CachedAsync( $"children:{parentId}:{take}", ct => _store.Analysis.ChildrenBySizeAsync(parentId, take, ct), cancellationToken); public Task> GetKnownSourcesAsync(CancellationToken cancellationToken = default) => RunOffUiAsync(ct => _store.Sources.GetAllAsync(ct), cancellationToken); public Task> GetClassifiedDuplicatesAsync( int take = 200, CancellationToken cancellationToken = default) => RunOffUiAsync(async ct => { var raw = await _store.Hashes.GetDuplicateGroupsAsync(null, null, Math.Max(take * 8, 400), ct) .ConfigureAwait(false); var ids = raw.SelectMany(g => g.Entries.Select(e => e.Id)).Distinct().ToList(); var relations = await _store.Relations.GetAmongAsync(ids, ct).ConfigureAwait(false); return (IReadOnlyList)raw .Select(g => DuplicateClassifier.ClassifyGroup(g, DuplicateClassifier.RelationsFor(g.Entries, relations))) .ToList(); }, cancellationToken); public Task MarkDuplicateGroupAsync( IReadOnlyList entries, FileRelationKind kind, CancellationToken cancellationToken = default) => RunOffUiAsync(async ct => { var ids = entries.Select(e => e.Id).Distinct().ToList(); await _store.Relations.DeleteAmongAsync( ids, [FileRelationKind.AccidentalDuplicate, FileRelationKind.IntentionalDuplicate], ct) .ConfigureAwait(false); var utc = DateTimeOffset.UtcNow; foreach (var (left, right) in DuplicateClassifier.Pairs(entries)) { await _store.Relations.UpsertAsync(new FileRelation { LeftEntryId = left, RightEntryId = right, Kind = kind, Origin = FileRelationOrigin.User, CreatedUtc = utc }, ct).ConfigureAwait(false); } return 0; }, cancellationToken); private async Task CachedAsync(string key, Func> query, CancellationToken cancellationToken) { await EnsureReadyAsync(cancellationToken).ConfigureAwait(false); var stamp = await GetIndexStampAsync(cancellationToken).ConfigureAwait(false); if (_cache.TryGet(key, stamp, out var hit)) { return hit; } var value = await RunOffUiAsync(query, cancellationToken).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); _cache.Set(key, stamp, value); return value; } public static async Task RunOffUiAsync(Func> work, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); if (SynchronizationContext.Current is null) { return await work(cancellationToken).ConfigureAwait(false); } return await Task.Run(async () => await work(cancellationToken).ConfigureAwait(false), cancellationToken) .ConfigureAwait(false); } }