using Microsoft.Extensions.Logging; namespace TeleWave.Infrastructure.Media; /// /// Уборка перевалочного каталога uploads/: удаляет файлы, которых не касались дольше порога. /// После регистрации ассета файл уходит в originals/, а токен загрузки нигде не хранится — /// значит, всё залежавшееся в uploads/ это след оборванной заливки или аварийного рестарта, /// и подобрать его некому. /// public static class UploadsCleaner { /// /// Один проход. Порог считается от времени последней записи, а не создания: идущая многочасовая /// заливка правит файл и под него не попадает. /// public static void Sweep(MediaPathResolver paths, TimeSpan retention, ILogger logger) { try { if (!Directory.Exists(paths.UploadsDir)) return; var deadline = DateTime.UtcNow - retention; foreach (var path in Directory.EnumerateFiles(paths.UploadsDir)) DeleteIfStale(path, deadline, logger); } catch (Exception ex) { logger.LogError(ex, "Ошибка уборки uploads/"); } } private static void DeleteIfStale(string path, DateTime deadline, ILogger logger) { try { var file = new FileInfo(path); if (!file.Exists || file.LastWriteTimeUtc > deadline) return; // Метрики снимаем до удаления: после него FileInfo их уже не отдаст. var size = file.Length; var lastWrite = file.LastWriteTimeUtc; file.Delete(); logger.LogInformation( "Удалён брошенный файл загрузки {Name} ({Size} байт, последняя запись {LastWrite:u})", file.Name, size, lastWrite ); } catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) { // Файл занят или права не те — попробуем на следующем тике. logger.LogWarning(ex, "Не удалось удалить файл загрузки {Path}", path); } } }