diff --git a/Controller/Backup.php b/Controller/Backup.php index c66d124..9aa3c47 100644 --- a/Controller/Backup.php +++ b/Controller/Backup.php @@ -57,6 +57,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; @@ -99,6 +102,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/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/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 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 97afba0..8497a0b 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-restore-error": "Error al restaurar la copia de seguridad: %error%", "backup-restore-strict-mode": "El servidor MySQL tiene activado innodb_strict_mode y alguna tabla supera el tamaño máximo de fila. Pide a tu proveedor que desactive innodb_strict_mode en la configuración de MySQL, o que conceda al usuario de la base de datos el privilegio SESSION_VARIABLES_ADMIN.", @@ -15,6 +16,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/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 %} +
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