Files
SharepointToolbox-Web/Services/VersionCleanupService.cs
T
2026-06-02 10:56:03 +02:00

107 lines
5.8 KiB
C#

using Microsoft.Extensions.Logging;
using Microsoft.SharePoint.Client;
using SharepointToolbox.Web.Core.Helpers;
using SharepointToolbox.Web.Core.Models;
using SharepointToolbox.Web.Services.Audit;
namespace SharepointToolbox.Web.Services;
public class VersionCleanupService : IVersionCleanupService
{
private readonly ILogger<VersionCleanupService> _logger;
private readonly IAuditService _audit;
public VersionCleanupService(ILogger<VersionCleanupService> logger, IAuditService audit)
{
_logger = logger;
_audit = audit;
}
public async Task<IReadOnlyList<string>> ListLibraryTitlesAsync(ClientContext ctx, CancellationToken ct)
{
ct.ThrowIfCancellationRequested();
ctx.Load(ctx.Web, w => w.Lists.Include(l => l.Title, l => l.Hidden, l => l.BaseType));
await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(ctx, null, ct);
return ctx.Web.Lists.Where(l => !l.Hidden && l.BaseType == BaseType.DocumentLibrary)
.Select(l => l.Title).OrderBy(t => t, StringComparer.OrdinalIgnoreCase).ToList();
}
public async Task<IReadOnlyList<VersionCleanupResult>> DeleteOldVersionsAsync(
ClientContext ctx, VersionCleanupOptions options,
IProgress<OperationProgress> progress, CancellationToken ct)
{
ct.ThrowIfCancellationRequested();
if (options.KeepLast < 0) throw new ArgumentOutOfRangeException(nameof(options), "KeepLast must be >= 0.");
ctx.Load(ctx.Web, w => w.Url, w => w.ServerRelativeUrl,
w => w.Lists.Include(l => l.Title, l => l.Hidden, l => l.BaseType, l => l.RootFolder.ServerRelativeUrl));
await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(ctx, progress, ct);
var allLibs = ctx.Web.Lists.Where(l => !l.Hidden && l.BaseType == BaseType.DocumentLibrary).ToList();
var titleFilter = options.LibraryTitles?.Count > 0 ? new HashSet<string>(options.LibraryTitles, StringComparer.OrdinalIgnoreCase) : null;
var libs = titleFilter is null ? allLibs : allLibs.Where(l => titleFilter.Contains(l.Title)).ToList();
var results = new List<VersionCleanupResult>();
string siteUrl = ctx.Web.Url;
int libIdx = 0;
foreach (var lib in libs)
{
ct.ThrowIfCancellationRequested();
libIdx++;
progress.Report(new OperationProgress(libIdx, libs.Count, $"Scanning versions: {lib.Title} ({libIdx}/{libs.Count})"));
var files = new List<string>();
await foreach (var item in SharePointPaginationHelper.GetItemsInFolderAsync(ctx, lib, lib.RootFolder.ServerRelativeUrl, recursive: true, viewFields: new[] { "FSObjType", "FileRef" }, ct: ct))
{
if (item["FSObjType"]?.ToString() != "0") continue;
var fileRef = item["FileRef"]?.ToString();
if (!string.IsNullOrEmpty(fileRef)) files.Add(fileRef);
}
int fileIdx = 0;
foreach (var fileRef in files)
{
ct.ThrowIfCancellationRequested();
fileIdx++;
if (fileIdx % 25 == 0 || fileIdx == files.Count)
progress.Report(new OperationProgress(fileIdx, files.Count, $"{lib.Title}: {fileIdx}/{files.Count} files"));
var result = await TrimFileVersionsAsync(ctx, siteUrl, lib.Title, fileRef, options, progress, ct);
if (result is not null) results.Add(result);
}
}
var totalDeleted = results.Sum(r => r.VersionsDeleted);
await _audit.LogAsync("VersionCleanup", siteUrl, new[] { siteUrl },
$"{totalDeleted} versions deleted across {results.Count} files");
return results;
}
private async Task<VersionCleanupResult?> TrimFileVersionsAsync(ClientContext ctx, string siteUrl, string libraryTitle, string fileServerRelativeUrl, VersionCleanupOptions options, IProgress<OperationProgress> progress, CancellationToken ct)
{
int before = 0;
try
{
var file = ctx.Web.GetFileByServerRelativeUrl(fileServerRelativeUrl);
ctx.Load(file, f => f.Name);
ctx.Load(file.Versions, vs => vs.Include(v => v.VersionLabel, v => v.Created, v => v.Size));
await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(ctx, progress, ct);
var versions = file.Versions.ToList();
before = versions.Count;
if (before == 0) return null;
var ordered = versions.OrderBy(v => v.Created).ToList();
var keep = new HashSet<int>();
int keepLast = Math.Min(options.KeepLast, ordered.Count);
for (int i = ordered.Count - keepLast; i < ordered.Count; i++) keep.Add(i);
if (options.KeepFirst && ordered.Count > 0) keep.Add(0);
long bytesFreed = 0; int deleted = 0;
for (int i = 0; i < ordered.Count; i++) { if (keep.Contains(i)) continue; bytesFreed += ordered[i].Size; ordered[i].DeleteObject(); deleted++; }
if (deleted == 0) return null;
await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(ctx, progress, ct);
return new VersionCleanupResult { SiteUrl = siteUrl, Library = libraryTitle, FileServerRelativeUrl = fileServerRelativeUrl, FileName = System.IO.Path.GetFileName(fileServerRelativeUrl), VersionsBefore = before, VersionsDeleted = deleted, VersionsRemaining = before - deleted, BytesFreed = bytesFreed };
}
catch (OperationCanceledException) { throw; }
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to trim versions for {File}", fileServerRelativeUrl);
return new VersionCleanupResult { SiteUrl = siteUrl, Library = libraryTitle, FileServerRelativeUrl = fileServerRelativeUrl, FileName = System.IO.Path.GetFileName(fileServerRelativeUrl), VersionsBefore = before, Error = ex.Message };
}
}
}