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(); $existingFileIds = []; $stmt = $pdo->query('SELECT id FROM thesis_files'); while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { $existingFileIds[(int)$row['id']] = true; } $trStaleCount = 0; $trStaleSize = 0; $trStaleFiles = []; $trActiveCount = 0; $trActiveSize = 0; if (is_dir($trashDir)) { $items = @scandir($trashDir); if ($items !== false) { foreach ($items as $item) { if ($item === '.' || $item === '..') { continue; } $filePath = $trashDir . '/' . $item; if (!is_file($filePath)) { continue; } $size = filesize($filePath); $mtime = filemtime($filePath); $ageDays = (int)(($now - $mtime) / 86400); $stale = false; if (preg_match('/^(\d+)_/', $item, $m)) { $dbId = (int)$m[1]; if (!isset($existingFileIds[$dbId])) { $stale = true; } } if (!$stale && $ageDays > ($maxAgeTrash / 86400)) { $stale = true; } if ($stale) { $trStaleCount++; $trStaleSize += $size; $trStaleFiles[] = [ 'name' => $item, 'size' => $size, 'human' => humanBytes($size), 'age_days' => $ageDays, ]; } else { $trActiveCount++; $trActiveSize += $size; } } } } 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), ]; }