🐛 fix(ReserverController): Corrige la route de la sitemap. ♻️ refactor(SiteMapListener): Génère les URLs des produits dans la sitemap. 🔧 chore(ansible): Ajoute le dossier seo aux dossiers à sauvegarder.
176 lines
6.4 KiB
PHP
176 lines
6.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Command;
|
|
|
|
use App\Entity\Backup;
|
|
use App\Service\Mailer\Mailer;
|
|
use Doctrine\ORM\EntityManagerInterface;
|
|
use Symfony\Component\Console\Attribute\AsCommand;
|
|
use Symfony\Component\Console\Command\Command;
|
|
use Symfony\Component\Console\Input\InputInterface;
|
|
use Symfony\Component\Console\Output\OutputInterface;
|
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
|
use Symfony\Component\HttpKernel\KernelInterface;
|
|
use ZipArchive;
|
|
|
|
#[AsCommand(name: 'app:backup', description: 'Sauvegarde complète PostgreSQL + Images')]
|
|
class BackupCommand extends Command
|
|
{
|
|
public function __construct(
|
|
private readonly KernelInterface $kernel,
|
|
private readonly EntityManagerInterface $entityManager,
|
|
private readonly Mailer $mailer,
|
|
?string $name = null
|
|
) {
|
|
parent::__construct($name);
|
|
}
|
|
|
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
|
{
|
|
$io = new SymfonyStyle($input, $output);
|
|
$io->title('Démarrage de la sauvegarde PostgreSQL + Images');
|
|
|
|
$backupEntry = new Backup();
|
|
$this->entityManager->persist($backupEntry);
|
|
$this->entityManager->flush();
|
|
|
|
$projectDir = $this->kernel->getProjectDir();
|
|
$backupDir = $projectDir . '/sauvegarde';
|
|
$sqlDumpPath = $projectDir . '/var/temp_db.sql';
|
|
$imagesDir = $projectDir . '/public/images';
|
|
$pdfDir = $projectDir . '/public/pdf';
|
|
$seoDif = $projectDir . '/public/seo';
|
|
|
|
try {
|
|
if (!is_dir($backupDir)) {
|
|
mkdir($backupDir, 0777, true);
|
|
}
|
|
|
|
// 1. DUMP POSTGRESQL
|
|
$dbUrl = parse_url($_ENV['DATABASE_URL'] ?? '');
|
|
if (!$dbUrl) throw new \Exception("DATABASE_URL non configurée.");
|
|
|
|
$io->text('Extraction SQL via pg_dump...');
|
|
$command = sprintf(
|
|
'PGPASSWORD=%s pg_dump -h %s -p %s -U %s -F p %s > %s',
|
|
escapeshellarg($dbUrl['pass'] ?? ''),
|
|
escapeshellarg($dbUrl['host'] ?? '127.0.0.1'),
|
|
escapeshellarg((string)($dbUrl['port'] ?? 5432)),
|
|
escapeshellarg($dbUrl['user'] ?? ''),
|
|
escapeshellarg(ltrim($dbUrl['path'], '/')),
|
|
escapeshellarg($sqlDumpPath)
|
|
);
|
|
|
|
exec($command, $outputExec, $returnCode);
|
|
if ($returnCode !== 0) throw new \Exception("Échec pg_dump (Code: $returnCode)");
|
|
|
|
// 2. CRÉATION DU ZIP
|
|
$fileName = sprintf('db_backup_%s.zip', date('Y-m-d_H-i'));
|
|
$zipPath = $backupDir . '/' . $fileName;
|
|
$zip = new ZipArchive();
|
|
|
|
if ($zip->open($zipPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) === true) {
|
|
// Ajout SQL
|
|
if (file_exists($sqlDumpPath)) {
|
|
$zip->addFile($sqlDumpPath, 'database.sql');
|
|
}
|
|
|
|
// Ajout Images Récursif
|
|
if (is_dir($imagesDir)) {
|
|
$io->text('Compression du dossier images...');
|
|
$this->addFolderToZip($imagesDir, $zip, 'images');
|
|
}
|
|
if (is_dir($pdfDir)) {
|
|
$io->text('Compression du dossier pdf...');
|
|
$this->addFolderToZip($pdfDir, $zip, 'pdf');
|
|
}
|
|
if (is_dir($seoDif)) {
|
|
$io->text('Compression du dossier seo...');
|
|
$this->addFolderToZip($seoDif, $zip, 'seo');
|
|
}
|
|
$zip->close();
|
|
} else {
|
|
throw new \Exception("Impossible d'initialiser le fichier ZIP.");
|
|
}
|
|
|
|
// 3. STATISTIQUES ET SUCCÈS
|
|
$fileSizeMb = round(filesize($zipPath) / (1024 * 1024), 2);
|
|
$backupEntry->setStatus('SUCCESS');
|
|
$backupEntry->setSizeMb($fileSizeMb);
|
|
|
|
$io->success("Sauvegarde réussie : $fileName ($fileSizeMb MB)");
|
|
|
|
} catch (\Exception $e) {
|
|
$backupEntry->setStatus('ERROR');
|
|
$backupEntry->setErrorMessage($e->getMessage());
|
|
$io->error("Échec : " . $e->getMessage());
|
|
}
|
|
|
|
$this->entityManager->flush();
|
|
|
|
// NOTIFICATION
|
|
$statusEmoji = ($backupEntry->getStatus() === 'SUCCESS') ? '✅' : '❌';
|
|
$this->mailer->send(
|
|
'notification@siteconseil.fr',
|
|
'Intranet Ludievent',
|
|
$statusEmoji . ' Sauvegarde - ' . date('d/m/Y'),
|
|
'mails/backup_notification.twig',
|
|
['backup' => $backupEntry]
|
|
);
|
|
|
|
if (file_exists($sqlDumpPath)) unlink($sqlDumpPath);
|
|
$this->cleanOldFiles($backupDir, $io);
|
|
|
|
return Command::SUCCESS;
|
|
}
|
|
|
|
/**
|
|
* Ajoute un dossier récursivement en corrigeant les séparateurs de dossiers
|
|
*/
|
|
private function addFolderToZip(string $folderPath, ZipArchive $zip, string $zipSubFolder): void
|
|
{
|
|
$files = new \RecursiveIteratorIterator(
|
|
new \RecursiveDirectoryIterator($folderPath, \RecursiveDirectoryIterator::SKIP_DOTS),
|
|
\RecursiveIteratorIterator::LEAVES_ONLY
|
|
);
|
|
|
|
foreach ($files as $file) {
|
|
// 1. On ignore les dossiers (déjà géré par LEAVES_ONLY mais par sécurité)
|
|
if ($file->isDir()) {
|
|
continue;
|
|
}
|
|
|
|
$filePath = $file->getRealPath();
|
|
$fileName = $file->getFilename();
|
|
|
|
// 2. EXCLUSION : On ignore les fichiers .gitignore
|
|
if ($fileName === '.gitignore') {
|
|
continue;
|
|
}
|
|
|
|
if ($filePath) {
|
|
// Calcul du chemin relatif interne (on force les '/' pour la compatibilité ZIP)
|
|
$relativeInZip = $zipSubFolder . '/' . str_replace('\\', '/', substr($filePath, strlen($folderPath) + 1));
|
|
|
|
$zip->addFile($filePath, $relativeInZip);
|
|
}
|
|
}
|
|
}
|
|
private function cleanOldFiles(string $backupDir, SymfonyStyle $io): void
|
|
{
|
|
$io->text('Nettoyage historique...');
|
|
$limitDate = new \DateTimeImmutable('-7 days');
|
|
$retentionTs = time() - (7 * 24 * 60 * 60);
|
|
|
|
foreach (glob($backupDir . '/*.zip') as $file) {
|
|
if (filemtime($file) <= $retentionTs) unlink($file);
|
|
}
|
|
|
|
$this->entityManager->createQuery(
|
|
'DELETE FROM App\Entity\Backup b WHERE b.createdAt <= :limitDate'
|
|
)->setParameter('limitDate', $limitDate)->execute();
|
|
}
|
|
}
|