Files
xamxam/app/public/admin/actions/restore-trash.php
T
Pontoporeia 7013f79fc0 fix: merge corbeille lists + bulk restore button in cleanup
- Single unified 'Corbeille' table replaces the two separate lists
  (stale/orphelin + restorable). Each row has a Statut column showing
  '↺ Restaurable' or 'Orphelin', plus a checkbox for bulk ops.

- Checkboxes on restorable rows carry data-restorable='1' attribute
  so the bulk JS can distinguish them.

- Bulk actions bar now has both 'Supprimer la sélection' and
  '↺ Restaurer la sélection' buttons. The restore button only appears
  when at least one restorable item is checked.

- New cleanupBulkRestore() JS function populates a dedicated hidden
  form and confirms before submitting. Only restorable items are sent.

- restore-trash.php refactored to handle both single (trash_file) and
  bulk (trash_files[]) inputs via a loop, accumulating restored/skipped
  counts and re-rendering the fragment on HTMX requests.
2026-07-10 16:33:06 +02:00

147 lines
4.5 KiB
PHP

<?php
/**
* Restore a trashed file back to its original location (admin).
*
* POST /admin/actions/restore-trash.php
* Body: trash_file=123_filename.pdf
*
* Reads the JSON sidecar file (trash_file.json) to get the original
* thesis_files row metadata, re-inserts the DB row, and moves the file
* back from tmp/_trash to its original storage path.
*/
require_once __DIR__ . '/../../../bootstrap.php';
require_once __DIR__ . '/../../../src/AdminAuth.php';
AdminAuth::requireLogin();
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['ok' => false, 'error' => 'Méthode non autorisée.']);
exit;
}
if (!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(['ok' => false, 'error' => 'CSRF invalide.']);
exit;
}
$trashFile = trim($_POST['trash_file'] ?? '');
$trashFiles = $_POST['trash_files'] ?? [];
if (!is_array($trashFiles)) $trashFiles = [];
// Normalise: accept both single (trash_file) and bulk (trash_files[])
$allTrashFiles = $trashFile !== '' ? [$trashFile] : $trashFiles;
$allTrashFiles = array_values(array_filter(array_map('trim', $allTrashFiles), fn($n) => $n !== ''));
if (empty($allTrashFiles)) {
http_response_code(400);
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['ok' => false, 'error' => 'Aucun fichier spécifié.']);
exit;
}
require_once __DIR__ . '/../../../src/Database.php';
$db = new Database();
$storageRoot = STORAGE_ROOT;
$trashDir = $storageRoot . '/tmp/_trash';
$restored = 0;
$skipped = 0;
$errors = [];
foreach ($allTrashFiles as $trashFile) {
$trashFile = basename($trashFile);
$trashPath = $trashDir . '/' . $trashFile;
$sidecarPath = $trashPath . '.json';
if (!is_file($trashPath) || !str_starts_with(realpath($trashPath), realpath($trashDir))) {
$skipped++;
$errors[] = "$trashFile : introuvable dans la corbeille.";
continue;
}
if (!file_exists($sidecarPath)) {
$skipped++;
$errors[] = "$trashFile : métadonnées absentes (trop ancien ?).";
continue;
}
$sidecar = json_decode(file_get_contents($sidecarPath), true);
if (!is_array($sidecar) || empty($sidecar['file_path']) || empty($sidecar['thesis_id'])) {
$skipped++;
$errors[] = "$trashFile : métadonnées corrompues.";
continue;
}
$originalPath = $sidecar['file_path'];
$thesisId = (int)$sidecar['thesis_id'];
$fileType = $sidecar['file_type'] ?? 'other';
$fileName = $sidecar['file_name'] ?? basename($originalPath);
$mimeType = $sidecar['mime_type'] ?? 'application/octet-stream';
$fileSize = (int)($sidecar['file_size'] ?? 0);
$displayLabel = $sidecar['display_label'] ?? null;
$thesis = $db->getThesis($thesisId);
if (!$thesis) {
$skipped++;
$errors[] = "$trashFile : le TFE #$thesisId n'existe plus.";
continue;
}
$absOriginal = $storageRoot . '/' . $originalPath;
if (file_exists($absOriginal)) {
@unlink($trashPath);
@unlink($sidecarPath);
$skipped++;
$errors[] = "$trashFile : un fichier existe déjà à l'emplacement d'origine.";
continue;
}
$parentDir = dirname($absOriginal);
if (!is_dir($parentDir)) {
mkdir($parentDir, 0755, true);
}
if (!rename($trashPath, $absOriginal)) {
$skipped++;
$errors[] = "$trashFile : échec du déplacement.";
continue;
}
chmod($absOriginal, 0644);
@unlink($sidecarPath);
$db->insertThesisFile(
$thesisId, $fileType, $originalPath, $fileName,
$fileSize, $mimeType, $displayLabel, null
);
$newId = $db->getConnection()->lastInsertId();
error_log("[restore-trash] thesis_id=$thesisId file_id=$newId restored: $trashFile$originalPath");
$restored++;
}
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
if (isset($_SERVER['HTTP_HX_REQUEST'])) {
header('HX-Trigger: refreshStats');
require __DIR__ . '/cleanup-stats-fragment.php';
exit;
}
header('Content-Type: application/json; charset=utf-8');
echo json_encode([
'ok' => $restored > 0,
'restored' => $restored,
'skipped' => $skipped,
'errors' => $errors,
'message' => $restored . ' fichier(s) restauré(s).' . ($skipped > 0 ? ' ' . $skipped . ' ignoré(s).' : ''),
]);
exit;