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