diff options
| author | polo <ordipolo@gmx.fr> | 2026-10-23 00:47:32 +0200 |
|---|---|---|
| committer | polo <ordipolo@gmx.fr> | 2026-10-23 00:47:32 +0200 |
| commit | f27cca08ff786ceff1870147f6d6bad14d71117f (patch) | |
| tree | d36597a72025313d6faf4ed228755fa267ebe2f6 /src/service/Backup.php | |
| parent | c7309f900ef05bd5cb313110ec159b761af394ec (diff) | |
| download | cms-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.php | 233 |
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 | |||
| 4 | declare(strict_types=1); | ||
| 5 | |||
| 6 | use Doctrine\ORM\EntityManager; | ||
| 7 | use Symfony\Component\Process\Process; // protection injection dans le shell | ||
| 8 | |||
| 9 | class 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 | ||
