Files
myAi/Jobs/cv-cleanup-job/Tasks/CvStorageCleanupJobTask.cs
T
claude 75bc9509c5
Build and Push Docker Images / build (push) Successful in 4m35s
Changes
2026-05-14 14:12:29 +03:00

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));
}
}