using ClaudeDo.Data; using Microsoft.EntityFrameworkCore; namespace ClaudeDo.Worker.Lifecycle; /// /// Startup-only sweep: deletes attachment directories whose task no longer exists in the DB. /// public sealed class AttachmentOrphanRecovery : IHostedService { private readonly IDbContextFactory _dbFactory; private readonly AttachmentStore _store; private readonly ILogger _logger; public AttachmentOrphanRecovery( IDbContextFactory dbFactory, AttachmentStore store, ILogger logger) { _dbFactory = dbFactory; _store = store; _logger = logger; } public async Task StartAsync(CancellationToken cancellationToken) { var taskIds = _store.EnumerateTaskIds(); if (taskIds.Count == 0) { _logger.LogInformation("Attachment orphan recovery: no attachment directories found"); return; } await using var ctx = await _dbFactory.CreateDbContextAsync(cancellationToken); var existingIds = (await ctx.Tasks .Where(t => taskIds.Contains(t.Id)) .Select(t => t.Id) .ToListAsync(cancellationToken)).ToHashSet(); var orphans = taskIds.Where(id => !existingIds.Contains(id)).ToList(); foreach (var id in orphans) _store.DeleteTaskDir(id); if (orphans.Count > 0) _logger.LogWarning("Attachment orphan recovery: removed {Count} orphaned attachment director(ies)", orphans.Count); else _logger.LogInformation("Attachment orphan recovery: no orphaned attachment directories found"); } public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; }