Files
xamxam/app/public/admin/actions/_cleanup-stats-data.php
T
Pontoporeia 3cecee10c9 fix: prevent file deletion on relink + restore button now visible + OOB-style in-place update
Two critical fixes:

1. Relink flow no longer destroys/recreates FilePond instances:
   The relink (XamxamRelinkFile) and PeerTube relink (XamxamRelinkPeerTube)
   previously refreshed the entire fichiers fragment via HTMX after
   pond.addFile(). This triggered destroyFilePondsIn on ALL pools, which
   could fire server.remove callbacks and move existing files to corbeille.
   Now just closes the modal — the file is already added to the pool in-place,
   and syncOrderInput creates the hidden form input.

2. Cleanup page « Corbeille (restaurable) » now actually shows files:
   _cleanup-stats-data.php previously classified trash files by checking if
   the thesis_files DB row still existed. But both deleteThesisFileToTrash
   and FilepondHandler::handleRemove DELETE the DB row. So ALL trash files
   appeared as `stale` (not restorable). Now uses the JSON sidecar file
   presence as the classification criterion — if the sidecar exists and is
   recent, the file is restorable regardless of DB row state.

Also removed unused DB query from _cleanup-stats-data.php.
2026-07-10 16:29:04 +02:00

212 lines
7.3 KiB
PHP

<?php
/**
* Shared cleanup stats data-fetching logic.
*
* Used by both cleanup-stats.php (JSON endpoint) and
* cleanup-stats-fragment.php (HTML fragment).
*/
function dirSizeRecursive(string $dir): int
{
$size = 0;
if (!is_dir($dir)) {
return 0;
}
$items = @scandir($dir);
if ($items === false) {
return 0;
}
foreach ($items as $item) {
if ($item === '.' || $item === '..') {
continue;
}
$path = $dir . '/' . $item;
if (is_dir($path)) {
$size += dirSizeRecursive($path);
} else {
$size += filesize($path);
}
}
return $size;
}
function humanBytes(int $bytes): string
{
if ($bytes > 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,
];
}