Files
SharepointToolbox-Web/Components/Pages/Duplicates.razor
T

140 lines
7.1 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@page "/duplicates"
@attribute [Authorize]
@inject IUserSessionService Session
@inject ISessionManager SessionMgr
@inject IElevationCoordinator Elevation
@inject IDuplicatesService DupSvc
@inject DuplicatesCsvExportService CsvExport
@inject DuplicatesHtmlExportService HtmlExport
@inject WebExportService WebExport
@inject IAuditService Audit
@inject TranslationSource T
@rendermode InteractiveServer
<h1 class="page-title">@T["duplicates.page.title"]</h1>
@if (!Session.HasProfile) { <NoProfilePrompt /> return; }
<div class="card">
<SitePicker Profile="Session.CurrentProfile!" @bind-SelectedSites="_sites" />
<div class="form-row mt-8">
<div class="form-group">
<label class="form-label">@T["duplicates.lbl.mode"]</label>
<select class="form-select" @bind="_mode" style="width:120px">
<option value="Files">@T["duplicates.mode.files"]</option>
<option value="Folders">@T["duplicates.mode.folders"]</option>
</select>
</div>
<LibraryPicker Profile="Session.CurrentProfile!" SiteUrl="@(_sites.FirstOrDefault()?.Url)" @bind-Library="_library" Label="@T["duplicates.lbl.library_optional"]" Placeholder="" />
</div>
<div class="flex-row">
<label><input type="checkbox" @bind="_matchSize" /> @T["duplicates.chk.match_size"]</label>
<label><input type="checkbox" @bind="_matchCreated" /> @T["duplicates.chk.match_created"]</label>
<label><input type="checkbox" @bind="_matchModified" /> @T["duplicates.chk.match_modified"]</label>
@if (_mode == "Folders")
{
<label><input type="checkbox" @bind="_matchFolderCount" /> @T["duplicates.chk.match_folder_count"]</label>
<label><input type="checkbox" @bind="_matchFileCount" /> @T["duplicates.chk.match_file_count"]</label>
}
</div>
<div class="flex-row mt-8">
<button class="btn btn-primary" @onclick="RunScan" disabled="@_running">
@(_running ? T["duplicates.btn.scanning"] : T["duplicates.btn.find"])
</button>
@if (_running) { <button class="btn btn-secondary" @onclick="Cancel">@T["btn.cancel"]</button> }
</div>
<ProgressPanel IsRunning="_running" StatusMessage="@_status" Current="_current" Total="_total" />
</div>
@if (!string.IsNullOrEmpty(_error)) { <div class="alert alert-error">@_error</div> }
@if (_results.Count > 0)
{
<div class="card">
<div class="flex-row">
<div class="card-title">@T["duplicates.results.title"] <span class="count-badge">@_results.Count</span></div>
<div class="spacer"></div>
<MergeModeSelect Value="_mergeMode" ValueChanged="v => _mergeMode = v" Visible="_bySite.Count > 1" />
<button class="btn btn-secondary btn-sm" @onclick="ExportCsv">@T["audit.btn.exportCsv"]</button>
<button class="btn btn-secondary btn-sm" @onclick="ExportHtml">@T["audit.btn.exportHtml"]</button>
</div>
@foreach (var g in _results.Take(100))
{
<div style="margin-bottom:8px;border:1px solid var(--border);border-radius:4px;overflow:hidden">
<div style="background:#f0f0f0;padding:6px 12px;font-weight:600;font-size:13px">
@g.Name <span class="chip chip-blue">@g.Items.Count @T["report.text.copies"]</span>
</div>
@foreach (var item in g.Items)
{
<div style="padding:4px 12px;font-size:12px;border-top:1px solid var(--border)">
<span style="color:var(--text-muted)">@item.Library</span> @item.Path
@if (item.SizeBytes.HasValue) { <span class="text-muted"> (@((item.SizeBytes.Value/1024.0).ToString("F1")) KB)</span> }
</div>
}
</div>
}
@if (_results.Count > 100) { <div class="text-muted mt-8">@T["duplicates.results.truncated"]</div> }
</div>
}
@code {
private List<SiteInfo> _sites = new();
private string _library = string.Empty, _mode = "Files";
private bool _matchSize = true, _matchCreated, _matchModified, _matchFolderCount, _matchFileCount;
private bool _running; private string _status = string.Empty, _error = string.Empty;
private int _current, _total;
private List<DuplicateGroup> _results = new();
private List<(string Label, IReadOnlyList<DuplicateGroup> Results)> _bySite = new();
private ReportMergeMode _mergeMode = ReportMergeMode.SingleMerged;
private CancellationTokenSource? _cts;
private async Task RunScan()
{
_error = string.Empty; _results = new(); _bySite = new(); _running = true;
_cts = new CancellationTokenSource();
if (_sites.Count == 0) { _error = "Please select at least one site."; _running = false; return; }
var progress = new Progress<OperationProgress>(p => { _status = p.Message; _current = p.Current; _total = p.Total; InvokeAsync(StateHasChanged); });
try
{
var opts = new DuplicateScanOptions(_mode, _matchSize, _matchCreated, _matchModified, _matchFolderCount, _matchFileCount, Library: _library.TrimOrNull());
var bySite = new List<(string, IReadOnlyList<DuplicateGroup>)>();
var flat = new List<DuplicateGroup>();
int i = 0;
foreach (var site in _sites)
{
_cts.Token.ThrowIfCancellationRequested();
_status = $"Scanning {site.Title} ({++i}/{_sites.Count})…";
await InvokeAsync(StateHasChanged);
var groups = await Elevation.RunAsync(async c =>
{
var ctx = await SessionMgr.GetOrCreateContextAsync(site.Url, Session.CurrentProfile!, c);
return await DupSvc.ScanDuplicatesAsync(ctx, opts, progress, c);
}, _cts.Token);
bySite.Add((site.Title, groups));
flat.AddRange(groups);
}
_bySite = bySite; _results = flat;
_status = $"Found {_results.Count} duplicate groups across {_sites.Count} site(s).";
await Audit.LogAsync("DuplicateScan", Session.CurrentProfile?.Name ?? "", _sites.Select(s => s.Url),
$"{_results.Count} groups; mode={_mode} lib=[{_library}]");
}
catch (OperationCanceledException) { _status = "Cancelled."; }
catch (Exception ex) { _error = ex.Message; }
finally { _running = false; await InvokeAsync(StateHasChanged); }
}
private void Cancel() => _cts?.Cancel();
private async Task ExportCsv()
{
var ts = DateTime.Now.ToString("yyyyMMdd_HHmmss");
var output = ReportMergeHelper.Build(_bySite, _mergeMode, "duplicates", ts, ReportFormat.Csv, rs => CsvExport.BuildCsv(rs));
await WebExport.DownloadBytesAsync(output.Bytes, output.FileName, output.Mime);
}
private async Task ExportHtml()
{
var ts = DateTime.Now.ToString("yyyyMMdd_HHmmss");
var output = ReportMergeHelper.Build(_bySite, _mergeMode, "duplicates", ts, ReportFormat.Html, rs => HtmlExport.BuildHtml(rs, Session.CurrentBranding));
await WebExport.DownloadBytesAsync(output.Bytes, output.FileName, output.Mime);
}
}