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 @@
+
+
Выбрано: 0
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;
+ }
+}