openvk/CLI/UpgradeCommand.php

356 lines
11 KiB
PHP
Raw Permalink Normal View History

2025-02-22 20:34:00 +03:00
<?php
declare(strict_types=1);
namespace openvk\CLI;
use Nette\Database\Connection;
use Chandler\Database\DatabaseConnection;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
2025-02-22 21:51:43 +03:00
class UpgradeCommand extends Command
{
2025-02-22 20:34:00 +03:00
protected static $defaultName = "upgrade";
2025-02-22 21:51:43 +03:00
2025-02-22 20:34:00 +03:00
private Connection $db;
private Connection $eventDb;
2025-02-22 21:51:43 +03:00
2025-02-22 20:34:00 +03:00
private array $chandlerTables = [
"CHANDLERACLPERMISSIONALIASES",
"CHANDLERACLGROUPSPERMISSIONS",
"CHANDLERACLUSERSPERMISSIONS",
"CHANDLERACLRELATIONS",
"CHANDLERGROUPS",
"CHANDLERTOKENS",
2025-02-22 21:51:43 +03:00
"CHANDLERUSERS",
2025-02-22 20:34:00 +03:00
];
2025-02-22 21:51:43 +03:00
2025-02-22 20:34:00 +03:00
public function __construct()
{
$this->db = DatabaseConnection::i()->getConnection();
$this->eventDb = eventdb()->getConnection();
2025-02-22 21:51:43 +03:00
2025-02-22 20:34:00 +03:00
parent::__construct();
}
2025-02-22 21:51:43 +03:00
2025-02-22 20:34:00 +03:00
protected function configure(): void
{
$this->setDescription("Upgrade OpenVK installation")
->setHelp("This command upgrades database schema after OpenVK was updated")
2025-02-22 21:51:43 +03:00
->addOption(
"quick",
"Q",
InputOption::VALUE_NEGATABLE,
"Don't display warning before migrating database",
false
)
->addOption(
"repair",
"R",
InputOption::VALUE_NEGATABLE,
"Attempt to repair database schema if tables are missing",
false
)
->addOption(
"oneshot",
"O",
InputOption::VALUE_NONE,
"Only execute one operation"
)
->addArgument(
"chandler",
InputArgument::OPTIONAL,
"Location of Chandler installation"
);
2025-02-22 20:34:00 +03:00
}
2025-02-22 21:51:43 +03:00
protected function checkDatabaseReadiness(bool &$chandlerOk, bool &$ovkOk, bool &$eventOk, bool &$migrationsOk): void
2025-02-22 20:34:00 +03:00
{
$tables = $this->db->query("SHOW TABLES")->fetchAll();
2025-02-22 21:51:43 +03:00
$tables = array_map(fn($x) => strtoupper($x->offsetGet(0)), $tables);
2025-02-22 20:34:00 +03:00
$missingTables = array_diff($this->chandlerTables, $tables);
2025-02-22 21:51:43 +03:00
if (sizeof($missingTables) == 0) {
2025-02-22 20:34:00 +03:00
$chandlerOk = true;
2025-02-22 21:51:43 +03:00
} elseif (sizeof($missingTables) == sizeof($this->chandlerTables)) {
2025-02-22 20:34:00 +03:00
$chandlerOk = null;
2025-02-22 21:51:43 +03:00
} else {
2025-02-22 20:34:00 +03:00
$chandlerOk = false;
2025-02-22 21:51:43 +03:00
}
if (is_null($this->eventDb)) {
$eventOk = false;
2025-02-22 21:51:43 +03:00
} elseif (is_null($this->eventDb->query("SHOW TABLES LIKE \"notifications\"")->fetch())) {
$eventOk = null;
} else {
$eventOk = true;
}
2025-02-22 21:51:43 +03:00
2025-02-22 20:34:00 +03:00
$ovkOk = in_array("PROFILES", $tables);
$migrationsOk = in_array("OVK_UPGRADE_HISTORY", $tables);
}
2025-02-22 21:51:43 +03:00
protected function executeSqlScript(
int $errCode,
string $script,
SymfonyStyle $io,
bool $transaction = false,
bool $eventDb = false
): int {
$pdo = ($eventDb ? $this->eventDb : $this->db)->getPdo();
2025-02-22 21:51:43 +03:00
2025-02-22 20:34:00 +03:00
$res = false;
try {
2025-02-22 21:51:43 +03:00
if ($transaction) {
2025-02-22 20:34:00 +03:00
$res = $pdo->beginTransaction();
2025-02-22 21:51:43 +03:00
}
2025-02-22 20:34:00 +03:00
$res = $pdo->exec($script);
2025-02-22 21:51:43 +03:00
if ($transaction) {
2025-02-22 20:34:00 +03:00
$res = $pdo->commit();
2025-02-22 21:51:43 +03:00
}
} catch (\PDOException $e) {
}
if ($res === false) {
2025-02-22 20:34:00 +03:00
goto error;
2025-02-22 21:51:43 +03:00
}
2025-02-22 20:34:00 +03:00
return 0;
2025-02-22 21:51:43 +03:00
2025-02-22 20:34:00 +03:00
error:
$io->getErrorStyle()->error([
"Failed to execute SQL statement:",
2025-02-22 21:51:43 +03:00
implode("\t", $pdo->errorInfo()),
2025-02-22 20:34:00 +03:00
]);
2025-02-22 21:51:43 +03:00
2025-02-22 20:34:00 +03:00
return $errCode;
}
2025-02-22 21:51:43 +03:00
protected function getNextLevel(bool $eventDb = false): int
2025-02-22 20:34:00 +03:00
{
$tbl = $eventDb ? "ovk_events_upgrade_history" : "ovk_upgrade_history";
$record = $this->db->query("SELECT level FROM $tbl ORDER BY level DESC LIMIT 1");
2025-02-22 21:51:43 +03:00
if (!$record->getRowCount()) {
2025-02-22 20:34:00 +03:00
return 0;
2025-02-22 21:51:43 +03:00
}
2025-02-22 20:34:00 +03:00
return $record->fetchField() + 1;
}
2025-02-22 21:51:43 +03:00
protected function getMigrationFiles(bool $eventDb = false): array
2025-02-22 20:34:00 +03:00
{
$files = [];
$root = dirname(__DIR__ . "/../install/init-static-db.sql");
$dir = $eventDb ? "sqls/eventdb" : "sqls";
2025-02-22 21:51:43 +03:00
foreach (glob("$root/$dir/*.sql") as $file) {
2025-02-22 20:34:00 +03:00
$files[(int) basename($file)] = basename($file);
2025-02-22 21:51:43 +03:00
}
2025-02-22 20:34:00 +03:00
ksort($files);
2025-02-22 21:51:43 +03:00
2025-02-22 20:34:00 +03:00
return $files;
}
2025-02-22 21:51:43 +03:00
2025-02-22 20:34:00 +03:00
protected function installChandler(InputInterface $input, SymfonyStyle $io, bool $drop = false): int
{
$chandlerLocation = $input->getArgument("chandler") ?? (__DIR__ . "/../../../../");
$chandlerConfigLocation = "$chandlerLocation/chandler.yml";
2025-02-22 21:51:43 +03:00
2025-02-22 20:34:00 +03:00
if (!file_exists($chandlerConfigLocation)) {
$err = ["Could not find chandler location. Perhaps your config is too unique?"];
2025-02-22 21:51:43 +03:00
if (!$input->getOption("chandler")) {
2025-02-22 20:34:00 +03:00
$err[] = "Specify absolute path to your chandler installation using the --chandler option.";
2025-02-22 21:51:43 +03:00
}
2025-02-22 20:34:00 +03:00
$io->getErrorStyle()->error($err);
2025-02-22 21:51:43 +03:00
2025-02-22 20:34:00 +03:00
return 21;
}
2025-02-22 21:51:43 +03:00
2025-02-22 20:34:00 +03:00
if ($drop) {
$bar = new ProgressBar($io, sizeof($this->chandlerTables));
$io->writeln("Dropping chandler tables...");
2025-02-22 21:51:43 +03:00
foreach ($bar->iterate($this->chandlerTables) as $table) {
2025-02-22 20:34:00 +03:00
$this->db->query("DROP TABLE IF EXISTS $table;");
2025-02-22 21:51:43 +03:00
}
2025-02-22 20:34:00 +03:00
$io->newLine();
}
2025-02-22 21:51:43 +03:00
2025-02-22 20:34:00 +03:00
$installFile = file_get_contents("$chandlerLocation/install/init-db.sql");
2025-02-22 21:51:43 +03:00
2025-02-22 20:34:00 +03:00
return $this->executeSqlScript(22, $installFile, $io);
}
2025-02-22 21:51:43 +03:00
2025-02-22 20:34:00 +03:00
protected function initSchema(SymfonyStyle $io): int
{
$installFile = file_get_contents(__DIR__ . "/../install/init-static-db.sql");
2025-02-22 21:51:43 +03:00
2025-02-22 20:34:00 +03:00
return $this->executeSqlScript(31, $installFile, $io);
}
2025-02-22 21:51:43 +03:00
protected function initEventSchema(SymfonyStyle $io): int
{
$installFile = file_get_contents(__DIR__ . "/../install/init-event-db.sql");
2025-02-22 21:51:43 +03:00
return $this->executeSqlScript(31, $installFile, $io, true, true);
}
2025-02-22 21:51:43 +03:00
2025-02-22 20:34:00 +03:00
protected function initUpgradeLog(SymfonyStyle $io): int
{
$installFile = file_get_contents(__DIR__ . "/../install/init-migration-table.sql");
2025-02-22 21:51:43 +03:00
return $this->executeSqlScript(31, $installFile, $io, true);
2025-02-22 20:34:00 +03:00
}
2025-02-22 21:51:43 +03:00
protected function runMigrations(SymfonyStyle $io, bool $eventDb, bool $oneshot): int
2025-02-22 20:34:00 +03:00
{
$dir = $eventDb ? "sqls/eventdb" : "sqls";
$tbl = $eventDb ? "ovk_events_upgrade_history" : "ovk_upgrade_history";
$nextLevel = $this->getNextLevel($eventDb);
2025-02-22 21:51:43 +03:00
$migrations = array_filter(
$this->getMigrationFiles($eventDb),
fn($id) => $id >= $nextLevel,
ARRAY_FILTER_USE_KEY
);
if (!sizeof($migrations)) {
2025-02-22 20:34:00 +03:00
return 24;
2025-02-22 21:51:43 +03:00
}
2025-02-22 20:34:00 +03:00
$uname = addslashes(`whoami`);
$bar = new ProgressBar($io, sizeof($migrations));
$bar->setFormat("very_verbose");
2025-02-22 21:51:43 +03:00
2025-02-22 20:34:00 +03:00
foreach ($bar->iterate($migrations) as $num => $migration) {
$script = file_get_contents(__DIR__ . "/../install/$dir/$migration");
$res = $this->executeSqlScript(100 + $num, $script, $io, true, $eventDb);
2025-02-22 20:34:00 +03:00
if ($res != 0) {
$io->getErrorStyle()->error("Error while executing migration №$num");
2025-02-22 21:51:43 +03:00
2025-02-22 20:34:00 +03:00
return $res;
}
2025-02-22 21:51:43 +03:00
2025-02-22 20:34:00 +03:00
$t = time();
$this->db->query("INSERT INTO $tbl VALUES ($num, $t, \"$uname\");");
2025-02-22 21:51:43 +03:00
if ($oneshot) {
return 5;
2025-02-22 21:51:43 +03:00
}
2025-02-22 20:34:00 +03:00
}
2025-02-22 21:51:43 +03:00
$io->newLine();
2025-02-22 21:51:43 +03:00
2025-02-22 20:34:00 +03:00
return 0;
}
2025-02-22 21:51:43 +03:00
2025-02-22 20:34:00 +03:00
protected function execute(InputInterface $input, OutputInterface $output): int
{
$oneShotMode = $input->getOption("oneshot");
2025-02-22 20:34:00 +03:00
$io = new SymfonyStyle($input, $output);
2025-02-22 21:51:43 +03:00
2025-02-22 20:34:00 +03:00
if (!$input->getOption("quick")) {
$io->writeln("Do full backup of the database before executing this command!");
$io->writeln("Command will resume execution after 5 seconds.");
$io->writeln("You can skip this warning with --quick option.");
sleep(5);
}
2025-02-22 21:51:43 +03:00
2025-02-22 20:34:00 +03:00
$migrationsOk = false;
$chandlerOk = false;
$eventOk = false;
2025-02-22 20:34:00 +03:00
$ovkOk = false;
2025-02-22 21:51:43 +03:00
$this->checkDatabaseReadiness($chandlerOk, $ovkOk, $eventOk, $migrationsOk);
2025-02-22 21:51:43 +03:00
$res = -1;
2025-02-22 20:34:00 +03:00
if ($chandlerOk === null) {
$io->writeln("Chandler schema not detected, attempting to install...");
2025-02-22 21:51:43 +03:00
2025-02-22 20:34:00 +03:00
$res = $this->installChandler($input, $io);
2025-02-22 21:51:43 +03:00
} elseif ($chandlerOk === false) {
2025-02-22 20:34:00 +03:00
if ($input->getOption("repair")) {
$io->warning("Chandler schema detected but is broken, attempting to repair...");
2025-02-22 21:51:43 +03:00
2025-02-22 20:34:00 +03:00
$res = $this->installChandler($input, $io, true);
} else {
$io->writeln("Chandler schema detected but is broken");
$io->writeln("Run command with --repair to repair (PERMISSIONS WILL BE LOST)");
2025-02-22 21:51:43 +03:00
2025-02-22 20:34:00 +03:00
return 1;
}
}
2025-02-22 21:51:43 +03:00
if ($res > 0) {
2025-02-22 20:34:00 +03:00
return $res;
2025-02-22 21:51:43 +03:00
} elseif ($res == 0 && $oneShotMode) {
return 5;
2025-02-22 21:51:43 +03:00
}
2025-02-22 20:34:00 +03:00
if (!$ovkOk) {
$io->writeln("Initializing OpenVK schema...");
$res = $this->initSchema($io);
2025-02-22 21:51:43 +03:00
if ($res > 0) {
2025-02-22 20:34:00 +03:00
return $res;
2025-02-22 21:51:43 +03:00
} elseif ($oneShotMode) {
return 5;
2025-02-22 21:51:43 +03:00
}
2025-02-22 20:34:00 +03:00
}
2025-02-22 21:51:43 +03:00
2025-02-22 20:34:00 +03:00
if (!$migrationsOk) {
$io->writeln("Initializing upgrade log...");
$res = $this->initUpgradeLog($io);
2025-02-22 21:51:43 +03:00
if ($res > 0) {
2025-02-22 20:34:00 +03:00
return $res;
2025-02-22 21:51:43 +03:00
} elseif ($oneShotMode) {
return 5;
2025-02-22 21:51:43 +03:00
}
2025-02-22 20:34:00 +03:00
}
2025-02-22 21:51:43 +03:00
if ($eventOk !== false) {
if ($eventOk === null) {
$io->writeln("Initializing event database...");
$res = $this->initEventSchema($io);
2025-02-22 21:51:43 +03:00
if ($res > 0) {
return $res;
2025-02-22 21:51:43 +03:00
} elseif ($oneShotMode) {
return 5;
2025-02-22 21:51:43 +03:00
}
}
2025-02-22 21:51:43 +03:00
$io->writeln("Upgrading event database...");
$res = $this->runMigrations($io, true, $oneShotMode);
2025-02-22 21:51:43 +03:00
if ($res == 24) {
$output->writeln("Event database already up to date.");
2025-02-22 21:51:43 +03:00
} elseif ($res > 0) {
return $res;
2025-02-22 21:51:43 +03:00
}
}
2025-02-22 21:51:43 +03:00
2025-02-22 20:34:00 +03:00
$io->writeln("Upgrading database...");
$res = $this->runMigrations($io, false, $oneShotMode);
2025-02-22 21:51:43 +03:00
2025-02-22 20:34:00 +03:00
if (!$res) {
$io->success("Database has been upgraded!");
2025-02-22 21:51:43 +03:00
2025-02-22 20:34:00 +03:00
return 0;
2025-02-22 21:51:43 +03:00
} elseif ($res != 24) {
2025-02-22 20:34:00 +03:00
return $res;
}
2025-02-22 21:51:43 +03:00
$io->writeln("Database up to date. Nothing left to do.");
2025-02-22 21:51:43 +03:00
2025-02-22 20:34:00 +03:00
return 0;
}
2025-02-22 21:51:43 +03:00
}