135 lines
4.7 KiB
C#
135 lines
4.7 KiB
C#
using CvCleanupJob.Models;
|
|
using JobScheduler.Tasks;
|
|
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Options;
|
|
using Models.Settings;
|
|
|
|
namespace CvCleanupJob.Tasks;
|
|
|
|
/// <summary>
|
|
/// Deletes oldest cached CV files until the total size of remaining files is at or below the configured budget.
|
|
/// </summary>
|
|
public sealed class CvStorageCleanupJobTask : IJobTask
|
|
{
|
|
private readonly FileStorageSettings _fileStorage;
|
|
private readonly ILogger<CvStorageCleanupJobTask> _logger;
|
|
|
|
public CvStorageCleanupJobTask(IOptions<FileStorageSettings> fileStorage, ILogger<CvStorageCleanupJobTask> logger)
|
|
{
|
|
_fileStorage = fileStorage.Value;
|
|
_logger = logger;
|
|
}
|
|
|
|
public string TaskType => "CvStorageCleanup";
|
|
|
|
public Task ExecuteAsync(IConfiguration parametersSection, CancellationToken cancellationToken)
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
|
var parameters = parametersSection.Get<CvStorageCleanupParameters>()
|
|
?? new CvStorageCleanupParameters();
|
|
|
|
if (parameters.MaxTotalSizeMegabytes <= 0)
|
|
{
|
|
_logger.LogWarning(
|
|
"CvStorageCleanup skipped: MaxTotalSizeMegabytes must be positive (got {Value}).",
|
|
parameters.MaxTotalSizeMegabytes);
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
var root = ResolveStorageRoot(_fileStorage.Path);
|
|
if (!Directory.Exists(root))
|
|
{
|
|
_logger.LogDebug("CvStorageCleanup: directory does not exist yet: {Path}", root);
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
var maxBytes = (long)Math.Round(parameters.MaxTotalSizeMegabytes * 1024d * 1024d, MidpointRounding.AwayFromZero);
|
|
|
|
var candidatePaths = Directory
|
|
.EnumerateFiles(root, parameters.SearchPattern, SearchOption.TopDirectoryOnly)
|
|
.Where(p => !parameters.RestrictToCvStyleFileNamesOnly || IsCvStyleFileName(Path.GetFileName(p)))
|
|
.Select(p => new FileInfo(p))
|
|
.Where(f => f.Exists)
|
|
.ToList();
|
|
|
|
if (candidatePaths.Count == 0)
|
|
{
|
|
_logger.LogDebug("CvStorageCleanup: no files matched under {Path}.", root);
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
long totalBytes = candidatePaths.Sum(f => f.Length);
|
|
if (totalBytes <= maxBytes)
|
|
{
|
|
_logger.LogInformation(
|
|
"CvStorageCleanup: within budget ({TotalMb:F2} MiB / {MaxMb:F2} MiB). No files removed.",
|
|
totalBytes / (1024d * 1024d),
|
|
parameters.MaxTotalSizeMegabytes);
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
var orderedOldestFirst = candidatePaths
|
|
.OrderBy(f => f.LastWriteTimeUtc)
|
|
.ThenBy(f => f.FullName, StringComparer.Ordinal)
|
|
.ToList();
|
|
|
|
long remaining = totalBytes;
|
|
var deleted = 0;
|
|
long freedBytes = 0;
|
|
|
|
while (remaining > maxBytes && orderedOldestFirst.Count > 0)
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
|
var oldest = orderedOldestFirst[0];
|
|
orderedOldestFirst.RemoveAt(0);
|
|
|
|
try
|
|
{
|
|
var len = oldest.Length;
|
|
oldest.Delete();
|
|
remaining -= len;
|
|
freedBytes += len;
|
|
deleted++;
|
|
_logger.LogInformation(
|
|
"CvStorageCleanup: deleted oldest file {File} ({SizeKb} KiB, remaining aggregate ~{RemainMb:F2} MiB).",
|
|
oldest.Name,
|
|
len / 1024d,
|
|
remaining / (1024d * 1024d));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "CvStorageCleanup: could not delete {File}", oldest.FullName);
|
|
}
|
|
}
|
|
|
|
_logger.LogInformation(
|
|
"CvStorageCleanup finished: removed {Deleted} file(s), freed ~{FreedMb:F2} MiB; size now ~{RemainMb:F2} MiB (budget {MaxMb:F2} MiB).",
|
|
deleted,
|
|
freedBytes / (1024d * 1024d),
|
|
remaining / (1024d * 1024d),
|
|
parameters.MaxTotalSizeMegabytes);
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
/// <summary>Matches API behavior for cached CV paths (alphanumeric stem).</summary>
|
|
internal static bool IsCvStyleFileName(string fileName)
|
|
{
|
|
var stem = Path.GetFileNameWithoutExtension(fileName);
|
|
return stem.Length > 0 && stem.All(char.IsLetterOrDigit);
|
|
}
|
|
|
|
internal static string ResolveStorageRoot(string configuredPath)
|
|
{
|
|
if (Path.IsPathRooted(configuredPath))
|
|
{
|
|
return configuredPath;
|
|
}
|
|
|
|
return Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), configuredPath));
|
|
}
|
|
}
|