1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
|
<?php
// src/service/Backup.php
declare(strict_types=1);
use Doctrine\ORM\EntityManager;
use Symfony\Component\Process\Process; // protection injection dans le shell
class Backup
{
static public string $backup_dir = '../var/backups';
static private int $amount_to_keep = 30;
static public function mySQLdump(EntityManager $entityManager, string $type): string
{
$file_path = self::$backup_dir . '/' . Config::$database . '_' . new DateTime()->format('Y-m-d') . '_' . $type . '.sql';
// les versions de mysql sont comme ci: 8.0.36
// celles de mariadb sont comme ça: 10.11.6-MariaDB
$version = $entityManager->getConnection()->fetchOne('SELECT VERSION()');
$engine = stripos($version, 'mariadb') !== false ? 'mariadb-dump' : 'mysqldump';
$tmp = tempnam('../var', 'tmp_db_codes_'); // crée un fichier avec un nom aléatoire et des droits 600 (concurrence)
file_put_contents($tmp,
"[client]\n
user=" . Config::$user . "\n
password=" . Config::$password . "\n
host=" . Config::$db_host . "\n");
$command = new Process([
$engine,
'--defaults-extra-file=' . $tmp, // pour ne pas enregistrer les codes dans l'historique de la console ou dans les processus de l'OS
'--single-transaction',
'--quick', // évite d'exploser la RAM si beaucoup de données
'--result-file=' . $file_path,
Config::$database
]);
try{
// unlink et chmod permettent que le serveur et l'utilisateur appelant bin/mysqldump.php réussissent
if(file_exists($file_path)){
unlink($file_path);
}
$command->mustRun(); // comme run() mais lance une ProcessFailedException
chmod($file_path, 0666);
return $file_path;
}
finally{
// exécuté même quand situé après "return"
unlink($tmp);
self::cleanBackups();
}
// compression gzip (gros gain de place sur le serveur), nécessite l'extension zlib
/*try{
file_put_contents(
$file_path . '.gz',
gzencode(file_get_contents($file_path), 5), // plus rapide que 9 et taille identique d'après mes essais
);
return $file_path . '.gz';
}
finally{
unlink($file_path);
}*/
}
static public function getBackupList(): array
{
$backup_array = [];
foreach(scandir(Backup::$backup_dir) as $file){
if($file[0] === '.'){
continue;
}
$backup_array[] = $file;
}
return $backup_array;
}
static public function getLastBackupName(): string
{
$backup_list = self::getBackupList();
return $backup_list[count($backup_list) - 1];
}
static public function cleanBackups(): void {
$files = glob(self::$backup_dir . '/*.sql');
usort($files, fn($a, $b) => filemtime($b) <=> filemtime($a)); // filemtime = date de dernière modification
$files_to_delete = array_slice($files, self::$amount_to_keep);
foreach($files_to_delete as $file){
unlink($file);
}
}
static public function restoreDatabase(EntityManager $entityManager, string $file_name): void
{
// backup de sécurité
Backup::mySQLdump($entityManager, 'before-restore');
$version = $entityManager->getConnection()->fetchOne('SELECT VERSION()');
$engine = stripos($version, 'mariadb') !== false ? 'mariadb' : 'mysql';
// choisir les tables à restaurer
$tables = $entityManager->getConnection()->createSchemaManager()->listTableNames();
foreach($tables as $key => $elem){
if($elem === TABLE_PREFIX . 'user'){
unset($tables[$key]);
}
}
// sécurité cas pas normal
if(empty($tables)){
throw new Exception("Aucune table à supprimer");
}
$tmp = tempnam('../var', 'tmp_db_codes_'); // crée un fichier avec un nom aléatoire et des droits 600 (concurrence)
file_put_contents($tmp,
"[client]\n
user=" . Config::$user . "\n
password=" . Config::$password . "\n
host=" . Config::$db_host . "\n");
$command = new Process([
$engine, // mariadb ou mysql
'--defaults-extra-file=' . $tmp, // pour ne pas enregistrer les codes dans l'historique de la console ou dans les processus de l'OS
Config::$database
]);
$command->setInput(file_get_contents(Backup::$backup_dir . '/' . $file_name)); // l'entrée <
try{
// tout effacer
$tables_with_backquotes = array_map(fn($t) => '`' . $t . '`', $tables);
$sql = "SET FOREIGN_KEY_CHECKS=0; DROP TABLE " . implode(', ', $tables_with_backquotes) . "; SET FOREIGN_KEY_CHECKS=1;";
$entityManager->getConnection()->executeStatement($sql);
// la table user restante va poser problème
$entityManager->getConnection()->executeStatement('RENAME TABLE `' . TABLE_PREFIX . 'user` TO `' . TABLE_PREFIX . 'user_dont_touch`;');
// restaurer
$command->mustRun(); // comme run() mais lance une ProcessFailedException
// remettre table user comme avant
$entityManager->getConnection()->executeStatement('DROP TABLE `' . TABLE_PREFIX . 'user`;');
$entityManager->getConnection()->executeStatement('RENAME TABLE `' . TABLE_PREFIX . 'user_dont_touch` TO `' . TABLE_PREFIX . 'user`;');
}
finally{
// exécuté même quand situé après "return"
unlink($tmp);
}
}
}
|