db = $db; } public static function create(): self { require_once APP_ROOT . '/src/Database.php'; return new self(Database::getInstance()); } // ── Database export ────────────────────────────────────────────────── /** * Return the absolute path of the live database file. */ public function getDatabasePath(): string { return $this->db->getDatabasePath(); } // ── Files export ──────────────────────────────────────────────────── /** * Fetch all thesis file records with their thesis identifier. * * @return list */ 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 ─────────────────────────────────────────────────────── /** * Column headers matching the import format. */ public const CSV_HEADERS = [ 'Identifiant', 'Titre', 'Sous-titre', 'Auteur·ice(s)', 'Contact', 'Promoteur·ice(s)', 'Format(s)', 'Année', 'AP', 'Orientation', 'Finalité', 'Mots-clés', 'Synopsis', 'Contexte', 'Remarques', 'Langue', 'Autorisation', 'Licence', 'Taille', 'Points sur 20', 'Lien BAIU', ]; /** * Fetch all theses and their related data, then return a list of rows * shaped to match the import CSV column order. * * Uses batch queries (one per related table) to avoid N+1. * * @return list> Each inner list has CSV_HEADERS_COUNT elements. */ public function exportAllTheses(): array { // 1) Base thesis data (includes license_name via migration; fallback to license_type from the view) $theses = $this->db->getAllThesesForExport(); if ($theses === []) { return []; } // 2) Load related data in batches $byThesis = function (array $rows): array { $map = []; foreach ($rows as $r) { $tid = (int) $r['thesis_id']; $map[$tid][] = $r; } return $map; }; $authors = $byThesis($this->db->getAllThesisAuthorsForExport()); $supervisors = $byThesis($this->db->getAllThesisSupervisorsForExport()); $tags = $byThesis($this->db->getAllThesisTagsForExport()); $languages = $byThesis($this->db->getAllThesisLanguagesForExport()); $formats = $byThesis($this->db->getAllThesisFormatsForExport()); // 3) Build CSV rows $csvRows = []; foreach ($theses as $t) { $tid = (int) $t['id']; // Authors + contact (first author with email) $authorList = []; $contact = ''; foreach (($authors[$tid] ?? []) as $a) { $authorList[] = $a['name']; if ($contact === '' && !empty($a['email'])) { $contact = $a['email']; } } // Supervisors $supList = []; foreach (($supervisors[$tid] ?? []) as $s) { $supList[] = $s['name']; } // Tags $tagList = []; foreach (($tags[$tid] ?? []) as $tg) { $tagList[] = $tg['name']; } // Languages $langList = []; foreach (($languages[$tid] ?? []) as $l) { $langList[] = $l['name']; } // Formats $fmtList = []; foreach (($formats[$tid] ?? []) as $f) { $fmtList[] = $f['name']; } $csvRows[] = [ $t['identifier'] ?? '', $t['title'] ?? '', $t['subtitle'] ?? '', implode(', ', $authorList), $contact, implode(', ', $supList), implode(', ', $fmtList), $t['year'] ?? '', $t['ap_program'] ?? '', $t['orientation'] ?? '', $t['finality_type'] ?? '', implode(', ', $tagList), $t['synopsis'] ?? '', $t['context_note'] ?? '', $t['remarks'] ?? '', implode(', ', $langList), $t['access_type'] ?? '', $t['license_name'] ?? '', $t['file_size_info'] ?? '', isset($t['jury_points']) ? (string) $t['jury_points'] : '', $t['baiu_link'] ?? '', ]; } return $csvRows; } }