190 lines
6.8 KiB
C#
190 lines
6.8 KiB
C#
using Microsoft.Extensions.Logging;
|
|
using Microsoft.SharePoint.Client;
|
|
using SharepointToolbox.Core.Helpers;
|
|
using SharepointToolbox.Core.Models;
|
|
|
|
namespace SharepointToolbox.Services;
|
|
|
|
public class VersionCleanupService : IVersionCleanupService
|
|
{
|
|
private readonly ILogger<VersionCleanupService> _logger;
|
|
|
|
public VersionCleanupService(ILogger<VersionCleanupService> logger)
|
|
{
|
|
_logger = logger;
|
|
}
|
|
|
|
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>();
|
|
var 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})"));
|
|
|
|
// Enumerate files via paginated CAML so libs > 5,000 items work.
|
|
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);
|
|
}
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
private async Task<VersionCleanupResult?> TrimFileVersionsAsync(
|
|
ClientContext ctx,
|
|
string siteUrl,
|
|
string libraryTitle,
|
|
string fileServerRelativeUrl,
|
|
VersionCleanupOptions options,
|
|
IProgress<OperationProgress> progress,
|
|
CancellationToken ct)
|
|
{
|
|
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);
|
|
|
|
// file.Versions contains only HISTORICAL versions; the current published
|
|
// version lives on `file` itself and is never deletable here.
|
|
var versions = file.Versions.ToList();
|
|
int before = versions.Count;
|
|
if (before == 0) return null;
|
|
|
|
// Sort by Created ascending so [0] is the oldest historical version.
|
|
var ordered = versions
|
|
.OrderBy(v => v.Created)
|
|
.ToList();
|
|
|
|
// Preserve set: the last N most recent + optionally the very first.
|
|
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;
|
|
var v = ordered[i];
|
|
bytesFreed += v.Size;
|
|
v.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 (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),
|
|
Error = ex.Message,
|
|
};
|
|
}
|
|
}
|
|
}
|