diff --git a/public/app.js b/public/app.js index 50026d4..5347fd1 100644 --- a/public/app.js +++ b/public/app.js @@ -2280,3 +2280,87 @@ async function performExport(mode, selectedColumns) { document.getElementById('btnManageColumns').addEventListener('click', () => { showColumnManager(); }); + +// ✅ Бэкап баз данных +document.getElementById('btnBackup').addEventListener('click', () => { + showBackupDialog(); +}); + +async function showBackupDialog() { + // Получаем список доступных БД из дерева + const databases = schemaTree.map(s => s.name); + + const overlay = document.createElement('div'); + overlay.className = 'columns-menu-overlay'; + overlay.onclick = () => overlay.remove(); + + const dialog = document.createElement('div'); + dialog.className = 'columns-menu'; + dialog.onclick = e => e.stopPropagation(); + + dialog.innerHTML = ` +

💾 Резервное копирование

+

Выберите базу данных для скачивания дампа:

+
+ +
+ ${databases.map(db => ` + + `).join('')} +
+
+ +
+ `; + + overlay.appendChild(dialog); + document.body.appendChild(overlay); + + dialog.querySelector('#backupClose').onclick = () => overlay.remove(); + + // Обработчики для кнопок бэкапа + dialog.querySelectorAll('.backup-btn').forEach(btn => { + btn.onmouseover = () => btn.style.opacity = '0.8'; + btn.onmouseout = () => btn.style.opacity = '1'; + btn.onclick = async () => { + const db = btn.dataset.db; + btn.disabled = true; + btn.textContent = '⏳ Создание дампа...'; + + try { + const url = db === '__all__' + ? '/api/backup/all' + : `/api/backup/database/${encodeURIComponent(db)}`; + + const response = await fetch(url); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || 'Ошибка создания бэкапа'); + } + + // Скачиваем файл + const blob = await response.blob(); + const filename = response.headers.get('Content-Disposition')?.match(/filename="(.+)"/)?.[1] + || (db === '__all__' ? 'backup_all.sql.gz' : `${db}.sql.gz`); + + const link = document.createElement('a'); + link.href = URL.createObjectURL(blob); + link.download = filename; + link.click(); + URL.revokeObjectURL(link.href); + + overlay.remove(); + alert(`✅ Бэкап успешно скачан: ${filename}`); + } catch (err) { + alert('❌ Ошибка: ' + err.message); + btn.disabled = false; + btn.textContent = db === '__all__' ? '📦 Скачать ВСЕ базы данных' : `🗄️ ${db}`; + } + }; + }); +} diff --git a/public/index.html b/public/index.html index 245b335..3b0e569 100644 --- a/public/index.html +++ b/public/index.html @@ -321,6 +321,8 @@ +
+
diff --git a/public/index.php b/public/index.php index 3cfcc74..ba5c11a 100644 --- a/public/index.php +++ b/public/index.php @@ -356,4 +356,44 @@ $app->post('/api/table/delete-batch', function (Request $request, Response $resp }); +// === BACKUP API === + +// Скачать дамп всех баз данных +$app->get('/api/backup/all', function (Request $request, Response $response) use ($container) { + try { + $pdo = $container->get('db'); + $backup = new \App\BackupService($_SESSION['db_user'], $_SESSION['db_pass']); + $data = $backup->dumpAllDatabases($pdo); + $filename = 'backup_all_' . date('Y-m-d_H-i-s') . '.sql.gz'; + + $response->getBody()->write($data); + return $response + ->withHeader('Content-Type', 'application/gzip') + ->withHeader('Content-Disposition', 'attachment; filename="' . $filename . '"') + ->withHeader('Content-Length', strlen($data)); + } catch (\Exception $e) { + $response->getBody()->write(json_encode(['error' => $e->getMessage()])); + return $response->withHeader('Content-Type', 'application/json')->withStatus(500); + } +}); + +// Скачать дамп конкретной базы +$app->get('/api/backup/database/{name}', function (Request $request, Response $response, array $args) { + try { + $database = $args['name']; + $backup = new \App\BackupService($_SESSION['db_user'], $_SESSION['db_pass']); + $data = $backup->dumpDatabase($database); + $filename = $database . '_' . date('Y-m-d_H-i-s') . '.sql.gz'; + + $response->getBody()->write($data); + return $response + ->withHeader('Content-Type', 'application/gzip') + ->withHeader('Content-Disposition', 'attachment; filename="' . $filename . '"') + ->withHeader('Content-Length', strlen($data)); + } catch (\Exception $e) { + $response->getBody()->write(json_encode(['error' => $e->getMessage()])); + return $response->withHeader('Content-Type', 'application/json')->withStatus(500); + } +}); + $app->run(); diff --git a/src/BackupService.php b/src/BackupService.php new file mode 100644 index 0000000..39e1a3f --- /dev/null +++ b/src/BackupService.php @@ -0,0 +1,78 @@ +query($sql)->fetchAll(\PDO::FETCH_COLUMN); + } + + /** + * Создать дамп и вернуть содержимое (gzip сжатое) + */ + public function dumpDatabase(string $database): string + { + $cmd = sprintf( + 'mysqldump --host=%s --port=%s --user=%s --password=%s --single-transaction --routines --triggers %s 2>/dev/null | gzip', + escapeshellarg($this->host), + escapeshellarg($this->port), + escapeshellarg($this->user), + escapeshellarg($this->pass), + escapeshellarg($database) + ); + + $output = shell_exec($cmd); + + if ($output === null || $output === '') { + throw new \RuntimeException("Failed to create dump for database: $database"); + } + + return $output; + } + + /** + * Создать дамп всех баз данных (один файл) + */ + public function dumpAllDatabases(\PDO $pdo): string + { + $databases = $this->getAvailableDatabases($pdo); + + if (empty($databases)) { + throw new \RuntimeException("No databases available for backup"); + } + + $dbList = implode(' ', array_map('escapeshellarg', $databases)); + + $cmd = sprintf( + 'mysqldump --host=%s --port=%s --user=%s --password=%s --single-transaction --routines --triggers --databases %s 2>/dev/null | gzip', + escapeshellarg($this->host), + escapeshellarg($this->port), + escapeshellarg($this->user), + escapeshellarg($this->pass), + $dbList + ); + + $output = shell_exec($cmd); + + if ($output === null || $output === '') { + throw new \RuntimeException("Failed to create dump"); + } + + return $output; + } +}