Add periodic cleanup of orphaned drafts: cleanup job, just command, deploy cron

This commit is contained in:
Pontoporeia
2026-06-11 12:23:49 +02:00
parent a19e9e1454
commit 00fed5f0e3
5 changed files with 148 additions and 7 deletions

View File

@@ -2307,6 +2307,59 @@ class Database
return $newId;
}
/**
* Find and optionally delete orphaned draft theses older than a threshold.
*
* Draft theses are created with status='draft' before file operations
* (two-phase commit). If the file phase throws after COMMIT, the draft
* remains orphaned indefinitely — no files are attached but the row blocks
* the identifier number.
*
* @param int $olderThanHours Drafts older than this many hours are candidates.
* @param bool $dryRun When true, only list candidates without deleting.
* @return array{deleted: int, candidates: array} Deleted count + list of IDs found.
*/
public function cleanupOrphanedDrafts(int $olderThanHours = 24, bool $dryRun = true): array
{
$cutoff = date('Y-m-d H:i:s', strtotime("-{$olderThanHours} hours"));
// Draft theses with no files attached = orphaned submissions.
$stmt = $this->pdo->prepare(
"SELECT t.id, t.identifier, t.title, t.submitted_at
FROM theses t
WHERE t.status = 'draft'
AND t.deleted_at IS NULL
AND t.submitted_at < ?
AND NOT EXISTS (
SELECT 1 FROM thesis_files tf WHERE tf.thesis_id = t.id
)
ORDER BY t.submitted_at ASC"
);
$stmt->execute([$cutoff]);
$candidates = $stmt->fetchAll();
if ($dryRun) {
return ['deleted' => 0, 'candidates' => $candidates];
}
$deleted = 0;
require_once __DIR__ . '/Audit.php';
$actor = Audit::actor();
foreach ($candidates as $row) {
$id = (int)$row['id'];
$old = $this->fetchRow('theses', $id);
// Hard-delete the orphan row and its junction records.
// Cascade covers thesis_authors, thesis_tags, thesis_files (empty),
// thesis_supervisors, thesis_languages, thesis_formats.
$this->pdo->prepare('DELETE FROM theses WHERE id = ?')->execute([$id]);
Audit::log($this, $actor, 'DELETE', 'theses', $id, $old, null);
$deleted++;
error_log("[cleanup-drafts] Deleted orphaned draft thesis {$id} ({$row['identifier']}) — submitted {$row['submitted_at']}");
}
return ['deleted' => $deleted, 'candidates' => $candidates];
}
/**
* Soft-delete a single thesis (sets deleted_at).
*/