Files
ave-cms/adminx/modules/Database/Controller.php
T
2026-07-27 12:58:44 +03:00

300 lines
10 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/*
|--------------------------------------------------------------------------------------
| AVE.cms
|--------------------------------------------------------------------------------------
| @package AVE.cms
| @file adminx/modules/Database/Controller.php
| @author AVE.cms <support@ave-cms.ru>
| @copyright 2007-2026 (c) AVE.cms
| @link https://ave-cms.ru
| @version 3.3
*/
namespace App\Adminx\Database;
defined('BASEPATH') || die('Direct access to this location is not allowed.');
use App\Common\AdminAssets;
use App\Common\AuditLog;
use App\Common\Auth;
use App\Common\Controller as BaseController;
use App\Common\ModuleManager;
use App\Common\Permission;
use App\Helpers\Request;
use App\Helpers\Response;
/**
* База данных: таблицы + обслуживание + резервные копии. Мутации — ajax по
* контракту success/error с CSRF и правом manage_database. Операции только над
* таблицами текущего префикса.
*/
class Controller extends BaseController
{
/** GET /database */
public function index(array $params = [])
{
AdminAssets::addStyle($this->base() . '/modules/Database/assets/database.css', 50);
AdminAssets::addScript($this->base() . '/modules/Database/assets/database.js', 50);
return $this->render('@database/index.twig', [
'tables' => Model::tables(),
'stats' => Model::stats(),
'backups' => Backup::all(),
'schema' => ModuleManager::coreSchemaStatus(),
'can_manage' => Permission::check('manage_database'),
]);
}
/** POST /database/maintenance — OPTIMIZE/REPAIR/ANALYZE (table='*' = все). */
public function maintenance(array $params = [])
{
if (($resp = $this->guard()) !== null) {
return $resp;
}
$op = Request::postStr('op');
$table = Request::postStr('table');
if ($table === '*') {
$ok = 0; $fail = 0;
foreach (Model::names() as $name) {
$r = Model::maintenance($op, $name);
$r['ok'] ? $ok++ : $fail++;
}
if ($ok === 0 && $fail === 0) {
return $this->error('Нет таблиц для обработки', [], 422);
}
return $this->success('Готово: ' . $ok . ' таблиц' . ($fail ? ', ошибок: ' . $fail : ''));
}
$result = Model::maintenance($op, $table);
if (!$result['ok']) {
return $this->error($result['table'] . ': ' . $result['message'], [], 422);
}
return $this->success($result['table'] . ': ' . $result['message'], [
'data' => ['table' => $result['table']],
]);
}
/** POST /database/backup — создать дамп. */
public function backupCreate(array $params = [])
{
if (($resp = $this->guard()) !== null) {
return $resp;
}
try {
$info = Backup::create();
} catch (\Throwable $e) {
return $this->error('Не удалось создать бэкап: ' . $e->getMessage(), [], 500);
}
return $this->success('Бэкап создан: ' . $info['name'] . ' (' . $info['tables'] . ' таблиц)', [
'redirect' => $this->base() . '/database?tab=backups',
]);
}
/** POST /database/backup/upload — загрузить и проверить внешний файл дампа. */
public function backupUpload(array $params = array())
{
if (($resp = $this->guard()) !== null) { return $resp; }
try {
$info = Backup::upload(isset($_FILES['backup']) ? (array) $_FILES['backup'] : array());
return $this->success('Копия загружена и проверена: ' . $info['name'], array(
'data' => $info,
'redirect' => $this->base() . '/database?tab=backups',
));
} catch (\Throwable $e) {
return $this->error('Не удалось загрузить копию: ' . $e->getMessage(), array(), 422);
}
}
/** GET /database/backup/download?file=... — скачать дамп. */
public function backupDownload(array $params = [])
{
if (!Permission::check('view_database')) {
Response::forbidden();
return null;
}
$path = Backup::path(Request::getStr('file'));
if ($path === null) {
Response::notFound('Файл не найден');
return null;
}
$contentType = substr($path, -3) === '.gz' ? 'application/gzip' : 'application/sql';
Response::download($path, basename($path), $contentType);
return null;
}
/** POST /database/backup/delete — удалить дамп. */
public function backupDelete(array $params = [])
{
if (($resp = $this->guard()) !== null) {
return $resp;
}
$file = Request::postStr('file');
if (!Backup::delete($file)) {
return $this->error('Не удалось удалить файл', [], 422);
}
return $this->success('Бэкап удалён');
}
/** POST /database/backup/inspect — проверить состав дампа до подтверждения. */
public function backupInspect(array $params = array())
{
if (($resp = $this->guard()) !== null) { return $resp; }
try {
$summary = BackupRestore::inspect(Request::postStr('file'));
return $this->success('Резервная копия проверена', array('data' => $summary));
} catch (\Throwable $e) {
return $this->error($e->getMessage(), array(), 422);
}
}
/** POST /database/backup/restore/start — подтвердить и подготовить потоковое восстановление. */
public function backupRestoreStart(array $params = array())
{
if (($resp = $this->guard()) !== null) { return $resp; }
$file = Request::postStr('file');
try {
$prepared = BackupRestore::prepare($file, Request::postStr('restore_token'), Auth::id());
return $this->success('Восстановление подготовлено', array('data' => $prepared));
} catch (\Throwable $e) {
return $this->error($e->getMessage(), array(), 422);
}
}
/** POST /database/backup/restore/run — NDJSON-поток фактического восстановления. */
public function backupRestoreRun(array $params = array())
{
if (($resp = $this->guard()) !== null) { return $resp; }
$jobId = Request::postStr('job_id');
$runToken = Request::postStr('run_token');
@set_time_limit(0);
ignore_user_abort(true);
header('Content-Type: application/x-ndjson; charset=UTF-8');
header('Cache-Control: no-cache, no-store, must-revalidate');
header('X-Accel-Buffering: no');
@ini_set('output_buffering', '0');
@ini_set('zlib.output_compression', '0');
while (ob_get_level() > 0) { @ob_end_flush(); }
ob_implicit_flush(true);
$emit = function (array $payload) {
echo json_encode(array('type' => 'progress', 'job' => $payload), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n";
@ob_flush();
flush();
};
try {
$result = BackupRestore::restore($jobId, $runToken, $emit);
try {
$user = Auth::user();
AuditLog::record('database.backup_restored', array(
'actor_id' => Auth::id(),
'actor_name' => $user && isset($user['name']) ? $user['name'] : '',
'target_type' => 'database_backup',
'target_id' => null,
'meta' => $result,
));
} catch (\Throwable $auditError) {
error_log('Database restore audit failed: ' . $auditError->getMessage());
}
echo json_encode(array(
'type' => 'complete',
'message' => 'База восстановлена из ' . $result['name'],
'result' => $result,
'redirect' => $this->base() . '/database?tab=backups',
), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n";
} catch (\Throwable $e) {
try {
AuditLog::record('database.backup_restore_failed', array(
'actor_id' => Auth::id(),
'target_type' => 'database_backup',
'target_id' => null,
'meta' => array('job_id' => $jobId, 'reason' => $e->getMessage()),
));
} catch (\Throwable $auditError) {
error_log('Database restore failure audit failed: ' . $auditError->getMessage());
}
echo json_encode(array('type' => 'error', 'message' => $e->getMessage()), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n";
}
return null;
}
/** POST /database/backup/restore/status — состояние на случай обрыва потока. */
public function backupRestoreStatus(array $params = array())
{
if (($resp = $this->guard()) !== null) { return $resp; }
try {
return $this->success('Состояние восстановления', array(
'data' => array('job' => BackupRestore::status(Request::postStr('job_id'), Auth::id())),
));
} catch (\Throwable $e) {
return $this->error($e->getMessage(), array(), 404);
}
}
/** POST /database/engines/innodb — завершить перевод рабочей схемы на InnoDB. */
public function convertToInnoDb(array $params = array())
{
if (($resp = $this->guard()) !== null) { return $resp; }
try {
$result = Model::convertToInnoDb();
$converted = count($result['converted']);
$failed = count($result['failed']);
if ($failed > 0) {
return $this->error('Переведено: ' . $converted . ', ошибок: ' . $failed, array('data' => $result), 422);
}
return $this->success($converted > 0 ? 'Таблицы переведены в InnoDB: ' . $converted : 'Таблиц MyISAM не осталось', array('data' => $result));
} catch (\Throwable $e) {
return $this->error('Не удалось изменить движок таблиц: ' . $e->getMessage(), array(), 422);
}
}
/** POST /database/schema/update — применить новые миграции ядровых разделов. */
public function updateSchema(array $params = array())
{
if (($resp = $this->guard()) !== null) {
return $resp;
}
try {
$results = ModuleManager::updateCoreSchema();
$applied = 0;
foreach ($results as $result) {
if (isset($result['status']) && $result['status'] === 'applied') {
$applied++;
}
}
$permissions = Permission::syncRegistryToDb();
return $this->success($applied > 0
? 'Применено миграций ядра: ' . $applied
: 'Все миграции ядра уже применены', array('data' => array('applied' => $applied, 'permissions' => $permissions)));
} catch (\Throwable $e) {
return $this->error('Не удалось применить миграции ядра: ' . $e->getMessage(), array(), 422);
}
}
// ------------------------------------------------------------------ //
protected function guard()
{
return $this->guardPermission('manage_database');
}
}