238 lines
8.3 KiB
C#
238 lines
8.3 KiB
C#
using System.Collections.ObjectModel;
|
|
using System.Diagnostics;
|
|
using System.Windows;
|
|
using CommunityToolkit.Mvvm.ComponentModel;
|
|
using CommunityToolkit.Mvvm.Input;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Win32;
|
|
using SharepointToolbox.Core.Models;
|
|
using SharepointToolbox.Localization;
|
|
using SharepointToolbox.Services;
|
|
|
|
namespace SharepointToolbox.ViewModels.Tabs;
|
|
|
|
public partial class VersionCleanupViewModel : FeatureViewModelBase
|
|
{
|
|
private readonly IVersionCleanupService _versionService;
|
|
private readonly ISessionManager _sessionManager;
|
|
private readonly ILogger<FeatureViewModelBase> _logger;
|
|
private TenantProfile? _currentProfile;
|
|
|
|
[ObservableProperty]
|
|
private int _keepLast = 5;
|
|
|
|
[ObservableProperty]
|
|
private bool _keepFirstVersion;
|
|
|
|
[ObservableProperty]
|
|
private bool _confirmDelete = true;
|
|
|
|
[ObservableProperty]
|
|
private string _selectedLibrariesLabel = string.Empty;
|
|
|
|
public ObservableCollection<string> SelectedLibraries { get; } = new();
|
|
|
|
public ObservableCollection<VersionCleanupResult> Results { get; } = new();
|
|
|
|
public long TotalBytesFreed => Results.Sum(r => r.BytesFreed);
|
|
public int TotalVersionsDeleted => Results.Sum(r => r.VersionsDeleted);
|
|
public int TotalFilesAffected => Results.Count(r => r.VersionsDeleted > 0);
|
|
public bool HasResults => Results.Count > 0;
|
|
|
|
public TenantProfile? CurrentProfile => _currentProfile;
|
|
|
|
/// <summary>Set by the view to invoke <see cref="LibraryPickerDialog"/> against the current site.</summary>
|
|
public Func<string, IReadOnlyCollection<string>, Task<IReadOnlyList<string>?>>? PickLibrariesAsync { get; set; }
|
|
|
|
/// <summary>Set by the view to display a confirm dialog before destructive run.</summary>
|
|
public Func<string, bool>? ConfirmAction { get; set; }
|
|
|
|
public IAsyncRelayCommand SelectLibrariesCommand { get; }
|
|
public IRelayCommand ClearLibrariesCommand { get; }
|
|
public IAsyncRelayCommand ExportCsvCommand { get; }
|
|
|
|
public VersionCleanupViewModel(
|
|
IVersionCleanupService versionService,
|
|
ISessionManager sessionManager,
|
|
ILogger<FeatureViewModelBase> logger)
|
|
: base(logger)
|
|
{
|
|
_versionService = versionService;
|
|
_sessionManager = sessionManager;
|
|
_logger = logger;
|
|
|
|
SelectLibrariesCommand = new AsyncRelayCommand(SelectLibrariesAsync, CanPickLibraries);
|
|
ClearLibrariesCommand = new RelayCommand(ClearLibraries);
|
|
ExportCsvCommand = new AsyncRelayCommand(ExportCsvAsync, () => Results.Count > 0);
|
|
|
|
SelectedLibraries.CollectionChanged += (_, _) => UpdateSelectedLibrariesLabel();
|
|
Results.CollectionChanged += (_, _) =>
|
|
{
|
|
OnPropertyChanged(nameof(HasResults));
|
|
OnPropertyChanged(nameof(TotalBytesFreed));
|
|
OnPropertyChanged(nameof(TotalVersionsDeleted));
|
|
OnPropertyChanged(nameof(TotalFilesAffected));
|
|
ExportCsvCommand.NotifyCanExecuteChanged();
|
|
};
|
|
UpdateSelectedLibrariesLabel();
|
|
}
|
|
|
|
protected override void OnTenantSwitched(TenantProfile profile)
|
|
{
|
|
_currentProfile = profile;
|
|
SelectedLibraries.Clear();
|
|
Results.Clear();
|
|
OnPropertyChanged(nameof(CurrentProfile));
|
|
SelectLibrariesCommand.NotifyCanExecuteChanged();
|
|
}
|
|
|
|
protected override void OnGlobalSitesChanged(IReadOnlyList<SiteInfo> sites)
|
|
{
|
|
// Site changes invalidate library list — clear so user re-picks.
|
|
SelectedLibraries.Clear();
|
|
SelectLibrariesCommand.NotifyCanExecuteChanged();
|
|
}
|
|
|
|
private bool CanPickLibraries() => _currentProfile != null && GlobalSites.Count > 0;
|
|
|
|
private async Task SelectLibrariesAsync()
|
|
{
|
|
if (PickLibrariesAsync == null || _currentProfile == null) return;
|
|
var first = GlobalSites.FirstOrDefault();
|
|
if (first == null)
|
|
{
|
|
StatusMessage = TranslationSource.Instance["err.no_sites_selected"];
|
|
return;
|
|
}
|
|
|
|
var picked = await PickLibrariesAsync(first.Url, SelectedLibraries.ToArray());
|
|
if (picked == null) return;
|
|
SelectedLibraries.Clear();
|
|
foreach (var t in picked) SelectedLibraries.Add(t);
|
|
}
|
|
|
|
private void ClearLibraries() => SelectedLibraries.Clear();
|
|
|
|
private void UpdateSelectedLibrariesLabel()
|
|
{
|
|
SelectedLibrariesLabel = SelectedLibraries.Count == 0
|
|
? TranslationSource.Instance["versions.libs.all"]
|
|
: string.Format(TranslationSource.Instance["versions.libs.count"], SelectedLibraries.Count);
|
|
}
|
|
|
|
protected override async Task RunOperationAsync(CancellationToken ct, IProgress<OperationProgress> progress)
|
|
{
|
|
if (_currentProfile == null)
|
|
{
|
|
StatusMessage = TranslationSource.Instance["err.no_tenant_connected"];
|
|
return;
|
|
}
|
|
|
|
var urls = GlobalSites.Select(s => s.Url).Where(u => !string.IsNullOrWhiteSpace(u)).ToList();
|
|
if (urls.Count == 0)
|
|
{
|
|
StatusMessage = TranslationSource.Instance["err.no_sites_selected"];
|
|
return;
|
|
}
|
|
|
|
if (KeepLast < 0)
|
|
{
|
|
StatusMessage = TranslationSource.Instance["versions.err.keepLast"];
|
|
return;
|
|
}
|
|
|
|
if (ConfirmDelete && ConfirmAction != null)
|
|
{
|
|
var msg = string.Format(
|
|
TranslationSource.Instance["versions.confirm"],
|
|
KeepLast,
|
|
KeepFirstVersion ? TranslationSource.Instance["versions.confirm.keepFirst"] : string.Empty);
|
|
if (!ConfirmAction(msg)) return;
|
|
}
|
|
|
|
var options = new VersionCleanupOptions(
|
|
SelectedLibraries.ToList(), KeepLast, KeepFirstVersion);
|
|
|
|
Results.Clear();
|
|
int siteIdx = 0;
|
|
foreach (var url in urls)
|
|
{
|
|
ct.ThrowIfCancellationRequested();
|
|
siteIdx++;
|
|
progress.Report(new OperationProgress(siteIdx, urls.Count, $"Cleaning {url}..."));
|
|
|
|
var siteProfile = new TenantProfile
|
|
{
|
|
TenantUrl = url.TrimEnd('/'),
|
|
ClientId = _currentProfile.ClientId,
|
|
Name = _currentProfile.Name,
|
|
};
|
|
|
|
var ctx = await _sessionManager.GetOrCreateContextAsync(siteProfile, ct);
|
|
var siteResults = await _versionService.DeleteOldVersionsAsync(ctx, options, progress, ct);
|
|
|
|
if (Application.Current?.Dispatcher is { } dispatcher)
|
|
{
|
|
await dispatcher.InvokeAsync(() =>
|
|
{
|
|
foreach (var r in siteResults) Results.Add(r);
|
|
});
|
|
}
|
|
else
|
|
{
|
|
foreach (var r in siteResults) Results.Add(r);
|
|
}
|
|
}
|
|
}
|
|
|
|
private async Task ExportCsvAsync()
|
|
{
|
|
if (Results.Count == 0) return;
|
|
var dialog = new SaveFileDialog
|
|
{
|
|
Title = "Export version cleanup results",
|
|
Filter = "CSV files (*.csv)|*.csv",
|
|
DefaultExt = "csv",
|
|
FileName = "version_cleanup",
|
|
};
|
|
if (dialog.ShowDialog() != true) return;
|
|
try
|
|
{
|
|
using var w = new System.IO.StreamWriter(dialog.FileName);
|
|
await w.WriteLineAsync("Site,Library,File,Versions Before,Versions Deleted,Versions Remaining,Bytes Freed,Error");
|
|
foreach (var r in Results)
|
|
{
|
|
await w.WriteLineAsync(string.Join(",",
|
|
Csv(r.SiteUrl),
|
|
Csv(r.Library),
|
|
Csv(r.FileServerRelativeUrl),
|
|
r.VersionsBefore,
|
|
r.VersionsDeleted,
|
|
r.VersionsRemaining,
|
|
r.BytesFreed,
|
|
Csv(r.Error ?? string.Empty)));
|
|
}
|
|
try { Process.Start(new ProcessStartInfo(dialog.FileName) { UseShellExecute = true }); }
|
|
catch { }
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
StatusMessage = $"Export failed: {ex.Message}";
|
|
_logger.LogError(ex, "Version cleanup CSV export failed.");
|
|
}
|
|
}
|
|
|
|
private static string Csv(string value)
|
|
{
|
|
if (string.IsNullOrEmpty(value)) return string.Empty;
|
|
if (value.Contains(',') || value.Contains('"') || value.Contains('\n'))
|
|
return "\"" + value.Replace("\"", "\"\"") + "\"";
|
|
return value;
|
|
}
|
|
|
|
partial void OnKeepLastChanged(int value)
|
|
{
|
|
if (value < 0) KeepLast = 0;
|
|
}
|
|
}
|