From 6967a61d93198d860baf2630245f9f72ab60dca7 Mon Sep 17 00:00:00 2001 From: imnavajas Date: Mon, 9 Jun 2025 15:18:12 +0200 Subject: [PATCH] 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