package runners import ( "context" "log/slog" "time" "github.com/google/uuid" ) // AttachmentRow projects the chat.AttachmentRef fields the cleanup needs. The // production adapter (T16) maps chat.AttachmentRef → AttachmentRow 1:1. type AttachmentRow struct { ID uuid.UUID StoragePath string } // AttachmentCleanupRepo is the minimal repo surface needed. olderThan matches // chat.Repository's existing signature (time.Duration, not absolute cutoff). type AttachmentCleanupRepo interface { ListAttachmentsForSoftDeletedConversations(ctx context.Context, olderThan time.Duration) ([]AttachmentRow, error) ListExpiredOrphans(ctx context.Context, olderThan time.Duration) ([]AttachmentRow, error) DeleteAttachments(ctx context.Context, ids []uuid.UUID) error } // AttachmentStorage is the storage.Storage surface for delete. type AttachmentStorage interface { Delete(ctx context.Context, storagePath string) error } // AttachmentCleanupConfig matches jobs.AttachmentCleanupConfig minus enabled/interval. type AttachmentCleanupConfig struct { Retention time.Duration } // AttachmentCleanup is the C runner: clears both soft-deleted-conversation // attachments and orphan attachments older than Retention. type AttachmentCleanup struct { repo AttachmentCleanupRepo st AttachmentStorage cfg AttachmentCleanupConfig log *slog.Logger } // NewAttachmentCleanup constructs the runner. func NewAttachmentCleanup(repo AttachmentCleanupRepo, st AttachmentStorage, cfg AttachmentCleanupConfig, log *slog.Logger) *AttachmentCleanup { if log == nil { log = slog.Default() } return &AttachmentCleanup{repo: repo, st: st, cfg: cfg, log: log} } // Run runs both subphases. func (r *AttachmentCleanup) Run(ctx context.Context) { r.cleanup(ctx, "soft_deleted", func(ctx context.Context) ([]AttachmentRow, error) { return r.repo.ListAttachmentsForSoftDeletedConversations(ctx, r.cfg.Retention) }) r.cleanup(ctx, "orphan", func(ctx context.Context) ([]AttachmentRow, error) { return r.repo.ListExpiredOrphans(ctx, r.cfg.Retention) }) } func (r *AttachmentCleanup) cleanup(ctx context.Context, label string, list func(ctx context.Context) ([]AttachmentRow, error)) { rows, err := list(ctx) if err != nil { r.log.Error("attachment_cleanup.list", "label", label, "err", err.Error()) return } if len(rows) == 0 { return } deletedIDs := make([]uuid.UUID, 0, len(rows)) for _, row := range rows { if err := r.st.Delete(ctx, row.StoragePath); err != nil { r.log.Warn("attachment_cleanup.storage_delete_failed", "label", label, "id", row.ID, "path", row.StoragePath, "err", err.Error()) continue } deletedIDs = append(deletedIDs, row.ID) } if len(deletedIDs) == 0 { return } if err := r.repo.DeleteAttachments(ctx, deletedIDs); err != nil { r.log.Error("attachment_cleanup.db_delete", "label", label, "err", err.Error()) } }