aboutsummaryrefslogtreecommitdiff
path: root/src/service/Backup.php
diff options
context:
space:
mode:
authorpolo <ordipolo@gmx.fr>2026-10-23 00:47:32 +0200
committerpolo <ordipolo@gmx.fr>2026-10-23 00:47:32 +0200
commitf27cca08ff786ceff1870147f6d6bad14d71117f (patch)
treed36597a72025313d6faf4ed228755fa267ebe2f6 /src/service/Backup.php
parentc7309f900ef05bd5cb313110ec159b761af394ec (diff)
downloadcms-f27cca08ff786ceff1870147f6d6bad14d71117f.tar.gz
cms-f27cca08ff786ceff1870147f6d6bad14d71117f.tar.bz2
cms-f27cca08ff786ceff1870147f6d6bad14d71117f.zip
dossiers avec majuscule, namespace pour toutes les classes, autoload psr-4, UserDataService::createZip déplacé dans Backup
Diffstat (limited to 'src/service/Backup.php')
-rw-r--r--src/service/Backup.php233
1 files changed, 0 insertions, 233 deletions
diff --git a/src/service/Backup.php b/src/service/Backup.php
deleted file mode 100644
index 2ee69c1..0000000
--- a/src/service/Backup.php
+++ /dev/null
@@ -1,233 +0,0 @@
1<?php
2// src/service/Backup.php
3
4declare(strict_types=1);
5
6use Doctrine\ORM\EntityManager;
7use Symfony\Component\Process\Process; // protection injection dans le shell
8
9class Backup
10{
11 static public string $backup_dir = '../var/backups';
12 static private int $amount_to_keep = 20;
13
14 static public function mySQLdump(EntityManager $entityManager, string $type): string
15 {
16 $date = new DateTime;
17 $file_path = self::$backup_dir . '/' . Config::$database . '_' . $date->format('Y-m-d') . '_' . $type . '.sql';
18
19 // les versions de mysql sont comme ci: 8.0.36
20 // celles de mariadb sont comme ça: 10.11.6-MariaDB
21 $version = $entityManager->getConnection()->fetchOne('SELECT VERSION()');
22 $engine = stripos($version, 'mariadb') !== false ? 'mariadb-dump' : 'mysqldump';
23
24 $tmp = tempnam('../var', 'tmp_db_codes_'); // crée un fichier avec un nom aléatoire et des droits 600 (concurrence)
25 // attention, si le dossier de destination n'est pas disponible, le fichier est placé avec les fichiers temporaires
26 file_put_contents($tmp,
27 "[client]\n
28 user=" . Config::$user . "\n
29 password=" . Config::$password . "\n
30 host=" . Config::$db_host . "\n");
31
32 $command = new Process([
33 $engine,
34 '--defaults-extra-file=' . $tmp, // pour ne pas enregistrer les codes dans l'historique de la console ou dans les processus de l'OS
35 '--single-transaction',
36 '--quick', // évite d'exploser la RAM si beaucoup de données
37 '--result-file=' . $file_path,
38 Config::$database
39 ]);
40
41 try{
42 // unlink et chmod permettent que le serveur et l'utilisateur appelant bin/mysqldump.php réussissent
43 if(file_exists($file_path)){
44 unlink($file_path);
45 }
46 $command->mustRun(); // comme run() mais lance une ProcessFailedException
47
48 //$file_path = self::gzipCompress($file_path); // '.gz' ajouté à la fin
49
50 chmod($file_path, 0666);
51 return $file_path;
52 }
53 finally{
54 // exécuté même quand situé après "return"
55 unlink($tmp);
56 self::cleanBackups();
57 }
58 }
59
60 // compression gzip (gros gain de place sur le serveur), nécessite l'extension zlib
61 static public function gzipCompress(string $file_path): string
62 {
63 try{
64 file_put_contents(
65 $file_path . '.gz',
66 gzencode(file_get_contents($file_path), 5), // plus rapide que 9 et taille identique d'après mes essais
67 );
68 unlink($file_path);
69 $file_path .= '.gz';
70 }
71 finally{
72 return $file_path;
73 }
74 }
75
76 static public function getBackupList(): array
77 {
78 $files = scandir(Backup::$backup_dir); // affiche un warning si échoue (à cacher en prod)
79 if(!$files){
80 throw new RuntimeException("Le serveur a rencontré une erreur:<br>Accès aux backups impossible faute de permissions.");
81 }
82
83 $backup_array = [];
84 foreach($files as $file){
85 if($file[0] === '.'){
86 continue;
87 }
88 $backup_array[] = $file;
89 }
90 return $backup_array;
91 }
92
93 static public function cleanBackups(): void
94 {
95 $files = glob(self::$backup_dir . '/*.sql');
96 //usort($files, fn($a, $b) => filemtime($b) <=> filemtime($a)); // filemtime = date de dernière modification
97 arsort($files);
98
99 // tri par nom de BDD puis par date
100 $sorted_files = [];
101 $list_by_database = []; // pour le nettoyage 2
102 foreach($files as $file){
103 $exploded = explode('_', basename($file));
104 $sorted_files[$exploded[0]][$exploded[1]][] = $file;
105 $list_by_database[$exploded[0]][] = $file;
106 }
107
108 $date = new DateTime;
109 foreach($sorted_files as $db_name => $from_one_database){
110 // on garde une "quantité à garder" par BDD
111 if(count($from_one_database) > self::$amount_to_keep){
112 // nettoyage 1
113 foreach($from_one_database as $date_key => $with_same_date){
114 // pas touche à aujourd'hui
115 if($date_key != $date->format('Y-m-d')){
116 self::cleanBackupsByPriority($with_same_date);
117 }
118 }
119 // nettoyage 2
120 $files_to_delete = array_slice($list_by_database[$db_name], self::$amount_to_keep);
121 foreach($files_to_delete as $file){
122 unlink($file);
123 }
124 }
125 }
126 }
127
128 // conserver un seul backup par jour choisi dans cet ordre de préférence: console => before-restore => download => auto
129 // cet ordre correspond à: volonté de l'utilisateur => état du jour avant changement => volonté de changement => automatique sans contrôle
130 static private function cleanBackupsByPriority(array $files): void
131 {
132 $priorities = [
133 'console' => 1,
134 'before-restore' => 2,
135 'uploaded' => 3,
136 'auto' => 4,
137 ];
138 $best_priority = PHP_INT_MAX;
139
140 // recherche du fichier à conserver
141 $to_keep = null;
142 foreach($files as $file){
143 foreach($priorities as $keyword => $priority){
144 if(str_contains(basename($file), $keyword) && $priority < $best_priority){
145 $best_priority = $priority;
146 $to_keep = $file;
147 break;
148 }
149 }
150 }
151 // suppression des autres
152 foreach($files as $file){
153 if($file !== $to_keep){
154 unlink($file);
155 }
156 }
157 }
158
159 static public function restoreDatabase(EntityManager $entityManager, string $file_name, array $exclusion_list = []): void
160 {
161 // création d'un backup de sécurité non écrasable
162 $date = new DateTime;
163 if(!file_exists(self::$backup_dir . '/' . Config::$database . '_' . $date->format('Y-m-d') . '_before-restore.sql')){
164 Backup::mySQLdump($entityManager, 'before-restore');
165 }
166
167 $version = $entityManager->getConnection()->fetchOne('SELECT VERSION()');
168 $engine = stripos($version, 'mariadb') !== false ? 'mariadb' : 'mysql';
169
170 $tables = self::getDBListOfTables($entityManager, $exclusion_list); // choisir les tables à effacer et restaurer, exclure "user"
171
172 $tmp_db_codes = tempnam('../var', 'tmp_db_codes_'); // crée un fichier avec un nom aléatoire et des droits 600 (concurrence)
173 file_put_contents($tmp_db_codes,
174 "[client]\n
175 user=" . Config::$user . "\n
176 password=" . Config::$password . "\n
177 host=" . Config::$db_host . "\n");
178
179 //$file_name = self::gzipExtract($file_name); // '.gz' ajouté à la fin
180
181 $command = new Process([
182 $engine, // mariadb ou mysql
183 '--defaults-extra-file=' . $tmp_db_codes, // pour ne pas enregistrer les codes dans l'historique de la console ou dans les processus de l'OS
184 Config::$database
185 ]);
186 $command->setInput(file_get_contents(Backup::$backup_dir . '/' . $file_name)); // l'entrée <
187
188 // utiliser une liste de tables à exclure dynamique
189 try{
190 // tout effacer
191 $tables_with_backquotes = array_map(fn($t) => '`' . $t . '`', $tables);
192 if(!empty($tables_with_backquotes)){
193 $sql = "SET FOREIGN_KEY_CHECKS=0; DROP TABLE " . implode(', ', $tables_with_backquotes) . "; SET FOREIGN_KEY_CHECKS=1;";
194 $entityManager->getConnection()->executeStatement($sql);
195 }
196
197 // copie des tables à ne pas restaurer
198 foreach($exclusion_list as $excluded){
199 $entityManager->getConnection()->executeStatement('RENAME TABLE `' . TABLE_PREFIX . $excluded . '` TO `' . TABLE_PREFIX . $excluded . '_dont_touch`;');
200 }
201
202 // restaurer
203 $command->mustRun(); // comme run() mais lance une ProcessFailedException
204
205 // remettre table user comme avant
206 foreach($exclusion_list as $excluded){
207 $entityManager->getConnection()->executeStatement('DROP TABLE `' . TABLE_PREFIX . $excluded . '`;');
208 $entityManager->getConnection()->executeStatement('RENAME TABLE `' . TABLE_PREFIX . $excluded . '_dont_touch` TO `' . TABLE_PREFIX . $excluded . '`;');
209 }
210 }
211 finally{
212 // exécuté même quand situé après "return"
213 unlink($tmp_db_codes);
214 }
215 }
216
217 static private function getDBListOfTables(EntityManager $entityManager, array $exclusion_list = []): array
218 {
219 $tables = $entityManager->getConnection()->createSchemaManager()->listTableNames();
220 foreach($tables as $key => $elem){
221 foreach($exclusion_list as $excluded){
222 if(!is_string($excluded)){ // cas pas du tout censé arriver!
223 throw new LogicException("Un nom de table doit etre une chaîne de caractères.");
224 }
225 if($elem === TABLE_PREFIX . $excluded){
226 unset($tables[$key]);
227 break; // petite optimisation
228 }
229 }
230 }
231 return $tables;
232 }
233} \ No newline at end of file