backupModel = new BackupModel(); } public function index() { helper('filesystem'); $entorno = getenv('SK_ENVIRONMENT'); $backups = []; if ($entorno === 'development') { // Leer archivos directamente del disco en entorno de desarrollo $backups = []; // === 1. Backups locales === $localDir = WRITEPATH . 'backups/'; $localFiles = get_filenames($localDir); foreach ($localFiles as $file) { if (!str_ends_with($file, '.zip') || !str_starts_with($file, 'backup_dev_')) { continue; } $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, ]; } // === 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'); $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" . escapeshellarg($username) . " -p'" . addslashes($password) . "' {$database} > {$sqlPath}"; system($command, $retval); if ($retval !== 0) { throw new \RuntimeException("Error al crear el backup."); } // 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."); } // 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 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; if (!file_exists($path)) { throw new \CodeIgniter\Exceptions\PageNotFoundException("Backup no encontrado."); } $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(); $sqlFiles = glob($extractPath . '*.sql'); if (count($sqlFiles) === 0) { throw new \RuntimeException("No se encontró ningún archivo .sql en el ZIP"); } $sqlFile = $sqlFiles[0]; // === Verificar que el archivo SQL existe y tiene contenido if (!file_exists($sqlFile)) { throw new \RuntimeException("Archivo SQL no encontrado."); } if (filesize($sqlFile) === 0) { throw new \RuntimeException("El archivo SQL está vacío."); } // === Configuración de base de datos $dbConfig = config('Database')->default; $host = escapeshellarg($dbConfig['hostname']); $username = escapeshellarg($dbConfig['username']); $password = escapeshellarg($dbConfig['password']); $database = escapeshellarg($dbConfig['database']); // === Construcción del comando con stderr redirigido $cmd = "mysql -h $host -u $username -p$password $database -e \"source $sqlFile\" 2>&1"; // === Ejecutar y capturar la salida exec($cmd, $output, $retval); // === Verificar resultado if ($retval !== 0) { throw new \RuntimeException("Error al restaurar la base de datos:\n" . implode("\n", $output)); } // === Limpieza helper('filesystem'); delete_files($extractPath, true); // elimina contenido rmdir($extractPath); // elimina el directorio 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 restoreRemote($filename) { helper('filesystem'); $entorno = getenv('SK_ENVIRONMENT'); if ($entorno === 'development') { // Construir ruta remota directamente $remotePath = '/users/erp2019/backups_erp/' . $filename; $localPath = WRITEPATH . 'backups/' . $filename; $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 autenticar en el servidor SFTP.'); } $fileContents = $sftp->get($remotePath); if ($fileContents === false) { return redirect()->to(route_to('backupsList'))->with('error', 'No se pudo descargar el archivo remoto.'); } if (write_file($localPath, $fileContents) === false) { return redirect()->to(route_to('backupsList'))->with('error', 'No se pudo guardar el archivo localmente.'); } // Restaurar directamente return $this->restoreLocal($filename); } // Producción: flujo normal con 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.'); } $remotePath = $backup['path_remote']; $localPath = WRITEPATH . 'backups/' . $filename; $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 autenticar en el servidor SFTP.'); } $fileContents = $sftp->get($remotePath); if ($fileContents === false) { return redirect()->to(route_to('backupsList'))->with('error', 'No se pudo descargar el archivo remoto.'); } if (write_file($localPath, $fileContents) === false) { return redirect()->to(route_to('backupsList'))->with('error', 'No se pudo guardar el archivo localmente.'); } $this->backupModel->update($backup['id'], ['path_local' => $localPath]); return $this->restoreLocal($filename); } private function sendToSFTP($localPath, $remoteFilename) { $sftpHost = getenv('HIDRIVE_HOST'); $sftpUser = getenv('HIDRIVE_USER'); $sftpPass = getenv('HIDRIVE_PASS'); $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."); } } }