From d7a85ca04fa30a5f31af5a563b399ef5f9afe36e Mon Sep 17 00:00:00 2001 From: imnavajas Date: Fri, 2 May 2025 14:10:49 +0200 Subject: [PATCH 1/4] Primeros pasos sistema backup --- ci4/app/Config/Routes.php | 6 -- ci4/app/Config/Routes/SistemaRoutes.php | 30 +++++++ ci4/app/Controllers/Sistema/Backups.php | 83 +++++++++++++++++++ ci4/app/Language/es/App.php | 58 +------------ ci4/app/Language/es/RolesPermisos.php | 1 + .../themes/vuexy/form/backups/backupList.php | 28 +++++++ .../themes/vuexy/main/menus/sistema_menu.php | 18 +++- ci4/writable/backups/index.html | 11 +++ 8 files changed, 172 insertions(+), 63 deletions(-) create mode 100644 ci4/app/Config/Routes/SistemaRoutes.php create mode 100644 ci4/app/Controllers/Sistema/Backups.php create mode 100644 ci4/app/Views/themes/vuexy/form/backups/backupList.php create mode 100644 ci4/writable/backups/index.html diff --git a/ci4/app/Config/Routes.php b/ci4/app/Config/Routes.php index 352a871e..a0d6df47 100755 --- a/ci4/app/Config/Routes.php +++ b/ci4/app/Config/Routes.php @@ -20,12 +20,6 @@ $routes->get('viewmode/(:alpha)', 'Viewmode::index/$1'); $routes->get('test', 'Test::index'); -$routes->group('activity', ['namespace' => 'App\Controllers\Sistema'], function ($routes) { - $routes->get('', 'Actividad::index', ['as' => 'activityList']); - $routes->post('datatable', 'Actividad::datatable', ['as' => 'activityDT']); -}); - - /* * -------------------------------------------------------------------- * Route Definitions diff --git a/ci4/app/Config/Routes/SistemaRoutes.php b/ci4/app/Config/Routes/SistemaRoutes.php new file mode 100644 index 00000000..7a2b1d2a --- /dev/null +++ b/ci4/app/Config/Routes/SistemaRoutes.php @@ -0,0 +1,30 @@ +group('sistema', ['namespace' => 'App\Controllers\Sistema'], function ($routes) { + /* Actividad */ + $routes->group('actividad', ['namespace' => 'App\Controllers\Sistema'], function ($routes) { + /**====================== + * CRUD + *========================**/ + $routes->get('', 'Actividad::index', ['as' => 'activityList']); + $routes->post('datatable', 'Actividad::datatable', ['as' => 'activityDT']); + + }); + + /* Actividad */ + $routes->group('backups', ['namespace' => 'App\Controllers\Sistema'], function ($routes) { + /**====================== + * Tool + *========================**/ + $routes->get('', 'Backups::index', ['as' => 'backupsList']); + $routes->get('/create', 'Backups::create', ['as' => 'backupsCreate']); + $routes->get('restore/(:segment)', 'Backups::restore/$1', ['as' => 'backupsRestore']); + + }); + +}); \ No newline at end of file diff --git a/ci4/app/Controllers/Sistema/Backups.php b/ci4/app/Controllers/Sistema/Backups.php new file mode 100644 index 00000000..0d6fd9b8 --- /dev/null +++ b/ci4/app/Controllers/Sistema/Backups.php @@ -0,0 +1,83 @@ + $files]); + } + + public function create() + { + helper('filesystem'); + + $filename = 'backup_' . date('Ymd_His') . '.sql'; + $path = WRITEPATH . 'backups/' . $filename; + + $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} > {$path}"; + + system($command, $retval); + + if ($retval !== 0) { + throw new \RuntimeException("Error al crear el backup."); + } + + // Enviar a SFTP + $this->sendToSFTP($path, $filename); + + return redirect()->to(route_to('backupsList'))->with('message', 'Backup creado y enviado.'); + } + + public function restore($file) + { + $path = WRITEPATH . 'backups/' . $file; + if (!file_exists($path)) { + throw new \CodeIgniter\Exceptions\PageNotFoundException("Backup no encontrado."); + } + + $db = \Config\Database::connect(); + $sql = file_get_contents($path); + $db->query('SET FOREIGN_KEY_CHECKS=0;'); + $db->query($sql); + $db->query('SET FOREIGN_KEY_CHECKS=1;'); + + return redirect()->to('/backups')->with('message', 'Backup restaurado.'); + } + + private function sendToSFTP($localPath, $remoteFilename) + { + + $sftpHost = 'sftp.hidrive.ionos.com'; + $sftpUser = 'erp2019'; + $sftpPass = 'Z2CjX7kd2h'; + $remotePath = '/users/erp2019/backups_erp/' . $remoteFilename; + + $sftp = new SFTP($sftpHost); + + if (!$sftp->login($sftpUser, $sftpPass)) { + throw new \RuntimeException('Error de autenticación SFTP'); + } + + $fileContents = file_get_contents($localPath); + + if (!$sftp->put($remotePath, $fileContents)) { + throw new \RuntimeException("No se pudo subir el backup al servidor SFTP."); + } + + } +} diff --git a/ci4/app/Language/es/App.php b/ci4/app/Language/es/App.php index 417cb626..b64ca4ba 100755 --- a/ci4/app/Language/es/App.php +++ b/ci4/app/Language/es/App.php @@ -315,59 +315,7 @@ return [ "group_rules_title_r" => "¡El campo del nombre del grupo es obligatorio!", "group_rules_dashboard_r" => "¡El campo del panel es obligatorio!", - // GROUP - Rules Name - /* JJO - "group_rules_label_group" => "Permiso de grupo", - "group_rules_label_user" => "Usuario", - "group_rules_label_settings" => "Configuración", - "group_rules_label_index" => "Lista", - "group_rules_label_add" => "Agregar", - "group_rules_label_edit" => "Editar", - "group_rules_label_delete" => "Eliminar", - "group_rules_label_store" => "Guardar", - "group_rules_label_oauth" => "Autenticaciones", - "group_rules_label_template" => "Plantillas", - "group_rules_label_all" => "Ver todo", - "group_rules_label_my" => "Mis notificaciones", - "group_rules_label_view" => "Ver notificación", - "group_rules_label_oauth_store" => "Guardar oAuth", - "group_rules_label_template_store" => "Guardar plantillas", - */ - - // AUTH - index - "oauth_title" => "Autenticación oAuth", - "oauth_subtitle" => "Configuración de autenticación de redes sociales", - "oauth_label_id" => "ID de la Cuenta", - "oauth_label_id_ph" => "Escriba su id de la cuenta", - "oauth_label_key" => "Key de la Cuenta", - "oauth_label_key_ph" => "Escriba su key de la cuenta", - "oauth_label_secret" => "Llave Secreta", - "oauth_label_secret_ph" => "Escriba su llave secreta", - "oauth_label_view" => "Mostrar texto", - "oauth_label_active" => "Activar red social", - "oauth_alert_add" => "¡Guardado exitosamente!", - "oauth_alert_error" => "¡Error al guardar!", - - // TEMPLATE - index - "template_title" => "Plantillas", - "template_subtitle" => "Configuración de Plantilla", - "template_subtitle_email" => "Plantillas de Correo Electrónico", - "template_label_title" => "Título", - "template_label_title_ph" => "Escriba su título", - "template_label_message" => "Mensaje", - "template_label_forgot_pass" => "Olvido la contraseña", - "template_label_welcome" => "Bienvenida", - "template_label_tfa" => "Autenticación de dos factores (2FA)", - "template_label_tag" => "Ver palabras clave", - "template_alert_add" => "¡Guardado exitosamente!", - "template_alert_error" => "¡Error al guardar!", - "template_modal_title" => "Palabras Clave", - "template_modal_subtitle" => "A continuación, se muestran algunas palabras clave que se pueden incorporar al texto:", - "template_modal_btn_1" => "Cerrar", - "template_modal_copy" => "Copiado!", - "template_modal_copy_msg" => "¡Copiado con éxito!", - "template_label_confirmation_email" => "Confirmación por correo electrónico", - "template_label_notification" => "Notificación de cuentas nuevas", + // SETTINGS - index "settings_title" => "Configuración", @@ -633,7 +581,6 @@ return [ "permisos_tarifaenvio" => "Envío", "permisos_tarifaimpresion" => "Impresión", - "permisos_configuracion" => "Configuración", "permisos_tareasservicio" => "Tareas servicio", "permisos_formaspago" => "Formas de pago", "permisos_papelgenerico" => "Papel genérico", @@ -826,11 +773,10 @@ return [ "menu_logout" => "Salir", "menu_profile" => "Mi Perfil", "menu_activity" => "Actividad", + "menu_backups" => "Backups", "menu_notification" => "Notificaciones", "menu_list" => "Lista", "menu_add" => "Agregar", - "menu_oauth" => "Autenticaciones", - "menu_template" => "Plantillas", "menu_soporte" => "Soporte", "menu_soporte_new_ticket" => "Crear ticket", diff --git a/ci4/app/Language/es/RolesPermisos.php b/ci4/app/Language/es/RolesPermisos.php index e9d23840..9326991d 100755 --- a/ci4/app/Language/es/RolesPermisos.php +++ b/ci4/app/Language/es/RolesPermisos.php @@ -62,6 +62,7 @@ return [ 'seriesFacturasSection' => 'Series facturas', 'ajustesSection' => 'Ajustes', 'actividadSection' => 'Accesos', + 'backupSection' => 'Backups', 'facturasSection' => 'Facturas', 'albaranesPermission' => 'Albaranes', 'vencimientosPermission' => 'Vencimientos', diff --git a/ci4/app/Views/themes/vuexy/form/backups/backupList.php b/ci4/app/Views/themes/vuexy/form/backups/backupList.php new file mode 100644 index 00000000..cc306dce --- /dev/null +++ b/ci4/app/Views/themes/vuexy/form/backups/backupList.php @@ -0,0 +1,28 @@ +include("themes/_commonPartialsBs/select2bs5") ?> +include("themes/_commonPartialsBs/datatables") ?> +extend('themes/vuexy/main/defaultlayout') ?> + +section('content'); ?> + +
+
+ +

Backups disponibles

+ Crear backup + + +
+
+endSection() ?> + +section('additionalInlineJs') ?> + + +endSection() ?> \ No newline at end of file diff --git a/ci4/app/Views/themes/vuexy/main/menus/sistema_menu.php b/ci4/app/Views/themes/vuexy/main/menus/sistema_menu.php index 7d7a649c..870964cc 100755 --- a/ci4/app/Views/themes/vuexy/main/menus/sistema_menu.php +++ b/ci4/app/Views/themes/vuexy/main/menus/sistema_menu.php @@ -3,7 +3,7 @@ * SEPARADOR Y MENUS DE SISTEMA */ -if (auth()->user()->can('actividad.menu')) { +if (auth()->user()->can('actividad.menu') || auth()->user()->can('backup.menu')) { ?> + + user()->can('backup.menu')) { + ?> + + + + diff --git a/ci4/writable/backups/index.html b/ci4/writable/backups/index.html new file mode 100644 index 00000000..b702fbc3 --- /dev/null +++ b/ci4/writable/backups/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + From 6967a61d93198d860baf2630245f9f72ab60dca7 Mon Sep 17 00:00:00 2001 From: imnavajas Date: Mon, 9 Jun 2025 15:18:12 +0200 Subject: [PATCH 2/4] Avances --- ci4/app/Config/Routes/SistemaRoutes.php | 3 +- ci4/app/Controllers/Sistema/Backups.php | 146 +++++++++++++++--- .../2025-06-09-102500_CreateBackupsTable.php | 33 ++++ .../2025-06-09-110500_CreateRestoresTable.php | 26 ++++ ci4/app/Models/Sistema/BackupModel.php | 15 ++ .../themes/vuexy/form/backups/backupList.php | 68 ++++++-- 6 files changed, 258 insertions(+), 33 deletions(-) create mode 100644 ci4/app/Database/Migrations/2025-06-09-102500_CreateBackupsTable.php create mode 100644 ci4/app/Database/Migrations/2025-06-09-110500_CreateRestoresTable.php create mode 100644 ci4/app/Models/Sistema/BackupModel.php diff --git a/ci4/app/Config/Routes/SistemaRoutes.php b/ci4/app/Config/Routes/SistemaRoutes.php index 7a2b1d2a..a0a8a90c 100644 --- a/ci4/app/Config/Routes/SistemaRoutes.php +++ b/ci4/app/Config/Routes/SistemaRoutes.php @@ -22,7 +22,8 @@ $routes->group('sistema', ['namespace' => 'App\Controllers\Sistema'], function ( * Tool *========================**/ $routes->get('', 'Backups::index', ['as' => 'backupsList']); - $routes->get('/create', 'Backups::create', ['as' => 'backupsCreate']); + $routes->get('create', 'Backups::create', ['as' => 'backupsCreate']); + $routes->get('delete-local/(:num)', 'Backups::deleteLocal/$1', ['as' => 'backupsDeleteLocal']); $routes->get('restore/(:segment)', 'Backups::restore/$1', ['as' => 'backupsRestore']); }); diff --git a/ci4/app/Controllers/Sistema/Backups.php b/ci4/app/Controllers/Sistema/Backups.php index 0d6fd9b8..28e93ff3 100644 --- a/ci4/app/Controllers/Sistema/Backups.php +++ b/ci4/app/Controllers/Sistema/Backups.php @@ -1,46 +1,122 @@ backupModel = new BackupModel(); + } + public function index() { helper('filesystem'); - // Muestra la vista con la lista de backups - $files = directory_map(WRITEPATH . 'backups/', 1); - return view('themes/vuexy/form/backups/backupList', ['files' => $files]); + + $entries = $this->backupModel->orderBy('created_at', 'DESC')->findAll(); + + $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 ($isLocal) { + $fecha = date('Y-m-d H:i', filemtime($localPath)); + $tamano = filesize($localPath); + $tamanoFmt = number_format($tamano / 1024, 2) . ' KB'; + + // Actualizar la BD si ha cambiado + if ($entry['size'] != $tamano) { + $this->backupModel->update($entry['id'], [ + 'size' => $tamano, + ]); + } + + } else { + $fecha = $entry['created_at'] ?? '-'; + $tamano = $entry['size'] ?? null; + $tamanoFmt = $tamano ? 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() { helper('filesystem'); - $filename = 'backup_' . date('Ymd_His') . '.sql'; - $path = WRITEPATH . 'backups/' . $filename; + $timestamp = date('Ymd_His'); + $sqlFilename = "backup_{$timestamp}.sql"; + $zipFilename = "backup_{$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} > {$path}"; - + $command = "mysqldump -h {$host} -u{$username} -p'{$password}' {$database} > {$sqlPath}"; system($command, $retval); if ($retval !== 0) { throw new \RuntimeException("Error al crear el backup."); } - // Enviar a SFTP - $this->sendToSFTP($path, $filename); + // Crear el zip + $zip = new ZipArchive(); + if ($zip->open($zipPath, ZipArchive::CREATE) === TRUE) { + $zip->addFile($sqlPath, $sqlFilename); + $zip->close(); + unlink($sqlPath); // eliminar el .sql original + } else { + throw new \RuntimeException("Error al comprimir el backup."); + } - return redirect()->to(route_to('backupsList'))->with('message', 'Backup creado y enviado.'); + // Insertar en BD + $backupId = $this->backupModel->insert([ + 'filename' => $zipFilename, + 'type' => 'manual', + 'path_local' => $zipPath, + 'path_remote' => null, + 'size' => filesize($zipPath), + 'status' => 'pendiente', + 'created_at' => date('Y-m-d H:i:s') + ], true); + + // Enviar a SFTP + $this->sendToSFTP($zipPath, $zipFilename); + + // Actualizar BD + $remotePath = "/users/erp2019/backups_erp/" . $zipFilename; + $this->backupModel->update($backupId, [ + 'path_remote' => $remotePath, + 'status' => 'subido' + ]); + + return redirect()->to(route_to('backupsList'))->with('message', 'Backup creado, comprimido y enviado.'); } public function restore($file) @@ -50,18 +126,49 @@ class Backups extends BaseController throw new \CodeIgniter\Exceptions\PageNotFoundException("Backup no encontrado."); } - $db = \Config\Database::connect(); - $sql = file_get_contents($path); - $db->query('SET FOREIGN_KEY_CHECKS=0;'); - $db->query($sql); - $db->query('SET FOREIGN_KEY_CHECKS=1;'); + $zip = new ZipArchive(); + if ($zip->open($path) === TRUE) { + $extractPath = WRITEPATH . 'backups/tmp_restore/'; + if (!is_dir($extractPath)) { + mkdir($extractPath, 0775, true); + } + $zip->extractTo($extractPath); + $zip->close(); - return redirect()->to('/backups')->with('message', 'Backup restaurado.'); + $sqlFiles = glob($extractPath . '*.sql'); + if (count($sqlFiles) === 0) { + 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;'); + + array_map('unlink', glob($extractPath . '*')); + rmdir($extractPath); + + return redirect()->to('/backups')->with('message', 'Backup restaurado correctamente.'); + } 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.'); + } + + private function sendToSFTP($localPath, $remoteFilename) { - $sftpHost = 'sftp.hidrive.ionos.com'; $sftpUser = 'erp2019'; $sftpPass = 'Z2CjX7kd2h'; @@ -78,6 +185,5 @@ class Backups extends BaseController if (!$sftp->put($remotePath, $fileContents)) { throw new \RuntimeException("No se pudo subir el backup al servidor SFTP."); } - } } diff --git a/ci4/app/Database/Migrations/2025-06-09-102500_CreateBackupsTable.php b/ci4/app/Database/Migrations/2025-06-09-102500_CreateBackupsTable.php new file mode 100644 index 00000000..d21b46f0 --- /dev/null +++ b/ci4/app/Database/Migrations/2025-06-09-102500_CreateBackupsTable.php @@ -0,0 +1,33 @@ +forge->addField([ + 'id' => [ + 'type' => 'INT', + 'unsigned' => true, + 'auto_increment' => true + ], + 'filename' => ['type' => 'VARCHAR', 'constraint' => 255], + 'type' => ['type' => 'ENUM', 'constraint' => ['manual', 'cron']], + 'path_local' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true], + 'path_remote' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true], + 'size' => ['type' => 'INT', 'unsigned' => true, 'null' => true], + 'status' => ['type' => 'ENUM', 'constraint' => ['pendiente', 'subido', 'error'], 'default' => 'pendiente'], + 'created_at' => ['type' => 'DATETIME', 'null' => false], + 'updated_at' => ['type' => 'DATETIME', 'null' => true], + ]); + $this->forge->addKey('id', true); + $this->forge->createTable('backups'); + } + + public function down() + { + $this->forge->dropTable('backups'); + } +} \ No newline at end of file diff --git a/ci4/app/Database/Migrations/2025-06-09-110500_CreateRestoresTable.php b/ci4/app/Database/Migrations/2025-06-09-110500_CreateRestoresTable.php new file mode 100644 index 00000000..16ecf321 --- /dev/null +++ b/ci4/app/Database/Migrations/2025-06-09-110500_CreateRestoresTable.php @@ -0,0 +1,26 @@ +forge->addField([ + 'id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true], + 'backup_id' => ['type' => 'INT', 'unsigned' => true], + 'restored_by' => ['type' => 'VARCHAR', 'constraint' => 100], + 'source' => ['type' => 'ENUM', 'constraint' => ['local', 'remote']], + 'restored_at' => ['type' => 'DATETIME'], + ]); + $this->forge->addKey('id', true); + $this->forge->addForeignKey('backup_id', 'backups', 'id', 'CASCADE', 'CASCADE'); + $this->forge->createTable('restores'); + } + + public function down() + { + $this->forge->dropTable('restores'); + } +} diff --git a/ci4/app/Models/Sistema/BackupModel.php b/ci4/app/Models/Sistema/BackupModel.php new file mode 100644 index 00000000..71ecf031 --- /dev/null +++ b/ci4/app/Models/Sistema/BackupModel.php @@ -0,0 +1,15 @@ +
+

Backups disponibles

+ Crear backup + +
+ + + + + + + + + + + + + + + + + + + + + + + +
ArchivoFechaTamañoLocalRemotoAcciones
+ + + + + + Restaurar + Eliminar local + + Restaurar Remoto + + No disponible + +
+ +
-

Backups disponibles

- Crear backup -
endSection() ?> section('additionalInlineJs') ?> - - + endSection() ?> \ No newline at end of file From 7aa577f316b4d233e1993aa613350a8bf3e3e53b Mon Sep 17 00:00:00 2001 From: imnavajas Date: Tue, 10 Jun 2025 15:10:19 +0200 Subject: [PATCH 3/4] Avances --- ci4/app/Config/Routes/SistemaRoutes.php | 6 ++- ci4/app/Controllers/Sistema/Backups.php | 49 ++++++++++++++++++- .../themes/vuexy/form/backups/backupList.php | 24 +++++---- 3 files changed, 63 insertions(+), 16 deletions(-) diff --git a/ci4/app/Config/Routes/SistemaRoutes.php b/ci4/app/Config/Routes/SistemaRoutes.php index a0a8a90c..d0d0559d 100644 --- a/ci4/app/Config/Routes/SistemaRoutes.php +++ b/ci4/app/Config/Routes/SistemaRoutes.php @@ -16,7 +16,7 @@ $routes->group('sistema', ['namespace' => 'App\Controllers\Sistema'], function ( }); - /* Actividad */ + /* Backups */ $routes->group('backups', ['namespace' => 'App\Controllers\Sistema'], function ($routes) { /**====================== * Tool @@ -24,7 +24,9 @@ $routes->group('sistema', ['namespace' => 'App\Controllers\Sistema'], function ( $routes->get('', 'Backups::index', ['as' => 'backupsList']); $routes->get('create', 'Backups::create', ['as' => 'backupsCreate']); $routes->get('delete-local/(:num)', 'Backups::deleteLocal/$1', ['as' => 'backupsDeleteLocal']); - $routes->get('restore/(:segment)', 'Backups::restore/$1', ['as' => 'backupsRestore']); + $routes->get('restore/(:segment)/local', 'Backups::restoreLocal/$1', ['as' => 'backupsRestoreLocal']); + $routes->get('restore/(:segment)/remote', 'Backups::restoreRemote/$1', ['as' => 'backupsRestoreRemote']); + }); diff --git a/ci4/app/Controllers/Sistema/Backups.php b/ci4/app/Controllers/Sistema/Backups.php index 28e93ff3..40ef0b54 100644 --- a/ci4/app/Controllers/Sistema/Backups.php +++ b/ci4/app/Controllers/Sistema/Backups.php @@ -119,7 +119,7 @@ class Backups extends BaseController return redirect()->to(route_to('backupsList'))->with('message', 'Backup creado, comprimido y enviado.'); } - public function restore($file) + public function restoreLocal($file) { $path = WRITEPATH . 'backups/' . $file; if (!file_exists($path)) { @@ -166,6 +166,53 @@ class Backups extends BaseController return redirect()->to(route_to('backupsList'))->with('error', 'Archivo no encontrado.'); } + public function restoreRemote($filename) + { + helper('filesystem'); + + // Buscar el backup en la base de datos + $backup = $this->backupModel->where('filename', $filename)->first(); + + if (!$backup || empty($backup['path_remote'])) { + return redirect()->to(route_to('backupsList'))->with('error', 'Backup remoto no encontrado en la base de datos.'); + } + + // Parámetros SFTP + $sftpHost = 'sftp.hidrive.ionos.com'; + $sftpUser = 'erp2019'; + $sftpPass = 'Z2CjX7kd2h'; + $remotePath = $backup['path_remote']; + $localPath = WRITEPATH . 'backups/' . $filename; + + // Conectar al SFTP + $sftp = new SFTP($sftpHost); + + if (!$sftp->login($sftpUser, $sftpPass)) { + return redirect()->to(route_to('backupsList'))->with('error', 'No se pudo autenticar en el servidor SFTP.'); + } + + // Descargar el archivo + $fileContents = $sftp->get($remotePath); + + if ($fileContents === false) { + return redirect()->to(route_to('backupsList'))->with('error', 'No se pudo descargar el archivo remoto.'); + } + + // Guardar localmente + if (write_file($localPath, $fileContents) === false) { + return redirect()->to(route_to('backupsList'))->with('error', 'No se pudo guardar el archivo localmente.'); + } + + // Actualizar la base de datos para marcar el archivo como local + $this->backupModel->update($backup['id'], [ + 'path_local' => $localPath, + ]); + + // Restaurar usando el método local + return $this->restoreLocal($filename); + } + + private function sendToSFTP($localPath, $remoteFilename) { diff --git a/ci4/app/Views/themes/vuexy/form/backups/backupList.php b/ci4/app/Views/themes/vuexy/form/backups/backupList.php index 247752f3..6984f811 100644 --- a/ci4/app/Views/themes/vuexy/form/backups/backupList.php +++ b/ci4/app/Views/themes/vuexy/form/backups/backupList.php @@ -35,12 +35,12 @@ - Restaurar - Restaurar Local + Eliminar local - Restaurar Remoto No disponible @@ -59,14 +59,12 @@ endSection() ?> section('additionalInlineJs') ?> - +}); endSection() ?> \ No newline at end of file From 82882198727b4e05411c18f5683adeaef8ec01c7 Mon Sep 17 00:00:00 2001 From: imnavajas Date: Thu, 12 Jun 2025 14:57:48 +0200 Subject: [PATCH 4/4] 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