backupDir = storage_path('app/backups'); } /** * Muestra el panel con el listado de copias de seguridad. */ public function index() { // Asegurarse de que el directorio exista if (!File::exists($this->backupDir)) { File::makeDirectory($this->backupDir, 0755, true); } $files = File::files($this->backupDir); $backups = []; $totalSize = 0; foreach ($files as $file) { if ($file->getExtension() === 'zip' && strpos($file->getFilename(), 'backup-') === 0) { $size = $file->getSize(); $totalSize += $size; $backups[] = [ 'filename' => $file->getFilename(), 'size' => $this->formatBytes($size), 'raw_size' => $size, 'created_at' => \Carbon\Carbon::createFromTimestamp($file->getMTime())->format('d/m/Y H:i:s'), 'mtime' => $file->getMTime(), ]; } } // Ordenar de más nuevo a más viejo usort($backups, function ($a, $b) { return $b['mtime'] <=> $a['mtime']; }); // Estadísticas de disco $freeDiskSpace = disk_free_space($this->backupDir); $totalDiskSpace = disk_total_space($this->backupDir); $diskStats = [ 'total_backups_size' => $this->formatBytes($totalSize), 'free_space' => $this->formatBytes($freeDiskSpace), 'total_space' => $this->formatBytes($totalDiskSpace), 'free_percentage' => round(($freeDiskSpace / $totalDiskSpace) * 100, 1), ]; return view('admin.backups.index', compact('backups', 'diskStats')); } public function create() { try { $exitCode = Artisan::call('db:backup'); $output = Artisan::output(); if ($exitCode === 0) { return redirect()->route('admin.backups.index') ->with('success', 'Copia de seguridad generada correctamente.'); } else { \Illuminate\Support\Facades\Log::error("Artisan db:backup failed with exit code {$exitCode}. Output: " . $output); return redirect()->route('admin.backups.index') ->with('error', 'Ocurrió un error al generar la copia de seguridad. Detalles: ' . trim($output)); } } catch (\Exception $e) { \Illuminate\Support\Facades\Log::error("Artisan db:backup threw exception: " . $e->getMessage()); return redirect()->route('admin.backups.index') ->with('error', 'Error: ' . $e->getMessage()); } } /** * Descargar una copia de seguridad. */ public function download($filename) { // Validación de seguridad para evitar directory traversal if (strpos($filename, '..') !== false || strpos($filename, '/') !== false || strpos($filename, '\\') !== false) { abort(404, 'Nombre de archivo no válido.'); } $path = $this->backupDir . '/' . $filename; if (!File::exists($path)) { abort(404, 'El archivo no existe.'); } return response()->download($path); } /** * Eliminar una copia de seguridad. */ public function destroy($filename) { // Validación de seguridad para evitar directory traversal if (strpos($filename, '..') !== false || strpos($filename, '/') !== false || strpos($filename, '\\') !== false) { abort(400, 'Nombre de archivo no válido.'); } $path = $this->backupDir . '/' . $filename; if (File::exists($path)) { File::delete($path); return redirect()->route('admin.backups.index') ->with('success', 'Copia de seguridad eliminada correctamente.'); } return redirect()->route('admin.backups.index') ->with('error', 'No se encontró el archivo a eliminar.'); } /** * Subir un archivo de copia de seguridad (.zip). */ public function upload(Request $request) { $request->validate([ 'backup_file' => 'required|file|mimes:zip|max:50000', // 50MB max ]); $file = $request->file('backup_file'); $filename = $file->getClientOriginalName(); // Validación de seguridad para el nombre del archivo if (strpos($filename, 'backup-') !== 0 || $file->getClientOriginalExtension() !== 'zip') { return redirect()->route('admin.backups.index') ->with('error', 'El archivo debe ser un .zip válido de respaldo (su nombre debe empezar con "backup-").'); } // Asegurarse de que el directorio exista if (!File::exists($this->backupDir)) { File::makeDirectory($this->backupDir, 0755, true); } $file->move($this->backupDir, $filename); return redirect()->route('admin.backups.index') ->with('success', 'Copia de seguridad subida correctamente.'); } /** * Restaurar la base de datos a partir de una copia de seguridad. */ public function restore($filename) { // Validación de seguridad para evitar directory traversal if (strpos($filename, '..') !== false || strpos($filename, '/') !== false || strpos($filename, '\\') !== false) { abort(400, 'Nombre de archivo no válido.'); } $zipPath = $this->backupDir . '/' . $filename; if (!File::exists($zipPath)) { return redirect()->route('admin.backups.index')->with('error', 'El archivo no existe.'); } // Extraer el archivo SQL del ZIP $zip = new \ZipArchive(); if ($zip->open($zipPath) === true) { // Buscamos el primer archivo SQL dentro del zip $sqlFilename = null; for ($i = 0; $i < $zip->numFiles; $i++) { $stat = $zip->statIndex($i); if (pathinfo($stat['name'], PATHINFO_EXTENSION) === 'sql') { $sqlFilename = $stat['name']; break; } } if (!$sqlFilename) { $zip->close(); return redirect()->route('admin.backups.index')->with('error', 'No se encontró ningún archivo SQL en el archivo comprimido.'); } // Extraer el SQL a un directorio temporal de backups $zip->extractTo($this->backupDir, $sqlFilename); $zip->close(); $sqlPath = $this->backupDir . '/' . $sqlFilename; // Datos de conexión $dbConfig = config('database.connections.mysql'); $host = $dbConfig['host'] ?? '127.0.0.1'; $port = $dbConfig['port'] ?? '3306'; $database = $dbConfig['database'] ?? 'lauck'; $username = $dbConfig['username'] ?? 'root'; $password = $dbConfig['password'] ?? ''; // Ejecutable de mysql $mysqlPath = 'mysql'; if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { $xamppPath = 'C:\\xampp\\mysql\\bin\\mysql.exe'; if (File::exists($xamppPath)) { $mysqlPath = $xamppPath; } } // Ejecutar importación usando Process y redirección de entrada de archivo $command = [ $mysqlPath, "--host={$host}", "--port={$port}", "--user={$username}", $database ]; $env = [ 'SystemRoot' => getenv('SystemRoot') ?: 'C:\\Windows', 'windir' => getenv('windir') ?: 'C:\\Windows', 'PATH' => getenv('PATH'), ]; if ($password !== '') { $env['MYSQL_PWD'] = $password; } $process = new \Symfony\Component\Process\Process($command, null, $env); // Pasar el contenido del archivo SQL a la entrada estándar (stdin) $process->setInput(File::get($sqlPath)); $process->setTimeout(300); try { $process->run(); // Limpiar archivo SQL temporal File::delete($sqlPath); if (!$process->isSuccessful()) { throw new \Exception(trim($process->getErrorOutput()) ?: 'Error desconocido al restaurar el respaldo.'); } return redirect()->route('admin.backups.index') ->with('success', 'Base de datos restaurada correctamente a partir del respaldo: ' . $filename); } catch (\Exception $e) { if (File::exists($sqlPath)) { File::delete($sqlPath); } \Illuminate\Support\Facades\Log::error("Backup restore failed: " . $e->getMessage()); return redirect()->route('admin.backups.index') ->with('error', 'Error al restaurar base de datos: ' . $e->getMessage()); } } else { return redirect()->route('admin.backups.index')->with('error', 'No se pudo abrir el archivo ZIP.'); } } /** * Formatear bytes a tamaño legible. */ private function formatBytes($bytes, $precision = 2) { $units = ['B', 'KB', 'MB', 'GB', 'TB']; $bytes = max($bytes, 0); $pow = floor(($bytes ? log($bytes) : 0) / log(1024)); $pow = min($pow, count($units) - 1); $bytes /= pow(1024, $pow); return round($bytes, $precision) . ' ' . $units[$pow]; } }