Skip to content

Commit f2010bd

Browse files
author
Daniel Fernández Giménez
committed
eliminada la librería de terceros para las copias de la BD por una propia
1 parent 15292d0 commit f2010bd

4 files changed

Lines changed: 365 additions & 102 deletions

File tree

Controller/Backup.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -646,7 +646,9 @@ private function restoreBackupAction(): void
646646
// importamos el backup con nuestro propio importador (no la librería vendor,
647647
// que parte el SQL por ";\n" y corrompe los valores con HTML/CSS/multilínea)
648648
try {
649-
$db = new PDO('mysql:host=' . Tools::config('db_host') . ';port=' . Tools::config('db_port') . ';dbname=' . Tools::config('db_name'), Tools::config('db_user'), Tools::config('db_pass'));
649+
$driver = Tools::config('db_type') === 'postgresql' ? 'pgsql' : 'mysql';
650+
$dsn = $driver . ':host=' . Tools::config('db_host') . ';port=' . Tools::config('db_port') . ';dbname=' . Tools::config('db_name');
651+
$db = new PDO($dsn, Tools::config('db_user'), Tools::config('db_pass'));
650652
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
651653

652654
$restore = BackupSQL::restore($db, $sqlFile);

Lib/BackupSQL.php

Lines changed: 281 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
namespace FacturaScripts\Plugins\Backup\Lib;
2121

22-
use DatabaseBackupManager\MySQLBackup;
22+
use FacturaScripts\Core\Base\DataBase;
2323
use FacturaScripts\Core\Tools;
2424
use PDO;
2525

@@ -30,50 +30,48 @@ class BackupSQL
3030
{
3131
public static function generate(string $channel = ''): bool
3232
{
33-
if (Tools::config('db_type') != 'mysql') {
34-
Tools::log($channel)->error('mysql-support-only');
33+
$folder = Tools::folder('MyFiles', 'Backups');
34+
if (false === Tools::folderCheckOrCreate($folder)) {
35+
Tools::log($channel)->error('folder-create-error');
3536
return false;
3637
}
3738

38-
// si el puerto no es el puerto por defecto, mostramos un aviso
39-
if (Tools::config('db_port') != 3306) {
40-
Tools::log($channel)->warning('backup-port-warning', [
41-
'%port%' => Tools::config('db_port')
42-
]);
43-
}
44-
45-
if (false === extension_loaded('pdo_mysql')) {
46-
Tools::log($channel)->error('pdo-mysql-support-only');
39+
$db = new DataBase();
40+
$type = $db->type();
41+
if (false === in_array($type, ['mysql', 'postgresql'], true)) {
42+
Tools::log($channel)->error('mysql-support-only');
4743
return false;
4844
}
4945

50-
if (false === extension_loaded('zip')) {
51-
Tools::log($channel)->error('php-extension-not-found', ['%extension%' => 'zip']);
46+
$file_path = Tools::folder('MyFiles', 'Backups', date('Y-m-d_H-i-s') . '.sql');
47+
$handle = fopen($file_path, 'w');
48+
if (false === $handle) {
49+
Tools::log($channel)->error('record-save-error');
5250
return false;
5351
}
5452

55-
$folder = Tools::folder('MyFiles', 'Backups');
56-
if (false === Tools::folderCheckOrCreate($folder)) {
57-
Tools::log($channel)->error('folder-create-error');
58-
return false;
59-
}
53+
// cabecera del volcado
54+
fwrite($handle, static::dumpHeader($db, $type));
6055

61-
$file_name = date('Y-m-d_H-i-s') . '.sql';
56+
// las claves foráneas se emiten al final (necesario en postgresql para evitar
57+
// problemas de orden entre tablas; en mysql van inline en el SHOW CREATE TABLE)
58+
$deferredForeignKeys = [];
6259

63-
// Definimos la configuración de la base de datos y el directorio de backup
64-
$db = new PDO('mysql:host=' . Tools::config('db_host') . ';port=' . Tools::config('db_port') . ';dbname=' . Tools::config('db_name'), Tools::config('db_user'), Tools::config('db_pass'));
65-
$backupDir = Tools::folder('MyFiles', 'Backups');
60+
foreach ($db->getTables() as $table) {
61+
// estructura de la tabla
62+
fwrite($handle, static::tableStructure($db, $type, $table, $deferredForeignKeys));
6663

67-
$backup = new MySQLBackup($db, $backupDir);
64+
// datos de la tabla (en streaming, paginado para no agotar memoria)
65+
static::tableData($db, $table, $handle);
66+
}
6867

69-
// exportamos la base de datos a un archivo y le cambiamos el nombre para que tenga el formato correcto
70-
$file = $backup->backup();
71-
if (false === rename($file, Tools::folder('MyFiles', 'Backups', $file_name))) {
72-
Tools::log($channel)->error('record-save-error');
73-
return false;
68+
// claves foráneas diferidas
69+
foreach ($deferredForeignKeys as $fkSql) {
70+
fwrite($handle, $fkSql . "\n");
7471
}
7572

76-
$file_path = Tools::folder('MyFiles', 'Backups', $file_name);
73+
fclose($handle);
74+
7775
if (false === file_exists($file_path)) {
7876
Tools::log($channel)->error('record-save-error');
7977
return false;
@@ -103,18 +101,32 @@ public static function restore(PDO $db, string $sqlFile)
103101
return Tools::trans('no-file-received');
104102
}
105103

106-
// intentamos desactivar el modo estricto de InnoDB (red de seguridad ante errores de
107-
// formato de fila). Algunos servidores no lo permiten por falta de privilegios; en ese
108-
// caso lo ignoramos y continuamos.
109-
try {
110-
$db->exec('SET SESSION innodb_strict_mode = OFF');
111-
} catch (\Throwable $e) {
112-
// sin privilegios para cambiar la variable; continuamos igualmente
104+
// arranque de sesión dependiente del motor. Todas las sentencias van protegidas:
105+
// si el servidor no permite cambiar la variable (falta de privilegios) lo ignoramos.
106+
$driver = $db->getAttribute(PDO::ATTR_DRIVER_NAME);
107+
if ($driver === 'mysql') {
108+
// desactivamos el modo estricto de InnoDB (red de seguridad ante errores de
109+
// formato de fila) y la comprobación de claves foráneas durante la importación
110+
try {
111+
$db->exec('SET SESSION innodb_strict_mode = OFF');
112+
} catch (\Throwable $e) {
113+
// sin privilegios para cambiar la variable; continuamos igualmente
114+
}
115+
try {
116+
$db->exec('SET FOREIGN_KEY_CHECKS = 0');
117+
} catch (\Throwable $e) {
118+
// continuamos igualmente
119+
}
120+
} elseif ($driver === 'pgsql') {
121+
// desactivamos los disparadores de claves foráneas (las FK se crean al final
122+
// del volcado, así que normalmente no es necesario, pero es una red de seguridad)
123+
try {
124+
$db->exec("SET session_replication_role = 'replica'");
125+
} catch (\Throwable $e) {
126+
// sin privilegios; continuamos igualmente
127+
}
113128
}
114129

115-
// desactivamos la comprobación de claves foráneas durante la importación
116-
$db->exec('SET FOREIGN_KEY_CHECKS = 0');
117-
118130
$statement = '';
119131
$length = strlen($content);
120132
$inString = false;
@@ -214,6 +226,25 @@ public static function restore(PDO $db, string $sqlFile)
214226
return true;
215227
}
216228

229+
/**
230+
* Devuelve la cabecera del volcado SQL, dependiente del motor.
231+
*/
232+
private static function dumpHeader(DataBase $db, string $type): string
233+
{
234+
$header = '-- FacturaScripts SQL backup' . "\n"
235+
. '-- Database: ' . Tools::config('db_name') . "\n"
236+
. '-- Engine: ' . $type . ' (' . $db->version() . ')' . "\n"
237+
. '-- Generated: ' . Tools::dateTime() . "\n\n";
238+
239+
if ($type === 'mysql') {
240+
// SET NAMES y desactivar comprobación de FK (sentencias sin privilegios especiales)
241+
$header .= 'SET NAMES ' . Tools::config('mysql_charset', 'utf8') . ";\n"
242+
. "SET FOREIGN_KEY_CHECKS = 0;\n\n";
243+
}
244+
245+
return $header;
246+
}
247+
217248
/**
218249
* Ejecuta una sentencia SQL. Devuelve null si fue correcta o ignorable, o el mensaje de error.
219250
*/
@@ -232,4 +263,214 @@ private static function execStatement(PDO $db, string $statement): ?string
232263

233264
return null;
234265
}
266+
267+
/**
268+
* Formatea un valor para SQL según el tipo de su columna, con escapado seguro.
269+
*/
270+
private static function formatValue(DataBase $db, array $column, $value): string
271+
{
272+
if ($value === null) {
273+
return 'NULL';
274+
}
275+
276+
$type = strtolower($column['type'] ?? '');
277+
278+
// tipos numéricos: valor crudo (sin comillas)
279+
if (preg_match('/^(int|integer|bigint|smallint|mediumint|tinyint|decimal|numeric|float|double|real|serial|bigserial)/', $type) && is_numeric($value)) {
280+
return (string)$value;
281+
}
282+
283+
// tipos binarios: literal hexadecimal
284+
if (preg_match('/(blob|binary|bytea)/', $type)) {
285+
$hex = bin2hex($value);
286+
if ($hex === '') {
287+
return "''";
288+
}
289+
return $db->type() === 'postgresql' ? "'\\x" . $hex . "'" : '0x' . $hex;
290+
}
291+
292+
// resto: cadena escapada según el motor
293+
return "'" . $db->escapeString($value) . "'";
294+
}
295+
296+
/**
297+
* Estructura de tabla en mysql/mariadb mediante SHOW CREATE TABLE (exacto).
298+
*/
299+
private static function mysqlTableStructure(DataBase $db, string $table): string
300+
{
301+
$rows = $db->select('SHOW CREATE TABLE ' . $db->escapeColumn($table));
302+
if (empty($rows) || false === isset($rows[0]['Create Table'])) {
303+
// podría ser una vista u otro objeto que no es tabla: lo ignoramos
304+
return '';
305+
}
306+
307+
$create = $rows[0]['Create Table'];
308+
309+
// forzamos ROW_FORMAT=DYNAMIC si no lo trae, para evitar el error 1118
310+
// "Row size too large" al restaurar (habitual con utf8mb4 y filas anchas)
311+
if (stripos($create, 'ENGINE=InnoDB') !== false && stripos($create, 'ROW_FORMAT') === false) {
312+
$create .= ' ROW_FORMAT=DYNAMIC';
313+
}
314+
315+
return '--' . "\n" . '-- Estructura de la tabla `' . $table . '`' . "\n" . '--' . "\n"
316+
. 'DROP TABLE IF EXISTS ' . $db->escapeColumn($table) . ";\n"
317+
. $create . ";\n\n";
318+
}
319+
320+
/**
321+
* Define una columna para el CREATE TABLE de postgresql.
322+
*/
323+
private static function postgresqlColumnDef(array $col): string
324+
{
325+
$type = $col['type'];
326+
327+
// columnas serie: si el default es nextval(...), usamos SERIAL/BIGSERIAL
328+
$isSerial = isset($col['default']) && is_string($col['default']) && stripos($col['default'], 'nextval(') !== false;
329+
if ($isSerial) {
330+
$def = (stripos($type, 'big') !== false) ? 'BIGSERIAL' : 'SERIAL';
331+
} elseif (!empty($col['character_maximum_length']) && stripos($type, 'char') !== false) {
332+
$def = $type . '(' . $col['character_maximum_length'] . ')';
333+
} else {
334+
$def = $type;
335+
}
336+
337+
if (($col['is_nullable'] ?? 'YES') === 'NO') {
338+
$def .= ' NOT NULL';
339+
}
340+
341+
if (false === $isSerial && isset($col['default']) && $col['default'] !== null && $col['default'] !== '') {
342+
$def .= ' DEFAULT ' . $col['default'];
343+
}
344+
345+
return $def;
346+
}
347+
348+
/**
349+
* Estructura de tabla en postgresql reconstruida desde la introspección del core.
350+
* Las claves foráneas se acumulan en $deferredForeignKeys para emitirlas al final.
351+
*/
352+
private static function postgresqlTableStructure(DataBase $db, string $table, array &$deferredForeignKeys): string
353+
{
354+
$columns = $db->getColumns($table);
355+
if (empty($columns)) {
356+
return '';
357+
}
358+
359+
$defs = [];
360+
foreach ($columns as $col) {
361+
$defs[] = ' ' . $db->escapeColumn($col['name']) . ' ' . static::postgresqlColumnDef($col);
362+
}
363+
364+
$constraints = $db->getConstraints($table, true);
365+
366+
// clave primaria
367+
$pkColumns = [];
368+
foreach ($constraints as $con) {
369+
if (strtoupper($con['type'] ?? '') === 'PRIMARY KEY' && !empty($con['column_name'])) {
370+
$pkColumns[$con['column_name']] = $db->escapeColumn($con['column_name']);
371+
}
372+
}
373+
if ($pkColumns) {
374+
$defs[] = ' PRIMARY KEY (' . implode(', ', $pkColumns) . ')';
375+
}
376+
377+
$sql = '--' . "\n" . '-- Estructura de la tabla "' . $table . '"' . "\n" . '--' . "\n"
378+
. 'DROP TABLE IF EXISTS ' . $db->escapeColumn($table) . " CASCADE;\n"
379+
. 'CREATE TABLE ' . $db->escapeColumn($table) . " (\n"
380+
. implode(",\n", $defs)
381+
. "\n);\n";
382+
383+
// índices que no sean la clave primaria
384+
foreach ($db->getAllIndexes($table) as $idx) {
385+
if (empty($idx['name']) || empty($idx['column']) || isset($pkColumns[$idx['column']])) {
386+
continue;
387+
}
388+
$sql .= 'CREATE INDEX ' . $idx['name'] . ' ON ' . $db->escapeColumn($table)
389+
. ' (' . $db->escapeColumn($idx['column']) . ");\n";
390+
}
391+
392+
// recogemos las claves foráneas para emitirlas al final
393+
foreach ($constraints as $con) {
394+
if (strtoupper($con['type'] ?? '') === 'FOREIGN KEY' && !empty($con['foreign_table_name'])) {
395+
$deferredForeignKeys[] = 'ALTER TABLE ' . $db->escapeColumn($table)
396+
. ' ADD CONSTRAINT ' . $con['name']
397+
. ' FOREIGN KEY (' . $db->escapeColumn($con['column_name']) . ')'
398+
. ' REFERENCES ' . $db->escapeColumn($con['foreign_table_name'])
399+
. ' (' . $db->escapeColumn($con['foreign_column_name']) . ');';
400+
}
401+
}
402+
403+
return $sql . "\n";
404+
}
405+
406+
/**
407+
* Vuelca los datos de una tabla al archivo en streaming, paginando para no agotar memoria.
408+
*/
409+
private static function tableData(DataBase $db, string $table, $handle): void
410+
{
411+
$columns = $db->getColumns($table);
412+
if (empty($columns)) {
413+
return;
414+
}
415+
416+
$colNames = array_keys($columns);
417+
$escapedCols = [];
418+
foreach ($colNames as $name) {
419+
$escapedCols[] = $db->escapeColumn($name);
420+
}
421+
$intoPrefix = 'INSERT INTO ' . $db->escapeColumn($table)
422+
. ' (' . implode(', ', $escapedCols) . ') VALUES ';
423+
424+
$pageSize = 1000;
425+
$offset = 0;
426+
$maxBuffer = 1000000;
427+
428+
while (true) {
429+
$rows = $db->selectLimit('SELECT * FROM ' . $db->escapeColumn($table), $pageSize, $offset);
430+
if (empty($rows)) {
431+
break;
432+
}
433+
434+
$buffer = '';
435+
$bufferRows = 0;
436+
foreach ($rows as $row) {
437+
$values = [];
438+
foreach ($colNames as $name) {
439+
$values[] = static::formatValue($db, $columns[$name], $row[$name] ?? null);
440+
}
441+
$tuple = '(' . implode(', ', $values) . ')';
442+
443+
$buffer .= ($bufferRows === 0) ? ($intoPrefix . $tuple) : (',' . $tuple);
444+
$bufferRows++;
445+
446+
// cerramos el INSERT si el buffer crece demasiado (límite max_allowed_packet)
447+
if (strlen($buffer) >= $maxBuffer) {
448+
fwrite($handle, $buffer . ";\n");
449+
$buffer = '';
450+
$bufferRows = 0;
451+
}
452+
}
453+
454+
if ($bufferRows > 0) {
455+
fwrite($handle, $buffer . ";\n");
456+
}
457+
458+
$offset += $pageSize;
459+
}
460+
461+
fwrite($handle, "\n");
462+
}
463+
464+
/**
465+
* Devuelve el DDL de la estructura de una tabla. En mysql/mariadb usa SHOW CREATE TABLE;
466+
* en postgresql lo reconstruye desde la introspección del core.
467+
*/
468+
private static function tableStructure(DataBase $db, string $type, string $table, array &$deferredForeignKeys): string
469+
{
470+
if ($type === 'mysql') {
471+
return static::mysqlTableStructure($db, $table);
472+
}
473+
474+
return static::postgresqlTableStructure($db, $table, $deferredForeignKeys);
475+
}
235476
}

0 commit comments

Comments
 (0)