ajout d'une fonction import/export de romans

This commit is contained in:
Esenjin 2025-02-16 12:22:19 +01:00
parent 36f371fda3
commit 0690d02bae
4 changed files with 536 additions and 0 deletions

View File

@ -0,0 +1,166 @@
<?php
require_once '../../includes/config.php';
require_once '../../includes/auth.php';
require_once '../../includes/stories.php';
if (!Auth::check()) {
http_response_code(401);
exit('Non autorisé');
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
exit('Méthode non autorisée');
}
if (empty($_POST['stories'])) {
http_response_code(400);
exit('Aucun roman sélectionné');
}
try {
$selectedIds = $_POST['stories'];
$tempDir = sys_get_temp_dir() . '/story_export_' . uniqid();
mkdir($tempDir);
// Créer un fichier manifeste pour stocker les métadonnées
$manifest = [
'exportDate' => date('Y-m-d H:i:s'),
'stories' => []
];
foreach ($selectedIds as $storyId) {
$story = Stories::get($storyId);
if (!$story) continue;
// Créer un dossier pour ce roman
$storyDir = $tempDir . '/' . $storyId;
mkdir($storyDir);
mkdir($storyDir . '/images');
// Copier l'image de couverture si elle existe
if (!empty($story['cover'])) {
$coverPath = __DIR__ . '/../../' . $story['cover'];
if (file_exists($coverPath)) {
copy($coverPath, $storyDir . '/cover' . pathinfo($coverPath, PATHINFO_EXTENSION));
$story['cover'] = 'cover' . pathinfo($coverPath, PATHINFO_EXTENSION);
}
}
// Extraire et copier les images des chapitres
foreach ($story['chapters'] as &$chapter) {
if (!empty($chapter['content'])) {
$content = $chapter['content'];
if (is_string($content) && isJson($content)) {
$content = json_decode($content, true);
}
// Traiter le contenu pour les images
if (is_array($content) && isset($content['ops'])) {
foreach ($content['ops'] as &$op) {
if (is_array($op['insert']) && isset($op['insert']['image'])) {
$imgUrl = $op['insert']['image'];
$imgPath = __DIR__ . '/../../' . preg_replace('/^(?:\.\.\/)+/', '', $imgUrl);
if (file_exists($imgPath)) {
$newImgName = 'image_' . uniqid() . pathinfo($imgPath, PATHINFO_EXTENSION);
copy($imgPath, $storyDir . '/images/' . $newImgName);
$op['insert']['image'] = 'images/' . $newImgName;
}
}
}
$chapter['content'] = json_encode($content);
}
}
}
// Sauvegarder les données du roman
file_put_contents($storyDir . '/story.json', json_encode($story, JSON_PRETTY_PRINT));
// Ajouter au manifeste
$manifest['stories'][] = [
'id' => $story['id'],
'title' => $story['title']
];
}
// Sauvegarder le manifeste
file_put_contents($tempDir . '/manifest.json', json_encode($manifest, JSON_PRETTY_PRINT));
// Créer l'archive ZIP
$zipFile = sys_get_temp_dir() . '/romans_' . date('Y-m-d_His') . '.zip';
$zip = new ZipArchive();
if ($zip->open($zipFile, ZipArchive::CREATE | ZipArchive::OVERWRITE) === true) {
addDirToZip($zip, $tempDir, '');
$zip->close();
// Nettoyer le dossier temporaire
deleteDir($tempDir);
// Envoyer l'archive
header('Content-Type: application/zip');
header('Content-Disposition: attachment; filename="' . basename($zipFile) . '"');
header('Content-Length: ' . filesize($zipFile));
readfile($zipFile);
unlink($zipFile);
} else {
throw new Exception('Impossible de créer l\'archive ZIP');
}
} catch (Exception $e) {
if (isset($tempDir) && file_exists($tempDir)) {
deleteDir($tempDir);
}
if (isset($zipFile) && file_exists($zipFile)) {
unlink($zipFile);
}
http_response_code(500);
echo 'Erreur lors de l\'export : ' . $e->getMessage();
}
// Fonction pour vérifier si une chaîne est du JSON valide
function isJson($string) {
json_decode($string);
return json_last_error() === JSON_ERROR_NONE;
}
// Fonction récursive pour ajouter un dossier à une archive ZIP
function addDirToZip($zip, $dir, $relativePath) {
$files = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($dir),
RecursiveIteratorIterator::LEAVES_ONLY
);
foreach ($files as $file) {
if (!$file->isDir()) {
$filePath = $file->getRealPath();
$zipPath = $relativePath . substr($filePath, strlen($dir));
// Normaliser les séparateurs de chemin pour Windows
$zipPath = str_replace('\\', '/', $zipPath);
$zip->addFile($filePath, $zipPath);
}
}
}
// Fonction récursive pour supprimer un dossier et son contenu
function deleteDir($dir) {
if (!file_exists($dir)) return;
$files = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($files as $file) {
if ($file->isDir()) {
rmdir($file->getRealPath());
} else {
unlink($file->getRealPath());
}
}
rmdir($dir);
}

View File

@ -0,0 +1,179 @@
<?php
require_once '../../includes/config.php';
require_once '../../includes/auth.php';
require_once '../../includes/stories.php';
if (!Auth::check()) {
header('Location: ../login.php');
exit;
}
$success = '';
$error = '';
try {
if (!isset($_FILES['importFile']) || $_FILES['importFile']['error'] !== UPLOAD_ERR_OK) {
throw new Exception('Erreur lors de l\'upload du fichier');
}
$overwrite = isset($_POST['overwrite']) && $_POST['overwrite'] === '1';
$tempDir = sys_get_temp_dir() . '/story_import_' . uniqid();
mkdir($tempDir);
// Extraire l'archive
$zip = new ZipArchive();
if ($zip->open($_FILES['importFile']['tmp_name']) !== true) {
throw new Exception('Impossible d\'ouvrir l\'archive ZIP');
}
$zip->extractTo($tempDir);
$zip->close();
// Vérifier la présence du manifeste
$manifestFile = $tempDir . '/manifest.json';
if (!file_exists($manifestFile)) {
throw new Exception('Archive invalide : manifeste manquant');
}
$manifest = json_decode(file_get_contents($manifestFile), true);
if (!$manifest || !isset($manifest['stories'])) {
throw new Exception('Manifeste invalide');
}
$importedCount = 0;
$skippedCount = 0;
$errors = [];
// Parcourir chaque roman dans le manifeste
foreach ($manifest['stories'] as $storyInfo) {
try {
$storyId = $storyInfo['id'];
$storyDir = $tempDir . '/' . $storyId;
if (!file_exists($storyDir . '/story.json')) {
throw new Exception("Fichier story.json manquant pour {$storyInfo['title']}");
}
$story = json_decode(file_get_contents($storyDir . '/story.json'), true);
if (!$story) {
throw new Exception("Données JSON invalides pour {$storyInfo['title']}");
}
// Vérifier si le roman existe déjà
$existingStory = Stories::get($storyId);
if ($existingStory && !$overwrite) {
$skippedCount++;
continue;
}
// Créer les dossiers nécessaires
$coverDir = __DIR__ . '/../../assets/images/covers/';
$chaptersImgDir = __DIR__ . '/../../assets/images/chapters/' . $storyId;
if (!file_exists($coverDir)) mkdir($coverDir, 0755, true);
if (!file_exists($chaptersImgDir)) mkdir($chaptersImgDir, 0755, true);
// Gérer l'image de couverture
if (!empty($story['cover'])) {
$coverFile = $storyDir . '/' . $story['cover'];
if (file_exists($coverFile)) {
$newCoverPath = 'assets/images/covers/' . $storyId . '.' . pathinfo($coverFile, PATHINFO_EXTENSION);
copy($coverFile, __DIR__ . '/../../' . $newCoverPath);
$story['cover'] = $newCoverPath;
}
}
// Gérer les images des chapitres
foreach ($story['chapters'] as &$chapter) {
if (!empty($chapter['content'])) {
$content = $chapter['content'];
if (is_string($content) && isJson($content)) {
$content = json_decode($content, true);
}
if (is_array($content) && isset($content['ops'])) {
foreach ($content['ops'] as &$op) {
if (is_array($op['insert']) && isset($op['insert']['image'])) {
$imgPath = $storyDir . '/' . $op['insert']['image'];
if (file_exists($imgPath)) {
$newImgName = 'image_' . uniqid() . '.' . pathinfo($imgPath, PATHINFO_EXTENSION);
copy($imgPath, $chaptersImgDir . '/' . $newImgName);
$op['insert']['image'] = 'assets/images/chapters/' . $storyId . '/' . $newImgName;
}
}
}
$chapter['content'] = json_encode($content);
}
}
}
// Sauvegarder le roman
Stories::save($story);
$importedCount++;
} catch (Exception $e) {
$errors[] = "Erreur pour {$storyInfo['title']}: " . $e->getMessage();
}
}
// Nettoyer
deleteDir($tempDir);
// Préparer le message de résultat
$messages = [];
if ($importedCount > 0) {
$messages[] = "$importedCount roman(s) importé(s)";
}
if ($skippedCount > 0) {
$messages[] = "$skippedCount roman(s) ignoré(s) (déjà existants)";
}
if (!empty($errors)) {
$messages[] = "Erreurs : " . implode(", ", $errors);
}
$_SESSION['import_result'] = [
'success' => implode(", ", $messages),
'error' => !empty($errors) ? implode("\n", $errors) : ''
];
header('Location: ../export-import.php');
exit;
} catch (Exception $e) {
if (isset($tempDir) && file_exists($tempDir)) {
deleteDir($tempDir);
}
$_SESSION['import_result'] = [
'error' => 'Erreur lors de l\'import : ' . $e->getMessage()
];
header('Location: ../export-import.php');
exit;
}
// Fonction pour vérifier si une chaîne est du JSON valide
function isJson($string) {
json_decode($string);
return json_last_error() === JSON_ERROR_NONE;
}
// Fonction récursive pour supprimer un dossier et son contenu
function deleteDir($dir) {
if (!file_exists($dir)) return;
$files = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($files as $file) {
if ($file->isDir()) {
rmdir($file->getRealPath());
} else {
unlink($file->getRealPath());
}
}
rmdir($dir);
}

190
admin/export-import.php Normal file
View File

@ -0,0 +1,190 @@
<?php
require_once '../includes/config.php';
require_once '../includes/auth.php';
require_once '../includes/stories.php';
// Vérification de l'authentification
if (!Auth::check()) {
header('Location: login.php');
exit;
}
// Récupération des messages de l'import s'ils existent
$success = '';
$error = '';
if (isset($_SESSION['import_result'])) {
if (!empty($_SESSION['import_result']['success'])) {
$success = $_SESSION['import_result']['success'];
}
if (!empty($_SESSION['import_result']['error'])) {
$error = $_SESSION['import_result']['error'];
}
unset($_SESSION['import_result']);
}
$stories = Stories::getAll();
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Import/Export des romans - Administration</title>
<?php if (file_exists(__DIR__ . '/../assets/images/site/favicon.png')): ?>
<link rel="icon" type="image/png" href="../assets/images/site/favicon.png">
<?php endif; ?>
<link rel="stylesheet" href="../assets/css/main.css">
</head>
<body>
<nav class="admin-nav">
<div class="nav-brand">
<?php
$config = Config::load();
if (!empty($config['site']['logo'])): ?>
<img src="<?= htmlspecialchars('../' . $config['site']['logo']) ?>"
alt="<?= htmlspecialchars($config['site']['name']) ?>">
<?php endif; ?>
<span>Administration</span>
</div>
<div class="nav-menu">
<a href="index.php" class="button">Retour</a>
</div>
</nav>
<main class="admin-main">
<h1>Import/Export des romans</h1>
<?php if ($success): ?>
<div class="success-message"><?= htmlspecialchars($success) ?></div>
<?php endif; ?>
<?php if ($error): ?>
<div class="error-message"><?= htmlspecialchars($error) ?></div>
<?php endif; ?>
<!-- Section Export -->
<section class="export-section">
<h2>Exporter des romans</h2>
<form id="exportForm" action="api/export-stories.php" method="POST">
<div class="stories-selection">
<?php foreach ($stories as $story): ?>
<div class="story-checkbox">
<label>
<input type="checkbox" name="stories[]" value="<?= htmlspecialchars($story['id']) ?>">
<?= htmlspecialchars($story['title']) ?>
</label>
</div>
<?php endforeach; ?>
</div>
<div class="form-actions">
<button type="button" class="button select-all">Tout sélectionner</button>
<button type="submit" class="button">Exporter la sélection</button>
</div>
</form>
</section>
<!-- Section Import -->
<section class="import-section">
<h2>Importer des romans</h2>
<form id="importForm" action="api/import-stories.php" method="POST" enctype="multipart/form-data">
<div class="form-group">
<label for="importFile">Sélectionner une archive ZIP</label>
<input type="file" id="importFile" name="importFile" accept=".zip" required>
</div>
<div class="form-group">
<label>
<input type="checkbox" name="overwrite" value="1">
Écraser les romans existants
</label>
</div>
<button type="submit" class="button">Importer</button>
</form>
</section>
</main>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Gestion de la sélection/désélection de tous les romans
const selectAllBtn = document.querySelector('.select-all');
const checkboxes = document.querySelectorAll('input[name="stories[]"]');
selectAllBtn.addEventListener('click', function() {
const allChecked = Array.from(checkboxes).every(cb => cb.checked);
checkboxes.forEach(cb => cb.checked = !allChecked);
this.textContent = allChecked ? 'Tout sélectionner' : 'Tout désélectionner';
});
// Validation du formulaire d'export
document.getElementById('exportForm').addEventListener('submit', function(e) {
const selected = document.querySelectorAll('input[name="stories[]"]:checked');
if (selected.length === 0) {
e.preventDefault();
alert('Veuillez sélectionner au moins un roman à exporter.');
}
});
// Validation de l'import
document.getElementById('importForm').addEventListener('submit', function(e) {
const file = document.getElementById('importFile').files[0];
if (!file) {
e.preventDefault();
alert('Veuillez sélectionner un fichier ZIP à importer.');
return;
}
if (!file.name.toLowerCase().endsWith('.zip')) {
e.preventDefault();
alert('Seuls les fichiers ZIP sont acceptés.');
return;
}
});
});
</script>
<style>
.export-section,
.import-section {
background: var(--bg-tertiary);
padding: var(--spacing-lg);
border-radius: var(--radius-md);
margin-bottom: var(--spacing-xl);
border: 1px solid var(--border-color);
}
.stories-selection {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: var(--spacing-md);
margin: var(--spacing-lg) 0;
}
.story-checkbox {
padding: var(--spacing-sm);
background: var(--bg-secondary);
border-radius: var(--radius-sm);
}
.story-checkbox label {
display: flex;
align-items: center;
gap: var(--spacing-sm);
cursor: pointer;
}
.form-actions {
display: flex;
gap: var(--spacing-md);
margin-top: var(--spacing-lg);
}
.import-section .form-group {
margin-bottom: var(--spacing-md);
}
</style>
</body>
</html>

View File

@ -37,6 +37,7 @@ $stories = Stories::getAll();
<a href="profile.php" class="button">Profil</a>
<a href="story-edit.php" class="button">Nouveau roman</a>
<a href="options.php" class="button">Options</a>
<a href="export-import.php" class="button">Import/Export</a>
<form method="POST" action="logout.php" class="logout-form">
<button type="submit">Déconnexion</button>
</form>