mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-06-25 16:19:19 +02:00
Fix dialog margins, add admin-dialog__body/styles, give trash page horizontal margins
This commit is contained in:
192
app/public/admin/actions/cleanup-stats.php
Normal file
192
app/public/admin/actions/cleanup-stats.php
Normal file
@@ -0,0 +1,192 @@
|
||||
<?php
|
||||
/**
|
||||
* Temp file statistics endpoint (admin).
|
||||
*
|
||||
* GET /admin/actions/cleanup-stats.php
|
||||
*
|
||||
* Returns JSON with counts and sizes of tmp/filepond/ and tmp/_trash/,
|
||||
* broken down by stale (will be cleaned) vs active (still session-bound).
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../../bootstrap.php';
|
||||
require_once __DIR__ . '/../../../src/AdminAuth.php';
|
||||
|
||||
AdminAuth::requireLogin();
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
|
||||
http_response_code(405);
|
||||
exit;
|
||||
}
|
||||
|
||||
$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;
|
||||
$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;
|
||||
} 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;
|
||||
$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;
|
||||
} else {
|
||||
$trActiveCount++;
|
||||
$trActiveSize += $size;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode([
|
||||
'filepond_stale_count' => $fpStaleCount,
|
||||
'filepond_stale_size' => $fpStaleSize,
|
||||
'filepond_stale_human' => humanBytes($fpStaleSize),
|
||||
'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_active_count' => $trActiveCount,
|
||||
'trash_active_size' => $trActiveSize,
|
||||
'trash_active_human' => humanBytes($trActiveSize),
|
||||
]);
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
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';
|
||||
}
|
||||
194
app/public/admin/actions/cleanup-tmp.php
Normal file
194
app/public/admin/actions/cleanup-tmp.php
Normal file
@@ -0,0 +1,194 @@
|
||||
<?php
|
||||
/**
|
||||
* On-demand temp file cleanup (admin).
|
||||
*
|
||||
* POST /admin/actions/cleanup-tmp.php
|
||||
*
|
||||
* Smart cleanup: detects truly orphaned files by checking:
|
||||
* - filepond dirs: whose PHP session no longer exists (abandoned upload)
|
||||
* - _trash files: whose DB thesis_files row is gone (thesis already hard-deleted)
|
||||
*
|
||||
* Falls back to time-based (>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);
|
||||
}
|
||||
@@ -473,6 +473,34 @@ try {
|
||||
$trashCount = $db->countTrashedTheses();
|
||||
$tab = $_GET['tab'] ?? 'list';
|
||||
$trashedTheses = ($tab === 'trash') ? $db->getTrashedTheses() : [];
|
||||
|
||||
// ── Tmp file stats ───────────────────────────────────────────────────
|
||||
$filepondDir = STORAGE_ROOT . '/tmp/filepond';
|
||||
$trashDir = STORAGE_ROOT . '/tmp/_trash';
|
||||
$tmpFilepondCount = 0;
|
||||
$tmpTrashCount = 0;
|
||||
$tmpTotalCount = 0;
|
||||
if (is_dir($filepondDir)) {
|
||||
$items = @scandir($filepondDir);
|
||||
if ($items !== false) {
|
||||
foreach ($items as $item) {
|
||||
if ($item !== '.' && $item !== '..' && $item !== '.gitkeep') {
|
||||
$tmpFilepondCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (is_dir($trashDir)) {
|
||||
$items = @scandir($trashDir);
|
||||
if ($items !== false) {
|
||||
foreach ($items as $item) {
|
||||
if ($item !== '.' && $item !== '..') {
|
||||
$tmpTrashCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$tmpTotalCount = $tmpFilepondCount + $tmpTrashCount;
|
||||
} catch (Exception $e) {
|
||||
error_log("Error loading theses list: " . $e->getMessage());
|
||||
die("Erreur lors du chargement de la liste.");
|
||||
|
||||
Reference in New Issue
Block a user