cleanup modal: list stale files to remove; storage restructure: documents/ → {objet}/

This commit is contained in:
Pontoporeia
2026-05-19 22:00:10 +02:00
parent c6199525f9
commit defc919cd0
19 changed files with 292 additions and 22 deletions

View File

@@ -34,6 +34,7 @@ $now = time();
// ── FilePond stats ───────────────────────────────────────────────────────
$fpStaleCount = 0;
$fpStaleSize = 0;
$fpStaleFiles = [];
$fpActiveCount = 0;
$fpActiveSize = 0;
@@ -75,6 +76,12 @@ if (is_dir($filepondDir)) {
if ($stale) {
$fpStaleCount++;
$fpStaleSize += $size;
$fpStaleFiles[] = [
'name' => $item,
'size' => $size,
'human' => humanBytes($size),
'age_minutes' => $ageMinutes,
];
} else {
$fpActiveCount++;
$fpActiveSize += $size;
@@ -96,6 +103,7 @@ while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
$trStaleCount = 0;
$trStaleSize = 0;
$trStaleFiles = [];
$trActiveCount = 0;
$trActiveSize = 0;
@@ -131,6 +139,12 @@ if (is_dir($trashDir)) {
if ($stale) {
$trStaleCount++;
$trStaleSize += $size;
$trStaleFiles[] = [
'name' => $item,
'size' => $size,
'human' => humanBytes($size),
'age_days' => $ageDays,
];
} else {
$trActiveCount++;
$trActiveSize += $size;
@@ -144,12 +158,14 @@ echo json_encode([
'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),

View File

@@ -51,8 +51,8 @@ if (!$thesisId || $filePath === '') {
relinkError(400, 'Paramètres invalides (thesis_id + file_path requis).');
}
// Security: only allow paths under documents/ or theses/
if (!preg_match('#^(documents|theses)/#', $filePath)) {
// Security: only allow paths under tfe/ these/ frart/ documents/ or theses/
if (!preg_match('#^(tfe|these|frart|documents|theses)/#', $filePath)) {
relinkError(403, 'Chemin de fichier non autorisé.');
}

View File

@@ -1,6 +1,6 @@
<?php
/**
* File browser fragment — returns a clickable directory tree of documents/ + theses/.
* File browser fragment — returns a clickable directory tree of tfe/ these/ frart/ documents/ + theses/.
*
* GET /admin/fragments/file-browser.php?dir=documents/2025
*
@@ -17,7 +17,7 @@ error_log('[file-browser] ENTRY | dir=' . ($_GET['dir'] ?? '(root)') . ' | stora
// Determine which directory to browse
$relDir = trim($_GET['dir'] ?? '', '/');
if ($relDir !== '' && !preg_match('#^(documents|theses)(/|$)#', $relDir)) {
if ($relDir !== '' && !preg_match('#^(tfe|these|frart|documents|theses)(/|$)#', $relDir)) {
$relDir = '';
}
@@ -84,7 +84,7 @@ if ($relDir !== '') {
$parentDir = implode('/', $parentParts);
}
$rootDirs = ['documents', 'theses'];
$rootDirs = ['tfe', 'these', 'frart', 'documents', 'theses'];
// SVG icon for a given extension
function fileIcon(string $ext): string {

View File

@@ -50,8 +50,8 @@ class MediaController
exit;
}
// 3. Visibility gate for thesis files (both legacy theses/ and new documents/ paths)
if (preg_match('#^(theses|documents)/#', $requestedPath)) {
// 3. Visibility gate for thesis files (legacy theses/ and documents/, new tfe/these/frart/ paths)
if (preg_match('#^(theses|documents|tfe|these|frart)/#', $requestedPath)) {
require_once APP_ROOT . '/src/Database.php';
require_once APP_ROOT . '/src/ErrorHandler.php';
try {

View File

@@ -188,10 +188,11 @@ class ThesisCreateController
}
// ── 5. File uploads (outside transaction — filesystem ops) ────────────
$tf = $this->buildThesisFolder($data['annee'], $allAuthorsStr, $data['titre']);
$objet = $data['objet'] ?? 'tfe';
$tf = $this->buildThesisFolder($data['annee'], $allAuthorsStr, $data['titre'], $objet);
$folderName = $this->ensureUniqueFolder($tf['folderPath']);
// Rebuild path with potentially modified folder name
$folderPath = 'documents/' . $data['annee'] . '/' . $folderName . '/';
$folderPath = $objet . '/' . $data['annee'] . '/' . $folderName . '/';
$filePrefix = $folderName;
if (!empty($post['filepond_mode'])) {

View File

@@ -313,8 +313,10 @@ class ThesisEditController
$year = intval($post['année'] ?? date('Y'));
$title = trim($post['titre'] ?? '');
$authors = trim($post['auteurice'] ?? '');
$thesis = $this->db->getThesis($thesisId);
$objet = $thesis['objet'] ?? 'tfe';
$tf = $this->buildThesisFolder($year, $authors, $title);
$tf = $this->buildThesisFolder($year, $authors, $title, $objet);
$folderPath = $tf['folderPath'];
$filePrefix = $tf['filePrefix'];
@@ -322,11 +324,12 @@ class ThesisEditController
$existingFiles = $this->db->getThesisFiles($thesisId);
foreach ($existingFiles as $f) {
$fp = $f['file_path'] ?? '';
if (str_starts_with($fp, 'documents/') || str_starts_with($fp, 'theses/')) {
if (preg_match('#^(theses|documents|tfe|these|frart)/#', $fp)) {
$parts = explode('/', $fp);
if (count($parts) >= 3) {
$parentDir = $parts[0];
$folderName = $parts[2];
$folderPath = 'documents/' . $year . '/' . $folderName . '/';
$folderPath = $parentDir . '/' . $year . '/' . $folderName . '/';
$filePrefix = $folderName;
break;
}

View File

@@ -6,7 +6,7 @@
* Shared file-upload logic used by ThesisCreateController and
* ThesisEditController. All on-disk files are stored under:
*
* documents/{YYYY}/{YYYY}_{AUTHORS}_{TITLE_SLUG}/
* {objet}/{YYYY}/{YYYY}_{AUTHORS}_{TITLE_SLUG}/
*
* Filenames follow:
*
@@ -88,7 +88,7 @@ trait ThesisFileHandler
*
* @param int $thesisId
* @param array|null $upload Single-file $_FILES entry.
* @param string $folderPath Relative path from STORAGE_ROOT to the thesis folder (e.g. "documents/2025/2025_SMITH_Mon_Titre/").
* @param string $folderPath Relative path from STORAGE_ROOT to the thesis folder (e.g. "tfe/2025/2025_SMITH_Mon_Titre/").
* @param string $filePrefix The prefix shared by all files in this folder (e.g. "2025_SMITH_Mon_Titre").
*/
protected function handleCoverUpload(int $thesisId, ?array $upload, string $folderPath, string $filePrefix): void
@@ -802,19 +802,19 @@ trait ThesisFileHandler
/**
* Build the folder path and file prefix for a thesis.
*
* Folder: documents/{YYYY}/{YYYY}_{AUTHORS}_{TITLE_SLUG}/
* Folder: {objet}/{YYYY}/{YYYY}_{AUTHORS}_{TITLE_SLUG}/
* Prefix: {YYYY}_{AUTHORS}_{TITLE_SLUG}
*
* @return array{folderPath: string, filePrefix: string, folderName: string}
*/
protected function buildThesisFolder(int $year, string $authorsStr, string $title): array
protected function buildThesisFolder(int $year, string $authorsStr, string $title, string $objet = 'tfe'): array
{
$authorSlug = $this->generateAuthorSlug($authorsStr);
$titleSlug = $this->generateTitleSlug($title);
$folderName = $year . '_' . $authorSlug . '_' . $titleSlug;
$filePrefix = $folderName;
$folderPath = 'documents/' . $year . '/' . $folderName . '/';
$folderPath = $objet . '/' . $year . '/' . $folderName . '/';
return [
'folderPath' => $folderPath,

View File

@@ -2335,9 +2335,15 @@ class Database
// Clean up thesis files from disk
$files = $this->getThesisFiles($thesisId);
$storageRoot = defined('STORAGE_ROOT') ? STORAGE_ROOT : '/var/www/xamxam/storage';
foreach ($files as $file) {
if (!empty($file['file_path']) && file_exists($file['file_path'])) {
@unlink($file['file_path']);
$fp = $file['file_path'] ?? '';
if ($fp === '' || str_starts_with($fp, 'http://') || str_starts_with($fp, 'https://') || str_starts_with($fp, 'peertube_ids:')) {
continue;
}
$abs = $storageRoot . '/' . $fp;
if (file_exists($abs)) {
@unlink($abs);
}
}

View File

@@ -292,8 +292,24 @@ async function fetchTmpStats() {
}
} else {
html = `<p style="margin:0 0 var(--space-xs) 0;font-weight:600">⚠️ ${totalStale} élément(s) obsolète(s) à nettoyer :</p>`;
if (data.filepond_stale_count) html += `<p style="margin:0 0 var(--space-xs) 0">📁 <strong>Téléversements abandonnés</strong> : ${data.filepond_stale_count} dossier(s) — ${data.filepond_stale_human} <span style="font-size:0.85em;color:var(--text-secondary)">(session expirée ou >2h)</span></p>`;
if (data.trash_stale_count) html += `<p style="margin:0 0 var(--space-xs) 0">🗑️ <strong>Fichiers supprimés orphelins</strong> : ${data.trash_stale_count} fichier(s) — ${data.trash_stale_human} <span style="font-size:0.85em;color:var(--text-secondary)">(référence DB disparue ou >30j)</span></p>`;
if (data.filepond_stale_count) {
html += `<details style="margin:0 0 var(--space-xs) 0;font-size:0.9em" open><summary>📁 <strong>Téléversements abandonnés</strong> : ${data.filepond_stale_count} dossier(s) — ${data.filepond_stale_human} <span style="font-size:0.85em;color:var(--text-secondary)">(session expirée ou >2h)</span></summary>`;
if (data.filepond_stale_files) {
html += '<ul style="margin:var(--space-xs) 0 0 var(--space-md);max-height:10em;overflow-y:auto">';
data.filepond_stale_files.forEach(f => { html += `<li>${f.name} <span style="color:var(--text-secondary)">(${f.human}, ~${Math.round(f.age_minutes)}min)</span></li>`; });
html += '</ul>';
}
html += '</details>';
}
if (data.trash_stale_count) {
html += `<details style="margin:0 0 var(--space-xs) 0;font-size:0.9em" open><summary>🗑️ <strong>Fichiers supprimés orphelins</strong> : ${data.trash_stale_count} fichier(s) — ${data.trash_stale_human} <span style="font-size:0.85em;color:var(--text-secondary)">(référence DB disparue ou >30j)</span></summary>`;
if (data.trash_stale_files) {
html += '<ul style="margin:var(--space-xs) 0 0 var(--space-md);max-height:10em;overflow-y:auto">';
data.trash_stale_files.forEach(f => { html += `<li>${f.name} <span style="color:var(--text-secondary)">(${f.human}, ~${Math.round(f.age_days)}j)</span></li>`; });
html += '</ul>';
}
html += '</details>';
}
if (data.filepond_active_count || data.trash_active_count) {
html += '<p style="margin:var(--space-xs) 0 0 0;font-size:0.85em;color:var(--text-secondary)">Conservés : ';
if (data.filepond_active_count) html += `${data.filepond_active_count} téléversement(s) actif(s), `;

View File

@@ -266,7 +266,6 @@
<div class="admin-action-bar">
<a href="/admin/edit.php?id=<?= $thesisId ?>" class="btn btn--primary">Modifier</a>
<a href="/admin/add.php" class="btn btn--secondary">Ajouter un autre TFE</a>
<a href="/admin/" class="btn btn--secondary">Retour à la liste</a>
</div>