Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Controller/Backup.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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);

Expand Down
32 changes: 32 additions & 0 deletions Cron.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Comment thread
daniel89fg marked this conversation as resolved.
$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)) {
Expand Down
1 change: 1 addition & 0 deletions Init.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
8 changes: 4 additions & 4 deletions Test/main/BackupSQLTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
136 changes: 136 additions & 0 deletions Test/main/CronApplyBackupLimitTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
<?php
/**
* This file is part of Backup plugin for FacturaScripts
* Copyright (C) 2026 Carlos Garcia Gomez <carlos@facturascripts.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/

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 <esteban@factura.city>
*/
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<string> */
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);
}
}
2 changes: 2 additions & 0 deletions Translation/es_ES.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand All @@ -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",
Expand Down
8 changes: 8 additions & 0 deletions View/Backup.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,14 @@
{% endif %}
<dt class="col-sm-5">{{ trans('hour') }}:</dt>
<dd class="col-sm-7">{{ fsc.cron_hour }}:00</dd>
<dt class="col-sm-5">{{ trans('max-backups') }}:</dt>
<dd class="col-sm-7">
{% if fsc.cron_max_backups == 0 %}
{{ trans('unlimited') }}
{% else %}
{{ fsc.cron_max_backups }}
{% endif %}
</dd>
</dl>
</div>
</div>
Expand Down
3 changes: 3 additions & 0 deletions XMLView/SettingsBackup.xml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@
<column name="hour" id="backup_hour" numcolumns="3" order="130">
<widget type="number" fieldname="hour" min="0" max="23" required="true"/>
</column>
<column name="max-backups" id="backup_max_backups" numcolumns="3" order="140">
<widget type="number" fieldname="max_backups" min="0" required="true"/>
</column>
</group>
</columns>
</view>