> /home/dobetternorge/logs/dms-purge.log 2>&1 */ declare(strict_types=1); if (PHP_SAPI !== 'cli') { http_response_code(403); exit("CLI only.\n"); } require_once dirname(__DIR__) . '/includes/bootstrap.php'; $dryRun = in_array('--dry-run', $argv ?? [], true); $cutoff = DBN_DMS_TRASH_RETENTION_DAYS; try { dbnToolsBootCaveau(); $db = getDb(); } catch (Throwable $e) { fwrite(STDERR, "[dbn-dms-purge] DB connect failed: " . $e->getMessage() . "\n"); exit(1); } $start = microtime(true); $purgedDocs = 0; $purgedFolders = 0; $skipped = 0; $errors = 0; try { $docs = $db->prepare( "SELECT id, client_id, storage_path FROM client_documents WHERE deleted_at IS NOT NULL AND deleted_at < (NOW() - INTERVAL ? DAY) LIMIT 5000" ); $docs->execute([$cutoff]); foreach ($docs->fetchAll(PDO::FETCH_ASSOC) as $row) { $docId = (int)$row['id']; $clientId = (int)$row['client_id']; $path = $row['storage_path'] ?? null; if ($dryRun) { echo "DRY: would purge document {$docId} (client {$clientId})\n"; $skipped++; continue; } try { // Version-file cleanup $vr = $db->prepare('SELECT storage_path FROM client_document_versions WHERE document_id = ? AND client_id = ?'); $vr->execute([$docId, $clientId]); foreach ($vr->fetchAll() as $v) { if (!empty($v['storage_path']) && is_file($v['storage_path'])) @unlink($v['storage_path']); } // Chunks try { $db->prepare('DELETE FROM client_chunks WHERE client_id = ? AND document_id = ?')->execute([$clientId, $docId]); } catch (Throwable $e) { /* tolerated */ } // Qdrant try { if (class_exists('QdrantClient')) { $qd = new QdrantClient(); $qd->deleteByFilter('bnl_client_chunks', [ 'must' => [ ['key' => 'client_id', 'match' => ['value' => $clientId]], ['key' => 'document_id', 'match' => ['value' => $docId]], ], ]); } } catch (Throwable $e) { /* tolerated */ } // Disk if ($path && is_file($path)) @unlink($path); if ($path) { $verDir = dirname($path) . '/' . $docId . '_versions'; if (is_dir($verDir)) { foreach (glob($verDir . '/*') ?: [] as $f) @unlink($f); @rmdir($verDir); } } // Row $db->prepare('DELETE FROM client_documents WHERE id = ? AND client_id = ?')->execute([$docId, $clientId]); $purgedDocs++; } catch (Throwable $e) { $errors++; fwrite(STDERR, "[dbn-dms-purge] doc {$docId}: " . $e->getMessage() . "\n"); } } if ($dryRun) { $foldCount = $db->prepare("SELECT COUNT(*) FROM client_folders WHERE deleted_at IS NOT NULL AND deleted_at < (NOW() - INTERVAL ? DAY)"); $foldCount->execute([$cutoff]); $skipped += (int)$foldCount->fetchColumn(); echo "DRY: would purge {$skipped} folders\n"; } else { $fold = $db->prepare("DELETE FROM client_folders WHERE deleted_at IS NOT NULL AND deleted_at < (NOW() - INTERVAL ? DAY)"); $fold->execute([$cutoff]); $purgedFolders = $fold->rowCount(); } } catch (Throwable $e) { fwrite(STDERR, "[dbn-dms-purge] fatal: " . $e->getMessage() . "\n"); exit(1); } $elapsed = round(microtime(true) - $start, 2); $mode = $dryRun ? 'DRY-RUN' : 'live'; printf("[dbn-dms-purge] %s done in %ss: docs=%d folders=%d errors=%d skipped=%d\n", $mode, $elapsed, $purgedDocs, $purgedFolders, $errors, $skipped);