From 980949635f417560e184844142a724d4ce2ccbf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esteban=20S=C3=A1nchez?= Date: Fri, 26 Jun 2026 12:31:20 +0200 Subject: [PATCH 1/3] =?UTF-8?q?A=C3=B1adir=20l=C3=ADmite=20de=20copias=20d?= =?UTF-8?q?e=20seguridad=20y=20mensajes=20de=20registro?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://facturascripts.com/roadmap/4772 - Se implementó la función `applyBackupLimit` en `Cron.php` para gestionar el número máximo de copias de seguridad permitidas. - Se añadió el mensaje de registro "Copia de seguridad eliminada por límite" en español en `es_ES.json`. - Se actualizó `Init.php` para establecer el valor predeterminado de `max_backups` en 0. - Se modificó `SettingsBackup.xml` para incluir un nuevo campo de configuración para el límite de copias de seguridad. - Se creó el archivo de prueba `CronApplyBackupLimitTest.php` para verificar el comportamiento de la nueva funcionalidad. --- Cron.php | 32 ++++++ Init.php | 1 + Test/main/CronApplyBackupLimitTest.php | 136 +++++++++++++++++++++++++ Translation/es_ES.json | 2 + XMLView/SettingsBackup.xml | 3 + 5 files changed, 174 insertions(+) create mode 100644 Test/main/CronApplyBackupLimitTest.php diff --git a/Cron.php b/Cron.php index 35eeaaf..f6b005d 100644 --- a/Cron.php +++ b/Cron.php @@ -87,9 +87,41 @@ public function run(): void $job->run(function () { $this->createBackup(); + $this->applyBackupLimit(); }); } + protected function applyBackupLimit(): void + { + $limit = (int) Tools::settings('backup', 'max_backups', 0); + if ($limit <= 0) { + return; + } + + $folder = Tools::folder('MyFiles', 'Backups'); + $files = is_dir($folder) ? scandir($folder) : []; + + foreach (['sql', 'zip'] as $ext) { + $byExt = []; + foreach ($files as $file) { + if (str_ends_with($file, '.' . $ext)) { + $byExt[] = $file; + } + } + + rsort($byExt); // más recientes primero (nombre = fecha) + $toDelete = array_slice($byExt, $limit); + + foreach ($toDelete as $filename) { + $path = $folder . DIRECTORY_SEPARATOR . $filename; + if (file_exists($path)) { + unlink($path); + Tools::log(self::JOB_NAME)->info('backup-deleted', ['%file%' => $filename]); + } + } + } + } + protected function createBackup(): void { if (false === BackupSQL::generate(self::JOB_NAME)) { diff --git a/Init.php b/Init.php index d91fb6a..cef04fb 100644 --- a/Init.php +++ b/Init.php @@ -45,6 +45,7 @@ public function update(): void Tools::settings('backup', 'weekly_day', 1); Tools::settings('backup', 'monthly_day', 1); Tools::settings('backup', 'hour', 3); + Tools::settings('backup', 'max_backups', 0); Tools::settingsSave(); } } diff --git a/Test/main/CronApplyBackupLimitTest.php b/Test/main/CronApplyBackupLimitTest.php new file mode 100644 index 0000000..89cd566 --- /dev/null +++ b/Test/main/CronApplyBackupLimitTest.php @@ -0,0 +1,136 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +namespace FacturaScripts\Test\Plugins; + +use FacturaScripts\Core\Tools; +use FacturaScripts\Plugins\Backup\Cron; +use FacturaScripts\Test\Traits\LogErrorsTrait; +use PHPUnit\Framework\TestCase; +use ReflectionMethod; + +/** + * @author Esteban Sanchez + */ +final class CronApplyBackupLimitTest extends TestCase +{ + use LogErrorsTrait; + + private string $folder = ''; + private int $sqlPrevios = 0; + private int $zipPrevios = 0; + + protected function setUp(): void + { + $this->folder = Tools::folder('MyFiles', 'Backups'); + Tools::folderCheckOrCreate($this->folder); + $this->sqlPrevios = count(glob($this->folder . DIRECTORY_SEPARATOR . '*.sql') ?: []); + $this->zipPrevios = count(glob($this->folder . DIRECTORY_SEPARATOR . '*.zip') ?: []); + } + + protected function tearDown(): void + { + foreach (glob($this->folder . DIRECTORY_SEPARATOR . '2000-01-01_*') ?: [] as $archivo) { + if (is_file($archivo)) { + unlink($archivo); + } + } + + $this->logErrors(); + } + + public function testSinLimiteNoElimina(): void + { + $archivos = $this->crearArchivos(['sql'], 5); + Tools::settingsSet('backup', 'max_backups', 0); + + $this->ejecutarLimite(); + + foreach ($archivos as $archivo) { + $this->assertFileExists($archivo, 'Con limite 0 no debe eliminar ningun archivo'); + } + } + + public function testEliminaLosMasAntiguosSql(): void + { + $archivos = $this->crearArchivos(['sql'], 5); + Tools::settingsSet('backup', 'max_backups', $this->sqlPrevios + 3); + + $this->ejecutarLimite(); + + $this->assertFileDoesNotExist($archivos[0], 'El sql mas antiguo debe eliminarse'); + $this->assertFileDoesNotExist($archivos[1], 'El segundo sql mas antiguo debe eliminarse'); + $this->assertFileExists($archivos[2], 'El tercer sql debe conservarse'); + $this->assertFileExists($archivos[3], 'El cuarto sql debe conservarse'); + $this->assertFileExists($archivos[4], 'El quinto sql debe conservarse'); + } + + public function testEliminaLosMasAntiguosZip(): void + { + $archivos = $this->crearArchivos(['zip'], 5); + Tools::settingsSet('backup', 'max_backups', $this->zipPrevios + 3); + + $this->ejecutarLimite(); + + $this->assertFileDoesNotExist($archivos[0], 'El zip mas antiguo debe eliminarse'); + $this->assertFileDoesNotExist($archivos[1], 'El segundo zip mas antiguo debe eliminarse'); + $this->assertFileExists($archivos[2], 'El tercer zip debe conservarse'); + $this->assertFileExists($archivos[3], 'El cuarto zip debe conservarse'); + $this->assertFileExists($archivos[4], 'El quinto zip debe conservarse'); + } + + public function testLimiteSeAplicaASqlYZipPorSeparado(): void + { + $archivosSql = $this->crearArchivos(['sql'], 5); + $archivosZip = $this->crearArchivos(['zip'], 5); + $limite = max($this->sqlPrevios, $this->zipPrevios) + 3; + Tools::settingsSet('backup', 'max_backups', $limite); + + $this->ejecutarLimite(); + + $restantesSql = array_filter($archivosSql, 'file_exists'); + $restantesZip = array_filter($archivosZip, 'file_exists'); + + $this->assertLessThanOrEqual(3, count($restantesSql), 'No deben quedar mas de 3 sql de test'); + $this->assertLessThanOrEqual(3, count($restantesZip), 'No deben quedar mas de 3 zip de test'); + } + + /** @return array */ + private function crearArchivos(array $extensiones, int $cantidad): array + { + $archivos = []; + for ($i = 1; $i <= $cantidad; $i++) { + foreach ($extensiones as $ext) { + $nombre = sprintf('2000-01-01_00-00-%02d.%s', $i, $ext); + $ruta = $this->folder . DIRECTORY_SEPARATOR . $nombre; + file_put_contents($ruta, ''); + $archivos[] = $ruta; + } + } + return $archivos; + } + + private function ejecutarLimite(): void + { + $cron = new Cron('Backup'); + $metodo = new ReflectionMethod(Cron::class, 'applyBackupLimit'); + $metodo->setAccessible(true); + $metodo->invoke($cron); + } +} \ No newline at end of file diff --git a/Translation/es_ES.json b/Translation/es_ES.json index 36a248c..8707d1b 100644 --- a/Translation/es_ES.json +++ b/Translation/es_ES.json @@ -6,6 +6,7 @@ "backup-charset-mixed-warning": "Advertencia: La copia de seguridad contiene tablas con múltiples codificaciones (%charsets%). Tu configuración actual es %config-charset%.", "backup-file-too-big": "El archivo de copia de seguridad es demasiado grande para subirlo al servidor. El tamaño máximo es %size% MB", "backup-memory-warning": "Advertencia: la copia de seguridad ocupa %size% MB, pero solamente tienes %memory% MB de memoria RAM disponible. Insuficiente para realizar la copia de seguridad. Como alternativa puedes comprimir la carpeta de FacturaScripts, que es equivalente a una copia de los archivos.", + "backup-deleted": "Copia de seguridad eliminada por límite: %file%", "backup-port-warning": "El puerto de MySQL debe ser el 3306, pero estás usando el %port%", "backup-use-fixer-plugin": "Se recomienda usar el plugin Fixer para corregir las codificaciones de las tablas antes de realizar copias de seguridad.", "config-mysql-charset-error": "Faltan las constantes de codificación en el archivo config.php ('%config-charset%', '%config-collate%')", @@ -13,6 +14,7 @@ "download-backup-p": "Descarga una copia de seguridad de tu base de datos y otra de tus archivos. Guarda estos ficheros en un lugar seguro. Si en algún momento quieres restaurar los datos de FacturaScripts, necesitarás estos ficheros.", "file-ready-to-download": "Archivo listo para descargar", "last-backup-more-30d": "La última copia de seguridad tiene más de 30 días. Haga una nueva desde el menú Administrador, Copias de seguridad.", + "max-backups": "Límite de copias de seguridad", "monthly-day": "Día del mes", "mysql-support-only": "Este plugin solamente funciona sobre MySQL", "new-admin-password": "Nueva contraseña del administrador", diff --git a/XMLView/SettingsBackup.xml b/XMLView/SettingsBackup.xml index bec0b7c..ec8c106 100644 --- a/XMLView/SettingsBackup.xml +++ b/XMLView/SettingsBackup.xml @@ -29,6 +29,9 @@ + + + \ No newline at end of file From 00888899f963ae48832dcff30f1ce0223f8f77a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esteban=20S=C3=A1nchez?= Date: Tue, 30 Jun 2026 19:18:05 +0200 Subject: [PATCH 2/3] =?UTF-8?q?A=C3=B1adir=20opci=C3=B3n=20para=20establec?= =?UTF-8?q?er=20el=20n=C3=BAmero=20m=C3=A1ximo=20de=20copias=20de=20seguri?= =?UTF-8?q?dad?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Se ha añadido la propiedad 'cron_max_backups' en Backup.php para definir el número máximo de copias de seguridad. - Se ha actualizado el archivo Backup.html.twig para mostrar el número máximo de copias de seguridad en la interfaz. --- Controller/Backup.php | 4 ++++ View/Backup.html.twig | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/Controller/Backup.php b/Controller/Backup.php index badc38b..32f1abc 100644 --- a/Controller/Backup.php +++ b/Controller/Backup.php @@ -56,6 +56,9 @@ class Backup extends Controller /** @var int */ public $cron_monthly_day = 1; + /** @var int */ + public $cron_max_backups = 0; + /** @var int */ public $cron_weekly_day = 1; @@ -98,6 +101,7 @@ public function privateCore(&$response, $user, $permissions) // cargamos la configuración del cron $this->cron_frequency = Tools::settings('backup', 'frequency', '1 week'); $this->cron_hour = Tools::settings('backup', 'hour', 3); + $this->cron_max_backups = (int) Tools::settings('backup', 'max_backups', 0); $this->cron_weekly_day = Tools::settings('backup', 'weekly_day', 1); $this->cron_monthly_day = Tools::settings('backup', 'monthly_day', 1); diff --git a/View/Backup.html.twig b/View/Backup.html.twig index 897fd99..19e8d82 100644 --- a/View/Backup.html.twig +++ b/View/Backup.html.twig @@ -347,6 +347,14 @@ {% endif %}
{{ trans('hour') }}:
{{ fsc.cron_hour }}:00
+
{{ trans('max-backups') }}:
+
+ {% if fsc.cron_max_backups == 0 %} + {{ trans('unlimited') }} + {% else %} + {{ fsc.cron_max_backups }} + {% endif %} +
From 976676a69fdeb90b0395a196961f9a123467b87e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esteban=20S=C3=A1nchez?= Date: Tue, 30 Jun 2026 20:04:20 +0200 Subject: [PATCH 3/3] Soporte para PostgreSQL en BackupSQL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Se modifica el test 'testGenerateReturnsFalseOnNonMySQL' para que valide la generación de copias de seguridad en PostgreSQL. Ahora se asegura que el método 'BackupSQL::generate' retorne verdadero cuando se utiliza PostgreSQL como tipo de base de datos. --- Test/main/BackupSQLTest.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Test/main/BackupSQLTest.php b/Test/main/BackupSQLTest.php index 12a8d7a..c0a9a34 100644 --- a/Test/main/BackupSQLTest.php +++ b/Test/main/BackupSQLTest.php @@ -162,14 +162,14 @@ public function testGenerateCreatesFile(): void $this->assertGreaterThan(0, filesize($newFile), 'El archivo .sql generado está vacío'); } - public function testGenerateReturnsFalseOnNonMySQL(): void + public function testGenerateReturnsTrueOnPostgreSQL(): void { - if (Tools::config('db_type') === 'mysql') { - $this->markTestSkipped('Test solo aplica cuando db_type != mysql'); + if (Tools::config('db_type') !== 'postgresql') { + $this->markTestSkipped('Test solo aplica con PostgreSQL'); } $result = BackupSQL::generate('test'); - $this->assertFalse($result); + $this->assertTrue($result, 'BackupSQL ahora soporta PostgreSQL y debe generar la copia'); } protected function setUp(): void