ajout d'une fonction import/export de romans
This commit is contained in:
parent
36f371fda3
commit
0690d02bae
166
admin/api/export-stories.php
Normal file
166
admin/api/export-stories.php
Normal 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);
|
||||
}
|
179
admin/api/import-stories.php
Normal file
179
admin/api/import-stories.php
Normal 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
190
admin/export-import.php
Normal 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>
|
@ -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>
|
||||
|
Loading…
x
Reference in New Issue
Block a user