This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user