2h filepond, >30d trash) as safety net. * Returns JSON with counts of removed items. */ require_once __DIR__ . '/../../../bootstrap.php'; require_once __DIR__ . '/../../../src/AdminAuth.php'; AdminAuth::requireLogin(); if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !isset($_POST['csrf_token'], $_SESSION['csrf_token']) || !hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) { http_response_code(403); header('Content-Type: application/json; charset=utf-8'); echo json_encode(['error' => 'CSRF invalide.']); exit; } require_once __DIR__ . '/../../../src/Database.php'; $storageRoot = STORAGE_ROOT; $filepondDir = $storageRoot . '/tmp/filepond'; $trashDir = $storageRoot . '/tmp/_trash'; $maxAgeFilepond = 7200; // 2 hours (fallback) $maxAgeTrash = 2592000; // 30 days (fallback) $now = time(); $filepondRemoved = 0; $trashRemoved = 0; $errors = []; $details = []; $db = new Database(); $pdo = $db->getPDO(); // ── Determine PHP session save path ────────────────────────────────────── $sessionSavePath = session_save_path(); if (!$sessionSavePath || $sessionSavePath === '') { $sessionSavePath = sys_get_temp_dir(); } // ── Cleanup stale filepond dirs ────────────────────────────────────────── if (is_dir($filepondDir)) { $items = @scandir($filepondDir); if ($items === false) { $errors[] = 'Impossible de lire tmp/filepond/.'; } else { foreach ($items as $item) { if ($item === '.' || $item === '..' || $item === '.gitkeep') { continue; } $dirPath = $filepondDir . '/' . $item; if (!is_dir($dirPath)) { continue; } $shouldDelete = false; $reason = ''; $manifestPath = $dirPath . '/manifest.json'; $mtime = filemtime($dirPath); $ageMinutes = (int)(($now - $mtime) / 60); // Strategy 1: session-based (preferred) if (file_exists($manifestPath)) { $manifest = json_decode(file_get_contents($manifestPath), true); if (is_array($manifest) && !empty($manifest['session_id'])) { $sessionFile = $sessionSavePath . '/sess_' . $manifest['session_id']; if (!file_exists($sessionFile)) { $shouldDelete = true; $reason = "session expirée (" . $manifest['session_id'] . ")"; } } } // Strategy 2: time-based fallback (no manifest or session still exists but very old) if (!$shouldDelete && $ageMinutes > ($maxAgeFilepond / 60)) { $shouldDelete = true; $reason = "plus de " . ($maxAgeFilepond / 3600) . "h"; } if ($shouldDelete) { rmdirRecursive($dirPath); $filepondRemoved++; $details[] = "filepond/$item: $reason"; } } } } // ── Cleanup orphaned _trash files ──────────────────────────────────────── if (is_dir($trashDir)) { // Build a set of existing thesis_files IDs for fast lookup $existingFileIds = []; $stmt = $pdo->query('SELECT id FROM thesis_files'); while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { $existingFileIds[(int)$row['id']] = true; } // Also collect thesis_ids from theses table (a file might refer to a hard-deleted thesis) // — but thesis_files rows are CASCADE-deleted when a thesis is hard-deleted, so if the // thesis_files row is gone, the trash file is truly orphaned. $items = @scandir($trashDir); if ($items === false) { $errors[] = 'Impossible de lire tmp/_trash/.'; } else { foreach ($items as $item) { if ($item === '.' || $item === '..') { continue; } $filePath = $trashDir . '/' . $item; if (!is_file($filePath)) { continue; } $shouldDelete = false; $reason = ''; $mtime = filemtime($filePath); $ageDays = (int)(($now - $mtime) / 86400); // Strategy 1: DB-based — file was trashed by deleteThesisFile(id=N), // file is named "N_basename". If thesis_files row N no longer exists, it's orphaned. if (preg_match('/^(\d+)_/', $item, $m)) { $dbId = (int)$m[1]; if (!isset($existingFileIds[$dbId])) { $shouldDelete = true; $reason = "ligne DB thesis_files#$dbId supprimée"; } } // Strategy 2: time-based fallback if (!$shouldDelete && $ageDays > ($maxAgeTrash / 86400)) { $shouldDelete = true; $reason = "plus de " . ($maxAgeTrash / 86400) . "j"; } if ($shouldDelete) { if (!@unlink($filePath)) { $errors[] = "Impossible de supprimer: $item"; } else { $trashRemoved++; $details[] = "_trash/$item: $reason"; } } } } } $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); header('Content-Type: application/json; charset=utf-8'); echo json_encode([ 'success' => true, 'filepond_removed' => $filepondRemoved, 'trash_removed' => $trashRemoved, 'errors' => $errors, 'details' => $details, ]); // ── Helper ──────────────────────────────────────────────────────────────── function rmdirRecursive(string $dir): void { if (!is_dir($dir)) { return; } $items = @scandir($dir); if ($items === false) { return; } foreach ($items as $item) { if ($item === '.' || $item === '..') { continue; } $path = $dir . '/' . $item; if (is_dir($path)) { rmdirRecursive($path); } else { @unlink($path); } } @rmdir($dir); }