mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-06-25 16:19:19 +02:00
add file export system for admins
- ExportController: getAllThesisFiles(), buildExportManifest(), createExportZip() builds a ZIP archive with manifest.json + files/ mirror of storage/theses/ - Database: getAllThesisFilesForExport() queries all thesis_files + identifier - AdminLogger: logFilesExport() audit log entry - admin/actions/export-files.php: thin dispatcher, streams zip with headers - templates/admin/index.php: 'Exporter fichiers' button next to CSV export
This commit is contained in:
@@ -185,6 +185,15 @@ class AdminLogger
|
||||
$this->write('system', 'db_export', 'success');
|
||||
}
|
||||
|
||||
/** Files export (ZIP with all thesis files + manifest) */
|
||||
public function logFilesExport(int $fileCount, int $byteSize): void
|
||||
{
|
||||
$this->write('system', 'files_export', 'success', [
|
||||
'file_count' => $fileCount,
|
||||
'byte_size' => $byteSize,
|
||||
]);
|
||||
}
|
||||
|
||||
/** Parametres: delete all TFEs */
|
||||
public function logDeleteAllTheses(int $count): void
|
||||
{
|
||||
|
||||
@@ -37,6 +37,146 @@ class ExportController
|
||||
return $this->db->getDatabasePath();
|
||||
}
|
||||
|
||||
// ── Files export ────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Fetch all thesis file records with their thesis identifier.
|
||||
*
|
||||
* @return list<array{id:int, thesis_id:int, identifier:?string, file_type:string,
|
||||
* file_path:string, file_name:string, file_size:?int,
|
||||
* mime_type:?string, description:?string, sort_order:int,
|
||||
* display_label:?string, file_hash:?string}>
|
||||
*/
|
||||
public function getAllThesisFiles(): array
|
||||
{
|
||||
return $this->db->getAllThesisFilesForExport();
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a JSON manifest describing every thesis and its files.
|
||||
*
|
||||
* The manifest maps identifier → { title, files: [{type, path, name, size, mime, hash, label}] }
|
||||
* and is used on restore to re-link files to DB records.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function buildExportManifest(): array
|
||||
{
|
||||
$files = $this->getAllThesisFiles();
|
||||
$theses = $this->db->getAllThesesForExport();
|
||||
|
||||
// Index theses by id for O(1) lookup
|
||||
$byId = [];
|
||||
foreach ($theses as $t) {
|
||||
$byId[(int) $t['id']] = $t;
|
||||
}
|
||||
|
||||
$entries = [];
|
||||
foreach ($files as $f) {
|
||||
$tid = (int) $f['thesis_id'];
|
||||
$t = $byId[$tid] ?? null;
|
||||
$key = $t['identifier'] ?? ('id_' . $tid);
|
||||
|
||||
if (!isset($entries[$key])) {
|
||||
$entries[$key] = [
|
||||
'id' => $tid,
|
||||
'identifier' => $t['identifier'] ?? null,
|
||||
'title' => $t['title'] ?? '(inconnu)',
|
||||
'year' => $t['year'] ?? 0,
|
||||
'files' => [],
|
||||
];
|
||||
}
|
||||
|
||||
$entries[$key]['files'][] = [
|
||||
'type' => $f['file_type'],
|
||||
'path' => $f['file_path'],
|
||||
'name' => $f['file_name'],
|
||||
'size' => $f['file_size'],
|
||||
'mime' => $f['mime_type'],
|
||||
'hash' => $f['file_hash'] ?? null,
|
||||
'label' => $f['display_label'] ?? null,
|
||||
'sort_order' => (int) $f['sort_order'],
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'exported_at' => date('c'),
|
||||
'db_file' => basename($this->db->getDatabasePath()),
|
||||
'total_theses' => count($entries),
|
||||
'total_files' => count($files),
|
||||
'theses' => $entries,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a zip archive containing all thesis files under a files/
|
||||
* directory and a manifest.json at the root.
|
||||
*
|
||||
* Returns the path to the temporary zip file. Caller is responsible
|
||||
* for unlink() after streaming.
|
||||
*
|
||||
* @param string|null $baseDir Base directory path for files inside the zip.
|
||||
* Defaults to "files" (so files are at "files/theses/...").
|
||||
* @return string Absolute path to the generated zip file.
|
||||
* @throws Exception if zip creation fails.
|
||||
*/
|
||||
public function createExportZip(?string $baseDir = null): string
|
||||
{
|
||||
$baseDir = $baseDir ?? 'files';
|
||||
$storageRoot = defined('STORAGE_ROOT') ? STORAGE_ROOT : APP_ROOT . '/storage';
|
||||
$files = $this->getAllThesisFiles();
|
||||
$manifest = $this->buildExportManifest();
|
||||
|
||||
$tmpPath = tempnam(sys_get_temp_dir(), 'xamxam-export-');
|
||||
if ($tmpPath === false) {
|
||||
throw new Exception('Impossible de créer un fichier temporaire.');
|
||||
}
|
||||
|
||||
// tempnam creates a regular file; we want a .zip file instead.
|
||||
unlink($tmpPath);
|
||||
$tmpPath .= '.zip';
|
||||
|
||||
$zip = new ZipArchive();
|
||||
if ($zip->open($tmpPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
|
||||
throw new Exception('Impossible de créer l\'archive ZIP.');
|
||||
}
|
||||
|
||||
// Add manifest.json at the root
|
||||
$zip->addFromString(
|
||||
'manifest.json',
|
||||
json_encode($manifest, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)
|
||||
);
|
||||
|
||||
// Add every thesis file under files/
|
||||
$addedCount = 0;
|
||||
$skippedCount = 0;
|
||||
foreach ($files as $f) {
|
||||
$fullPath = $storageRoot . '/' . $f['file_path'];
|
||||
if (!is_file($fullPath) || !is_readable($fullPath)) {
|
||||
$skippedCount++;
|
||||
continue;
|
||||
}
|
||||
$zipPath = $baseDir . '/' . $f['file_path'];
|
||||
$zip->addFile($fullPath, $zipPath);
|
||||
$addedCount++;
|
||||
}
|
||||
|
||||
$zip->addFromString(
|
||||
'manifest.json',
|
||||
json_encode(array_merge($manifest, [
|
||||
'zip_skipped_count' => $skippedCount,
|
||||
'zip_added_count' => $addedCount,
|
||||
]), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)
|
||||
);
|
||||
|
||||
if (!$zip->close()) {
|
||||
@unlink($tmpPath);
|
||||
throw new Exception('Erreur lors de la finalisation de l\'archive ZIP.');
|
||||
}
|
||||
|
||||
return $tmpPath;
|
||||
}
|
||||
|
||||
// ── CSV export ───────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
@@ -76,7 +216,7 @@ class ExportController
|
||||
*/
|
||||
public function exportAllTheses(): array
|
||||
{
|
||||
// 1) Base thesis data
|
||||
// 1) Base thesis data (includes license_name via migration; fallback to license_type from the view)
|
||||
$theses = $this->db->getAllThesesForExport();
|
||||
if ($theses === []) {
|
||||
return [];
|
||||
|
||||
@@ -2188,6 +2188,21 @@ class Database
|
||||
')->fetchAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* All thesis files for the file-export ZIP.
|
||||
* Includes every thesis_files column + the thesis identifier for manifest
|
||||
* construction.
|
||||
*/
|
||||
public function getAllThesisFilesForExport(): array
|
||||
{
|
||||
return $this->pdo->query('
|
||||
SELECT tf.*, t.identifier
|
||||
FROM thesis_files tf
|
||||
JOIN theses t ON t.id = tf.thesis_id
|
||||
ORDER BY t.year DESC, t.title ASC, tf.sort_order ASC
|
||||
')->fetchAll();
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// SINGLETON PATTERN ENFORCEMENT
|
||||
// ========================================================================
|
||||
|
||||
Reference in New Issue
Block a user