160 lines
6.3 KiB
C#
160 lines
6.3 KiB
C#
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<long> GetIndexStampAsync(CancellationToken cancellationToken = default)
|
|
=> RunOffUiAsync(ct => _store.Analysis.GetIndexStampAsync(ct), cancellationToken);
|
|
|
|
public Task<IReadOnlyList<IndexEntry>> GetDirectoryRootsAsync(CancellationToken cancellationToken = default)
|
|
=> CachedAsync("roots", ct => _store.Analysis.GetDirectoryRootsAsync(ct), cancellationToken);
|
|
|
|
public Task<IReadOnlyList<IndexEntry>> 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<IReadOnlyList<IndexEntry>> 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<IReadOnlyList<ExtensionUsage>> 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<IReadOnlyList<SourceUsage>> UsageBySourceAsync(CancellationToken cancellationToken = default)
|
|
=> CachedAsync("sources", ct => _store.Analysis.UsageBySourceAsync(ct), cancellationToken);
|
|
|
|
public Task<IReadOnlyList<IndexEntry>> DrilldownAsync(
|
|
long parentId,
|
|
int take = AppConstants.AnalysisTopN,
|
|
CancellationToken cancellationToken = default)
|
|
=> CachedAsync(
|
|
$"children:{parentId}:{take}",
|
|
ct => _store.Analysis.ChildrenBySizeAsync(parentId, take, ct),
|
|
cancellationToken);
|
|
|
|
public Task<IReadOnlyList<Source>> GetKnownSourcesAsync(CancellationToken cancellationToken = default)
|
|
=> RunOffUiAsync(ct => _store.Sources.GetAllAsync(ct), cancellationToken);
|
|
|
|
public Task<IReadOnlyList<ClassifiedDuplicateGroup>> 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<ClassifiedDuplicateGroup>)raw
|
|
.Select(g => DuplicateClassifier.ClassifyGroup(g, DuplicateClassifier.RelationsFor(g.Entries, relations)))
|
|
.ToList();
|
|
}, cancellationToken);
|
|
|
|
public Task MarkDuplicateGroupAsync(
|
|
IReadOnlyList<IndexEntry> 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<T> CachedAsync<T>(string key, Func<CancellationToken, Task<T>> query, CancellationToken cancellationToken)
|
|
{
|
|
await EnsureReadyAsync(cancellationToken).ConfigureAwait(false);
|
|
var stamp = await GetIndexStampAsync(cancellationToken).ConfigureAwait(false);
|
|
if (_cache.TryGet<T>(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<T> RunOffUiAsync<T>(Func<CancellationToken, Task<T>> 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);
|
|
}
|
|
}
|