From 82882198727b4e05411c18f5683adeaef8ec01c7 Mon Sep 17 00:00:00 2001 From: imnavajas Date: Thu, 12 Jun 2025 14:57:48 +0200 Subject: [PATCH] Implementada logica para entornos dev/prod --- ci4/app/Commands/CatalogoLibroAsignarIskn.php | 2 +- ci4/app/Commands/CatalogoLibroImportar.php | 2 +- ci4/app/Commands/RestoreBackup.php | 93 +++++ ci4/app/Config/Routes/SistemaRoutes.php | 6 +- ci4/app/Controllers/Sistema/Backups.php | 368 +++++++++++++++--- .../2025-06-09-110500_CreateRestoresTable.php | 4 +- .../themes/vuexy/form/backups/backupList.php | 153 +++++--- 7 files changed, 511 insertions(+), 117 deletions(-) create mode 100644 ci4/app/Commands/RestoreBackup.php diff --git a/ci4/app/Commands/CatalogoLibroAsignarIskn.php b/ci4/app/Commands/CatalogoLibroAsignarIskn.php index 75a7b9a2..d7472678 100644 --- a/ci4/app/Commands/CatalogoLibroAsignarIskn.php +++ b/ci4/app/Commands/CatalogoLibroAsignarIskn.php @@ -9,7 +9,7 @@ use App\Models\Catalogo\IdentificadorIsknModel; class CatalogoLibroAsignarIskn extends BaseCommand { - protected $group = 'custom'; + protected $group = 'Safekat'; protected $name = 'catalogo:libro-asignar-iskn'; protected $description = 'Asigna ISKN directamente en la base de datos a los libros que no lo tienen.'; diff --git a/ci4/app/Commands/CatalogoLibroImportar.php b/ci4/app/Commands/CatalogoLibroImportar.php index e6beb331..6feadc1e 100644 --- a/ci4/app/Commands/CatalogoLibroImportar.php +++ b/ci4/app/Commands/CatalogoLibroImportar.php @@ -7,7 +7,7 @@ use CodeIgniter\CLI\CLI; class CatalogoLibroImportar extends BaseCommand { - protected $group = 'custom'; + protected $group = 'Safekat'; protected $name = 'catalogo:libro-importar'; protected $description = 'Importa los registros de catalogo_libro a catalogo_libros para un customer_id dado'; diff --git a/ci4/app/Commands/RestoreBackup.php b/ci4/app/Commands/RestoreBackup.php new file mode 100644 index 00000000..1b3a9839 --- /dev/null +++ b/ci4/app/Commands/RestoreBackup.php @@ -0,0 +1,93 @@ + 'Simula el proceso de restauración sin ejecutarlo realmente.', + ]; + + public function run(array $params) + { + $isDryRun = CLI::getOption('dry-run'); + + $backupDir = WRITEPATH . 'backups/'; + $backups = glob($backupDir . '*.zip'); + + if (empty($backups)) { + CLI::error("No se encontraron backups .zip en: $backupDir"); + return; + } + + CLI::write("Backups disponibles:", 'blue'); + foreach ($backups as $i => $file) { + CLI::write("[" . ($i + 1) . "] " . basename($file)); + } + + $index = CLI::prompt("Selecciona el número del backup a restaurar", null, 'required'); + + if (!is_numeric($index) || $index < 1 || $index > count($backups)) { + CLI::error("Selección no válida."); + return; + } + + $selectedFile = $backups[$index - 1]; + CLI::write("🎯 Seleccionado: " . basename($selectedFile), 'cyan'); + + if ($isDryRun) { + CLI::write("🔍 Modo simulación activado (--dry-run)", 'yellow'); + } + + $zip = new ZipArchive(); + if ($zip->open($selectedFile) !== TRUE) { + CLI::error("No se pudo abrir el archivo ZIP."); + return; + } + + $extractPath = WRITEPATH . 'backups/tmp_restore/'; + if (!is_dir($extractPath)) { + mkdir($extractPath, 0775, true); + } + + $zip->extractTo($extractPath); + $zip->close(); + + $sqlFiles = glob($extractPath . '*.sql'); + if (count($sqlFiles) === 0) { + CLI::error("No se encontró ningún .sql dentro del backup."); + return; + } + + $sqlFile = escapeshellarg($sqlFiles[0]); + $db = config('Database')->default; + + $cmd = "mysql -h {$db['hostname']} -u {$db['username']} -p'{$db['password']}' {$db['database']} < {$sqlFile}"; + + if ($isDryRun) { + CLI::write("📋 Comando que se ejecutaría:", 'yellow'); + CLI::write($cmd, 'light_gray'); + CLI::write("✅ Simulación completa. No se hizo ninguna modificación.", 'green'); + } else { + CLI::write("⏳ Restaurando...", 'yellow'); + system($cmd, $retval); + + if ($retval !== 0) { + CLI::error("❌ Error al restaurar la base de datos (código $retval)."); + } else { + CLI::write("✅ Backup restaurado correctamente.", 'green'); + } + } + + array_map('unlink', glob($extractPath . '*')); + rmdir($extractPath); + } +} diff --git a/ci4/app/Config/Routes/SistemaRoutes.php b/ci4/app/Config/Routes/SistemaRoutes.php index d0d0559d..7bb095cf 100644 --- a/ci4/app/Config/Routes/SistemaRoutes.php +++ b/ci4/app/Config/Routes/SistemaRoutes.php @@ -17,17 +17,19 @@ $routes->group('sistema', ['namespace' => 'App\Controllers\Sistema'], function ( }); /* Backups */ - $routes->group('backups', ['namespace' => 'App\Controllers\Sistema'], function ($routes) { + $routes->group('backups', function ($routes) { /**====================== * Tool *========================**/ $routes->get('', 'Backups::index', ['as' => 'backupsList']); $routes->get('create', 'Backups::create', ['as' => 'backupsCreate']); + $routes->get('create-dev', 'Backups::createDevelopment', ['as' => 'backupsCreateDev']); + $routes->get('backups/download/(:segment)', 'Backups::download/$1', ['as' => 'backupsDownload']); $routes->get('delete-local/(:num)', 'Backups::deleteLocal/$1', ['as' => 'backupsDeleteLocal']); + $routes->get('delete-local-dev/(:segment)', 'Backups::deleteLocalDevelopment/$1', ['as' => 'backupsDeleteLocalDev']); $routes->get('restore/(:segment)/local', 'Backups::restoreLocal/$1', ['as' => 'backupsRestoreLocal']); $routes->get('restore/(:segment)/remote', 'Backups::restoreRemote/$1', ['as' => 'backupsRestoreRemote']); - }); }); \ No newline at end of file diff --git a/ci4/app/Controllers/Sistema/Backups.php b/ci4/app/Controllers/Sistema/Backups.php index 40ef0b54..84538b10 100644 --- a/ci4/app/Controllers/Sistema/Backups.php +++ b/ci4/app/Controllers/Sistema/Backups.php @@ -19,51 +19,141 @@ class Backups extends BaseController { helper('filesystem'); - $entries = $this->backupModel->orderBy('created_at', 'DESC')->findAll(); - + $entorno = getenv('SK_ENVIRONMENT'); $backups = []; - foreach ($entries as $entry) { - $file = $entry['filename']; - $localPath = $entry['path_local']; - $remotePath = $entry['path_remote']; - $isLocal = $localPath && file_exists($localPath); - $isRemote = !empty($remotePath); + if ($entorno === 'development') { + // Leer archivos directamente del disco en entorno de desarrollo + $backups = []; - if ($isLocal) { - $fecha = date('Y-m-d H:i', filemtime($localPath)); - $tamano = filesize($localPath); - $tamanoFmt = number_format($tamano / 1024, 2) . ' KB'; + // === 1. Backups locales === + $localDir = WRITEPATH . 'backups/'; + $localFiles = get_filenames($localDir); - // Actualizar la BD si ha cambiado - if ($entry['size'] != $tamano) { - $this->backupModel->update($entry['id'], [ - 'size' => $tamano, - ]); + foreach ($localFiles as $file) { + if (!str_ends_with($file, '.zip') || !str_starts_with($file, 'backup_dev_')) { + continue; } - } else { - $fecha = $entry['created_at'] ?? '-'; - $tamano = $entry['size'] ?? null; - $tamanoFmt = $tamano ? number_format($tamano / 1024, 2) . ' KB' : '-'; + $localPath = $localDir . $file; + + $fecha = date('Y-m-d H:i', filemtime($localPath)); + $tamano = filesize($localPath); + $tamanoFmt = $tamano > 1048576 + ? number_format($tamano / 1048576, 2) . ' MB' + : number_format($tamano / 1024, 2) . ' KB'; + + $backups[$file] = [ + 'id' => null, + 'filename' => $file, + 'fecha' => $fecha, + 'tamano' => $tamanoFmt, + 'local' => true, + 'remoto' => false, + ]; } - $backups[] = [ - 'id' => $entry['id'], - 'filename' => $file, - 'fecha' => $fecha, - 'tamano' => $tamanoFmt, - 'local' => $isLocal, - 'remoto' => $isRemote, - ]; + // === 2. Backups remotos en SFTP === + $sftpHost = getenv('HIDRIVE_HOST'); + $sftpUser = getenv('HIDRIVE_USER'); + $sftpPass = getenv('HIDRIVE_PASS'); + $remoteDir = '/users/erp2019/backups_erp/'; + + $sftp = new SFTP($sftpHost); + if ($sftp->login($sftpUser, $sftpPass)) { + $remoteFiles = $sftp->nlist($remoteDir); + + foreach ($remoteFiles as $file) { + if (!str_ends_with($file, '.zip') || !str_starts_with($file, 'backup_')) { + continue; + } + + // Verificar si ya se cargó como local + $alreadyLocal = isset($backups[$file]); + + // Obtener info de archivo remoto + $stat = $sftp->stat($remoteDir . $file); + $fecha = isset($stat['mtime']) ? date('Y-m-d H:i', $stat['mtime']) : '-'; + $tamano = $stat['size'] ?? null; + $tamanoFmt = $tamano > 1048576 + ? number_format($tamano / 1048576, 2) . ' MB' + : number_format($tamano / 1024, 2) . ' KB'; + + $backups[$file] = [ + 'id' => null, + 'filename' => $file, + 'fecha' => $fecha, + 'tamano' => $tamanoFmt, + 'local' => $alreadyLocal, + 'remoto' => true, + ]; + } + } else { + // Opcional: mostrar un error o dejarlo pasar silenciosamente + session()->setFlashdata('warning', 'No se pudo conectar al servidor SFTP para obtener backups remotos.'); + } + + // Convertir a lista (por si se usó índice por filename) + $backups = array_values($backups); + + } else { + // En producción: seguir usando la base de datos + $entries = $this->backupModel->orderBy('created_at', 'DESC')->findAll(); + + foreach ($entries as $entry) { + $file = $entry['filename']; + + if (!str_ends_with($file, '.zip')) { + continue; + } + + $localPath = $entry['path_local']; + $remotePath = $entry['path_remote']; + + $isLocal = $localPath && file_exists($localPath); + $isRemote = !empty($remotePath); + + if ($isLocal) { + $fecha = date('Y-m-d H:i', filemtime($localPath)); + $tamano = filesize($localPath); + $tamanoFmt = $tamano > 1048576 + ? number_format($tamano / 1048576, 2) . ' MB' + : number_format($tamano / 1024, 2) . ' KB'; + + if ($entry['size'] != $tamano) { + $this->backupModel->update($entry['id'], [ + 'size' => $tamano, + ]); + } + + } else { + $fecha = $entry['created_at'] ?? '-'; + $tamano = $entry['size'] ?? null; + $tamanoFmt = $tamano > 1048576 + ? number_format($tamano / 1048576, 2) . ' MB' + : number_format($tamano / 1024, 2) . ' KB'; + } + + $backups[] = [ + 'id' => $entry['id'], + 'filename' => $file, + 'fecha' => $fecha, + 'tamano' => $tamanoFmt, + 'local' => $isLocal, + 'remoto' => $isRemote, + ]; + } } return view('themes/vuexy/form/backups/backupList', ['backups' => $backups]); } - public function create() { + if (getenv('SK_ENVIRONMENT') !== 'production') { + return redirect()->to(route_to('backupsList'))->with('error', 'No permitido en entorno de desarrollo.'); + } + helper('filesystem'); $timestamp = date('Ymd_His'); @@ -78,7 +168,8 @@ class Backups extends BaseController $password = $dbConfig['password']; $database = $dbConfig['database']; - $command = "mysqldump -h {$host} -u{$username} -p'{$password}' {$database} > {$sqlPath}"; + $command = "mysqldump -h {$host} -u" . escapeshellarg($username) . " -p'" . addslashes($password) . "' {$database} > {$sqlPath}"; + system($command, $retval); if ($retval !== 0) { @@ -116,9 +207,175 @@ class Backups extends BaseController 'status' => 'subido' ]); - return redirect()->to(route_to('backupsList'))->with('message', 'Backup creado, comprimido y enviado.'); + return redirect()->to(route_to('backupsList'))->with('message', 'Backup del entorno de produccion creado, comprimido y enviado al remoto.'); } + public function createDevelopment() + { + if (getenv('SK_ENVIRONMENT') !== 'development') { + return redirect()->to(route_to('backupsList'))->with('error', 'Esta acción solo está permitida en desarrollo.'); + } + + helper('filesystem'); + + $timestamp = date('Ymd_His'); + $sqlFilename = "backup_dev_{$timestamp}.sql"; + $zipFilename = "backup_dev_{$timestamp}.zip"; + $sqlPath = WRITEPATH . 'backups/' . $sqlFilename; + $zipPath = WRITEPATH . 'backups/' . $zipFilename; + + $dbConfig = config('Database')->default; + $host = $dbConfig['hostname']; + $username = $dbConfig['username']; + $password = $dbConfig['password']; + $database = $dbConfig['database']; + + $command = "mysqldump -h {$host} -u{$username} -p'{$password}' {$database} > {$sqlPath}"; + system($command, $retval); + + if ($retval !== 0) { + throw new \RuntimeException("Error al crear el backup local."); + } + + $zip = new \ZipArchive(); + if ($zip->open($zipPath, \ZipArchive::CREATE) === TRUE) { + $zip->addFile($sqlPath, $sqlFilename); + $zip->close(); + unlink($sqlPath); + } else { + throw new \RuntimeException("Error al comprimir el backup local."); + } + + // Ya no insertamos en la base de datos; el archivo queda en disco y se listará dinámicamente + return redirect()->to(route_to('backupsList'))->with('message', 'Backup local del entorno de desarrollo creado.'); + } + + + public function download($filename) + { + helper('filesystem'); + + $entorno = getenv('SK_ENVIRONMENT'); + + $backup = $this->backupModel->where('filename', $filename)->first(); + + // 1. Si hay entrada en la base de datos + if ($backup) { + $localPath = $backup['path_local']; + $remotePath = $backup['path_remote']; + + if ($localPath && file_exists($localPath)) { + return $this->response->download($localPath, null)->setFileName($filename); + } + + if (!empty($remotePath)) { + $sftpHost = getenv('HIDRIVE_HOST'); + $sftpUser = getenv('HIDRIVE_USER'); + $sftpPass = getenv('HIDRIVE_PASS'); + + $sftp = new SFTP($sftpHost); + + if (!$sftp->login($sftpUser, $sftpPass)) { + return redirect()->to(route_to('backupsList'))->with('error', 'No se pudo conectar al servidor SFTP.'); + } + + $fileContents = $sftp->get($remotePath); + if ($fileContents === false) { + return redirect()->to(route_to('backupsList'))->with('error', 'Error al descargar desde SFTP.'); + } + + $newLocalPath = WRITEPATH . 'backups/' . $filename; + write_file($newLocalPath, $fileContents); + + // Omitimos update() si estamos en desarrollo sin base de datos + if ($entorno === 'production') { + $this->backupModel->update($backup['id'], ['path_local' => $newLocalPath]); + } + + return $this->response->download($newLocalPath, null)->setFileName($filename); + } + + return redirect()->to(route_to('backupsList'))->with('error', 'No se pudo encontrar el archivo ni local ni remoto.'); + } + + // 2. Si NO hay entrada en la BD y estamos en desarrollo + if ($entorno === 'development') { + $localPath = WRITEPATH . 'backups/' . $filename; + + if (file_exists($localPath)) { + return $this->response->download($localPath, null)->setFileName($filename); + } + + // También se puede intentar buscar en el SFTP si quieres + $sftpHost = getenv('HIDRIVE_HOST'); + $sftpUser = getenv('HIDRIVE_USER'); + $sftpPass = getenv('HIDRIVE_PASS'); + $remotePath = '/users/erp2019/backups_erp/' . $filename; + + $sftp = new SFTP($sftpHost); + if ($sftp->login($sftpUser, $sftpPass)) { + $fileContents = $sftp->get($remotePath); + if ($fileContents !== false) { + $newLocalPath = WRITEPATH . 'backups/' . $filename; + write_file($newLocalPath, $fileContents); + return $this->response->download($newLocalPath, null)->setFileName($filename); + } + } + + return redirect()->to(route_to('backupsList'))->with('error', 'Archivo no encontrado local ni remoto (sin base de datos).'); + } + + return redirect()->to(route_to('backupsList'))->with('error', 'Backup no encontrado.'); + } + + + + public function deleteLocal($id) + { + $entorno = getenv('SK_ENVIRONMENT'); + $backup = $this->backupModel->find($id); + + if (!$backup) { + return redirect()->to(route_to('backupsList'))->with('error', 'Backup no encontrado en la base de datos.'); + } + + $localPath = $backup['path_local']; + + // Si existe el archivo, intentamos borrarlo + if ($localPath && file_exists($localPath)) { + unlink($localPath); + } + + if ($entorno === 'production') { + // Solo desvincular el archivo local + $this->backupModel->update($id, ['path_local' => null]); + return redirect()->to(route_to('backupsList'))->with('message', 'Archivo local eliminado (registro conservado).'); + } else { + // Eliminar completamente en desarrollo + $this->backupModel->delete($id); + return redirect()->to(route_to('backupsList'))->with('message', 'Backup de desarrollo eliminado completamente.'); + } + } + + public function deleteLocalDevelopment($filename) + { + $entorno = getenv('SK_ENVIRONMENT'); + if ($entorno !== 'development') { + return redirect()->to(route_to('backupsList'))->with('error', 'Solo permitido en desarrollo.'); + } + + $path = WRITEPATH . 'backups/' . $filename; + + if (file_exists($path)) { + unlink($path); + return redirect()->to(route_to('backupsList'))->with('message', "Archivo '$filename' eliminado del sistema de archivos."); + } + + return redirect()->to(route_to('backupsList'))->with('error', "Archivo '$filename' no encontrado en el sistema."); + } + + + public function restoreLocal($file) { $path = WRITEPATH . 'backups/' . $file; @@ -126,7 +383,7 @@ class Backups extends BaseController throw new \CodeIgniter\Exceptions\PageNotFoundException("Backup no encontrado."); } - $zip = new ZipArchive(); + $zip = new \ZipArchive(); if ($zip->open($path) === TRUE) { $extractPath = WRITEPATH . 'backups/tmp_restore/'; if (!is_dir($extractPath)) { @@ -140,31 +397,32 @@ class Backups extends BaseController throw new \RuntimeException("No se encontró ningún archivo .sql en el ZIP"); } - $db = \Config\Database::connect(); - $sql = file_get_contents($sqlFiles[0]); - $db->query('SET FOREIGN_KEY_CHECKS=0;'); - $db->query($sql); - $db->query('SET FOREIGN_KEY_CHECKS=1;'); + $sqlFile = $sqlFiles[0]; + + $dbConfig = config('Database')->default; + $host = $dbConfig['hostname']; + $username = $dbConfig['username']; + $password = $dbConfig['password']; + $database = $dbConfig['database']; + + $cmd = "mysql -h {$host} -u{$username} -p'{$password}' {$database} < {$sqlFile}"; + system($cmd, $retval); + + if ($retval !== 0) { + throw new \RuntimeException("Error al restaurar la base de datos. Código: $retval"); + } array_map('unlink', glob($extractPath . '*')); rmdir($extractPath); - return redirect()->to('/backups')->with('message', 'Backup restaurado correctamente.'); + return redirect()->to(route_to('backupsList'))->with('message', 'Backup restaurado correctamente (vía sistema).'); } else { throw new \RuntimeException("No se pudo abrir el archivo ZIP"); } } - public function deleteLocal($id) - { - $backup = $this->backupModel->find($id); - if ($backup && $backup['path_local'] && file_exists($backup['path_local'])) { - unlink($backup['path_local']); - $this->backupModel->update($id, ['path_local' => null]); - return redirect()->to(route_to('backupsList'))->with('message', 'Backup local eliminado.'); - } - return redirect()->to(route_to('backupsList'))->with('error', 'Archivo no encontrado.'); - } + + public function restoreRemote($filename) { @@ -178,9 +436,9 @@ class Backups extends BaseController } // Parámetros SFTP - $sftpHost = 'sftp.hidrive.ionos.com'; - $sftpUser = 'erp2019'; - $sftpPass = 'Z2CjX7kd2h'; + $sftpHost = getenv('HIDRIVE_HOST'); + $sftpUser = getenv('HIDRIVE_USER'); + $sftpPass = getenv('HIDRIVE_PASS'); $remotePath = $backup['path_remote']; $localPath = WRITEPATH . 'backups/' . $filename; @@ -216,9 +474,9 @@ class Backups extends BaseController private function sendToSFTP($localPath, $remoteFilename) { - $sftpHost = 'sftp.hidrive.ionos.com'; - $sftpUser = 'erp2019'; - $sftpPass = 'Z2CjX7kd2h'; + $sftpHost = getenv('HIDRIVE_HOST'); + $sftpUser = getenv('HIDRIVE_USER'); + $sftpPass = getenv('HIDRIVE_PASS'); $remotePath = '/users/erp2019/backups_erp/' . $remoteFilename; $sftp = new SFTP($sftpHost); diff --git a/ci4/app/Database/Migrations/2025-06-09-110500_CreateRestoresTable.php b/ci4/app/Database/Migrations/2025-06-09-110500_CreateRestoresTable.php index 16ecf321..63ea6d2a 100644 --- a/ci4/app/Database/Migrations/2025-06-09-110500_CreateRestoresTable.php +++ b/ci4/app/Database/Migrations/2025-06-09-110500_CreateRestoresTable.php @@ -16,11 +16,11 @@ class CreateRestoresTable extends Migration ]); $this->forge->addKey('id', true); $this->forge->addForeignKey('backup_id', 'backups', 'id', 'CASCADE', 'CASCADE'); - $this->forge->createTable('restores'); + $this->forge->createTable('backup_restores'); } public function down() { - $this->forge->dropTable('restores'); + $this->forge->dropTable('backup_restores'); } } diff --git a/ci4/app/Views/themes/vuexy/form/backups/backupList.php b/ci4/app/Views/themes/vuexy/form/backups/backupList.php index 6984f811..2b8be11e 100644 --- a/ci4/app/Views/themes/vuexy/form/backups/backupList.php +++ b/ci4/app/Views/themes/vuexy/form/backups/backupList.php @@ -1,70 +1,111 @@ include("themes/_commonPartialsBs/select2bs5") ?> include("themes/_commonPartialsBs/datatables") ?> +include("themes/_commonPartialsBs/sweetalert") ?> extend('themes/vuexy/main/defaultlayout') ?> -section('content'); ?> - +section("content") ?>
-
-

Backups disponibles

- Crear backup +
+
+
+

Backups disponibles

+
-
- - - - - - - - - - - - - - - - - - - - - - - -
ArchivoFechaTamañoLocalRemotoAcciones
- - - - - - Restaurar Local - Eliminar local - - Restaurar Remoto - - No disponible - -
+
+ -
+ + Crear backup + + Crear backup desarrollo + - -
-
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
ArchivoFechaTamañoLocalRemotoAcciones
+ + + + + + + + + + + Descargar + + + + + + + Restaurar Local + + + + + + Eliminar [PROD] + + + + Eliminar [DEV] + + + + + + Restaurar Remoto + + + No disponible + +
+
+ +
+
+
+ endSection() ?> section('additionalInlineJs') ?> $(document).ready(function () { - $('#tablaBackups').DataTable({ - order: [[1, 'desc']], - language: { - url: '/assets/vendor/datatables/i18n/es-ES.json' // ajusta si usas idioma español - } - }); +$('#tablaBackups').DataTable({ +order: [[1, 'desc']] +}); }); endSection() ?> \ No newline at end of file