1073741824) { return number_format($bytes / 1073741824, 1) . ' GB'; } if ($bytes > 1048576) { return number_format($bytes / 1048576, 1) . ' MB'; } return number_format($bytes / 1024, 1) . ' KB'; } function getCleanupStats(): array { $storageRoot = STORAGE_ROOT; $filepondDir = $storageRoot . '/tmp/filepond'; $trashDir = $storageRoot . '/tmp/_trash'; $maxAgeFilepond = 7200; // 2h $maxAgeTrash = 2592000; // 30d $sessionSavePath = session_save_path(); if (!$sessionSavePath || $sessionSavePath === '') { $sessionSavePath = sys_get_temp_dir(); } $now = time(); // ── FilePond stats ─────────────────────────────────────────────────── $fpStaleCount = 0; $fpStaleSize = 0; $fpStaleFiles = []; $fpActiveCount = 0; $fpActiveSize = 0; if (is_dir($filepondDir)) { $items = @scandir($filepondDir); if ($items !== false) { foreach ($items as $item) { if ($item === '.' || $item === '..' || $item === '.gitkeep') { continue; } $dirPath = $filepondDir . '/' . $item; if (!is_dir($dirPath)) { continue; } $size = dirSizeRecursive($dirPath); $mtime = filemtime($dirPath); $ageMinutes = (int)(($now - $mtime) / 60); $stale = false; // Session-based detection $manifestPath = $dirPath . '/manifest.json'; 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)) { $stale = true; } } } // Time-based fallback if (!$stale && $ageMinutes > ($maxAgeFilepond / 60)) { $stale = true; } if ($stale) { $fpStaleCount++; $fpStaleSize += $size; $fpStaleFiles[] = [ 'name' => $item, 'size' => $size, 'human' => humanBytes($size), 'age_minutes' => $ageMinutes, ]; } else { $fpActiveCount++; $fpActiveSize += $size; } } } } // ── Trash stats ────────────────────────────────────────────────────── require_once __DIR__ . '/../../../src/Database.php'; $db = new Database(); $pdo = $db->getPDO(); $trStaleCount = 0; $trStaleSize = 0; $trStaleFiles = []; $trActiveCount = 0; $trActiveSize = 0; $trActiveFiles = []; if (is_dir($trashDir)) { $items = @scandir($trashDir); if ($items !== false) { foreach ($items as $item) { if ($item === '.' || $item === '..') { continue; } // Skip JSON sidecar files — they're listed alongside their parent. if (str_ends_with($item, '.json')) { continue; } $filePath = $trashDir . '/' . $item; if (!is_file($filePath)) { continue; } $size = filesize($filePath); $mtime = filemtime($filePath); $ageDays = (int)(($now - $mtime) / 86400); // Check for sidecar metadata — files with sidecars are restorable // regardless of whether the thesis_files DB row still exists. $sidecarPath = $filePath . '.json'; $hasSidecar = file_exists($sidecarPath); $sidecarData = null; if ($hasSidecar) { $sidecarData = json_decode(file_get_contents($sidecarPath), true); if (!is_array($sidecarData)) { $hasSidecar = false; $sidecarData = null; } } // Restorable = has a valid sidecar AND is younger than max age $restorable = $hasSidecar && $ageDays <= ($maxAgeTrash / 86400); if ($restorable) { $trActiveCount++; $trActiveSize += $size; $trActiveFiles[] = [ 'name' => $item, 'size' => $size, 'human' => humanBytes($size), 'age_days' => $ageDays, 'has_sidecar' => true, 'thesis_id' => $sidecarData['thesis_id'] ?? null, 'file_type' => $sidecarData['file_type'] ?? '?', 'original_name' => $sidecarData['file_name'] ?? '', 'original_path' => $sidecarData['file_path'] ?? '', ]; } else { $trStaleCount++; $trStaleSize += $size; $trStaleFiles[] = [ 'name' => $item, 'size' => $size, 'human' => humanBytes($size), 'age_days' => $ageDays, ]; } } } } return [ 'filepond_stale_count' => $fpStaleCount, 'filepond_stale_size' => $fpStaleSize, 'filepond_stale_human' => humanBytes($fpStaleSize), 'filepond_stale_files' => $fpStaleFiles, 'filepond_active_count' => $fpActiveCount, 'filepond_active_size' => $fpActiveSize, 'filepond_active_human' => humanBytes($fpActiveSize), 'trash_stale_count' => $trStaleCount, 'trash_stale_size' => $trStaleSize, 'trash_stale_human' => humanBytes($trStaleSize), 'trash_stale_files' => $trStaleFiles, 'trash_active_count' => $trActiveCount, 'trash_active_size' => $trActiveSize, 'trash_active_human' => humanBytes($trActiveSize), 'trash_active_files' => $trActiveFiles, ]; }