diff --git a/CLI/FetchToncoinTransactions.php b/CLI/FetchToncoinTransactions.php index 76af7a08..171ea091 100755 --- a/CLI/FetchToncoinTransactions.php +++ b/CLI/FetchToncoinTransactions.php @@ -24,8 +24,9 @@ class FetchToncoinTransactions extends Command public function __construct() { $ctx = DatabaseConnection::i()->getContext(); - if (in_array("cryptotransactions", $ctx->getStructure()->getTables())) + if (in_array("cryptotransactions", $ctx->getStructure()->getTables())) { $this->transactions = $ctx->table("cryptotransactions"); + } parent::__construct(); } diff --git a/CLI/RebuildImagesCommand.php b/CLI/RebuildImagesCommand.php index a75931dd..6340743c 100644 --- a/CLI/RebuildImagesCommand.php +++ b/CLI/RebuildImagesCommand.php @@ -21,8 +21,9 @@ class RebuildImagesCommand extends Command public function __construct() { $ctx = DatabaseConnection::i()->getContext(); - if (in_array("photos", $ctx->getStructure()->getTables())) + if (in_array("photos", $ctx->getStructure()->getTables())) { $this->images = $ctx->table("photos"); + } parent::__construct(); } diff --git a/CLI/UpgradeCommand.php b/CLI/UpgradeCommand.php index afe861ad..92abf161 100644 --- a/CLI/UpgradeCommand.php +++ b/CLI/UpgradeCommand.php @@ -14,11 +14,13 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; -class UpgradeCommand extends Command { +class UpgradeCommand extends Command +{ protected static $defaultName = "upgrade"; - + private Connection $db; - + private Connection $eventDb; + private array $chandlerTables = [ "CHANDLERACLPERMISSIONALIASES", "CHANDLERACLGROUPSPERMISSIONS", @@ -26,246 +28,337 @@ class UpgradeCommand extends Command { "CHANDLERACLRELATIONS", "CHANDLERGROUPS", "CHANDLERTOKENS", - "CHANDLERUSERS" + "CHANDLERUSERS", ]; - + public function __construct() { $this->db = DatabaseConnection::i()->getConnection(); - + $this->eventDb = eventdb()->getConnection(); + parent::__construct(); } - + protected function configure(): void { $this->setDescription("Upgrade OpenVK installation") ->setHelp("This command upgrades database schema after OpenVK was updated") - ->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"); + ->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" + ); } - - protected function checkDatabaseReadiness(bool &$chandlerOk, bool &$ovkOk, bool &$migrationsOk): void + + protected function checkDatabaseReadiness(bool &$chandlerOk, bool &$ovkOk, bool &$eventOk, bool &$migrationsOk): void { $tables = $this->db->query("SHOW TABLES")->fetchAll(); - $tables = array_map(fn ($x) => strtoupper($x->offsetGet(0)), $tables); - + $tables = array_map(fn($x) => strtoupper($x->offsetGet(0)), $tables); + $missingTables = array_diff($this->chandlerTables, $tables); - if (sizeof($missingTables) == 0) + if (sizeof($missingTables) == 0) { $chandlerOk = true; - else if (sizeof($missingTables) == sizeof($this->chandlerTables)) + } elseif (sizeof($missingTables) == sizeof($this->chandlerTables)) { $chandlerOk = null; - else + } else { $chandlerOk = false; - + } + + if (is_null($this->eventDb)) { + $eventOk = false; + } elseif (is_null($this->eventDb->query("SHOW TABLES LIKE \"notifications\"")->fetch())) { + $eventOk = null; + } else { + $eventOk = true; + } + $ovkOk = in_array("PROFILES", $tables); $migrationsOk = in_array("OVK_UPGRADE_HISTORY", $tables); } - - protected function executeSqlScript(int $errCode, string $script, SymfonyStyle $io, bool $transaction = false): int - { - $pdo = $this->db->getPdo(); - + + protected function executeSqlScript( + int $errCode, + string $script, + SymfonyStyle $io, + bool $transaction = false, + bool $eventDb = false + ): int { + $pdo = ($eventDb ? $this->eventDb : $this->db)->getPdo(); + $res = false; try { - if ($transaction) + if ($transaction) { $res = $pdo->beginTransaction(); - + } + $res = $pdo->exec($script); - - if ($transaction) + + if ($transaction) { $res = $pdo->commit(); - } catch (\PDOException $e) {} - - if ($res === false) + } + } catch (\PDOException $e) { + } + + if ($res === false) { goto error; - + } + return 0; - + error: $io->getErrorStyle()->error([ "Failed to execute SQL statement:", - implode("\t", $pdo->errorInfo()) + implode("\t", $pdo->errorInfo()), ]); - + return $errCode; } - - protected function getNextLevel(): int + + protected function getNextLevel(bool $eventDb = false): int { - $record = $this->db->query("SELECT level FROM ovk_upgrade_history ORDER BY level DESC LIMIT 1"); - if (!$record->getRowCount()) + $db = $eventDb ? $this->eventDb : $this->db; + $tbl = $eventDb ? "ovk_events_upgrade_history" : "ovk_upgrade_history"; + $record = $db->query("SELECT level FROM $tbl ORDER BY level DESC LIMIT 1"); + if (!$record->getRowCount()) { return 0; - + } + return $record->fetchField() + 1; } - - protected function getMigrationFiles(): array + + protected function getMigrationFiles(bool $eventDb = false): array { $files = []; $root = dirname(__DIR__ . "/../install/init-static-db.sql"); - - foreach (glob("$root/sqls/*.sql") as $file) + $dir = $eventDb ? "sqls/eventdb" : "sqls"; + + foreach (glob("$root/$dir/*.sql") as $file) { $files[(int) basename($file)] = basename($file); - + } + ksort($files); - + return $files; } - + protected function installChandler(InputInterface $input, SymfonyStyle $io, bool $drop = false): int { $chandlerLocation = $input->getArgument("chandler") ?? (__DIR__ . "/../../../../"); $chandlerConfigLocation = "$chandlerLocation/chandler.yml"; - + if (!file_exists($chandlerConfigLocation)) { $err = ["Could not find chandler location. Perhaps your config is too unique?"]; - if (!$input->getOption("chandler")) + if (!$input->getOption("chandler")) { $err[] = "Specify absolute path to your chandler installation using the --chandler option."; - + } + $io->getErrorStyle()->error($err); - + return 21; } - + if ($drop) { $bar = new ProgressBar($io, sizeof($this->chandlerTables)); $io->writeln("Dropping chandler tables..."); - - foreach ($bar->iterate($this->chandlerTables) as $table) + + foreach ($bar->iterate($this->chandlerTables) as $table) { $this->db->query("DROP TABLE IF EXISTS $table;"); - + } + $io->newLine(); } - + $installFile = file_get_contents("$chandlerLocation/install/init-db.sql"); - + return $this->executeSqlScript(22, $installFile, $io); } - + protected function initSchema(SymfonyStyle $io): int { $installFile = file_get_contents(__DIR__ . "/../install/init-static-db.sql"); - + return $this->executeSqlScript(31, $installFile, $io); } - + + protected function initEventSchema(SymfonyStyle $io): int + { + $installFile = file_get_contents(__DIR__ . "/../install/init-event-db.sql"); + + return $this->executeSqlScript(31, $installFile, $io, true, true); + } + protected function initUpgradeLog(SymfonyStyle $io): int { $installFile = file_get_contents(__DIR__ . "/../install/init-migration-table.sql"); - - return $this->executeSqlScript(31, $installFile, $io); + $rc = $this->executeSqlScript(31, $installFile, $io); + if ($rc) { + var_dump($rc); + return $rc; + } + + $installFile = file_get_contents(__DIR__ . "/../install/init-migration-table-event.sql"); + + return $this->executeSqlScript(32, $installFile, $io, false, true); } - - protected function runMigrations(SymfonyStyle $io, bool $oneshot): int + + protected function runMigrations(SymfonyStyle $io, bool $eventDb, bool $oneshot): int { - $nextLevel = $this->getNextLevel(); - $migrations = array_filter($this->getMigrationFiles(), fn ($id) => $id >= $nextLevel, ARRAY_FILTER_USE_KEY); - + $dir = $eventDb ? "sqls/eventdb" : "sqls"; + $tbl = $eventDb ? "ovk_events_upgrade_history" : "ovk_upgrade_history"; + $db = $eventDb ? $this->eventDb : $this->db; + $nextLevel = $this->getNextLevel($eventDb); + $migrations = array_filter( + $this->getMigrationFiles($eventDb), + fn($id) => $id >= $nextLevel, + ARRAY_FILTER_USE_KEY + ); + if (!sizeof($migrations)) { - $io->writeln("Database up to date. Nothing left to do."); - return 24; } - + $uname = addslashes(`whoami`); $bar = new ProgressBar($io, sizeof($migrations)); $bar->setFormat("very_verbose"); - + foreach ($bar->iterate($migrations) as $num => $migration) { - $script = file_get_contents(__DIR__ . "/../install/sqls/$migration"); - $res = $this->executeSqlScript(100 + $num, $script, $io, true); + $script = file_get_contents(__DIR__ . "/../install/$dir/$migration"); + $res = $this->executeSqlScript(100 + $num, $script, $io, true, $eventDb); if ($res != 0) { $io->getErrorStyle()->error("Error while executing migration №$num"); - + return $res; } - + $t = time(); - $this->db->query("INSERT INTO ovk_upgrade_history VALUES ($num, $t, \"$uname\");"); - - if ($oneshot) + $db->query("INSERT INTO $tbl VALUES ($num, $t, \"$uname\");"); + + if ($oneshot) { return 5; + } } - + + $io->newLine(); + return 0; } - + protected function execute(InputInterface $input, OutputInterface $output): int { $oneShotMode = $input->getOption("oneshot"); $io = new SymfonyStyle($input, $output); - + 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); } - + $migrationsOk = false; $chandlerOk = false; + $eventOk = false; $ovkOk = false; - - $this->checkDatabaseReadiness($chandlerOk, $ovkOk, $migrationsOk); - + + $this->checkDatabaseReadiness($chandlerOk, $ovkOk, $eventOk, $migrationsOk); + $res = -1; if ($chandlerOk === null) { $io->writeln("Chandler schema not detected, attempting to install..."); - + $res = $this->installChandler($input, $io); - } else if ($chandlerOk === false) { + } elseif ($chandlerOk === false) { if ($input->getOption("repair")) { $io->warning("Chandler schema detected but is broken, attempting to repair..."); - + $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)"); - + return 1; } } - - if ($res > 0) + + if ($res > 0) { return $res; - else if ($res == 0 && $oneShotMode) + } elseif ($res == 0 && $oneShotMode) { return 5; - + } + if (!$ovkOk) { $io->writeln("Initializing OpenVK schema..."); $res = $this->initSchema($io); - if ($res > 0) + if ($res > 0) { return $res; - else if ($oneShotMode) + } elseif ($oneShotMode) { return 5; + } } - + if (!$migrationsOk) { $io->writeln("Initializing upgrade log..."); $res = $this->initUpgradeLog($io); - if ($res > 0) + if ($res > 0) { return $res; - else if ($oneShotMode) + } elseif ($oneShotMode) { return 5; + } } - + + if ($eventOk !== false) { + if ($eventOk === null) { + $io->writeln("Initializing event database..."); + $res = $this->initEventSchema($io); + if ($res > 0) { + return $res; + } elseif ($oneShotMode) { + return 5; + } + } + + $io->writeln("Upgrading event database..."); + $res = $this->runMigrations($io, true, $oneShotMode); + if ($res == 24) { + $output->writeln("Event database already up to date."); + } elseif ($res > 0) { + return $res; + } + } + $io->writeln("Upgrading database..."); - $res = $this->runMigrations($io, $oneShotMode); - + $res = $this->runMigrations($io, false, $oneShotMode); + if (!$res) { $io->success("Database has been upgraded!"); - + return 0; - } else if ($res != 24) { + } elseif ($res != 24) { return $res; } - + + $io->writeln("Database up to date. Nothing left to do."); + return 0; } -} \ No newline at end of file +} diff --git a/bootstrap.php b/bootstrap.php index 9100d3f2..21fdc65b 100644 --- a/bootstrap.php +++ b/bootstrap.php @@ -443,9 +443,10 @@ function downloadable_name(string $text): string } return (function () { - if (php_sapi_name() != "cli") + if (php_sapi_name() != "cli") { _ovk_check_environment(); - + } + require __DIR__ . "/vendor/autoload.php"; setlocale(LC_TIME, "POSIX"); diff --git a/install/init-migration-table-event.sql b/install/init-migration-table-event.sql new file mode 100644 index 00000000..ca59eb45 --- /dev/null +++ b/install/init-migration-table-event.sql @@ -0,0 +1,5 @@ +CREATE TABLE `ovk_events_upgrade_history` ( + `level` smallint UNSIGNED NOT NULL, + `timestamp` bigint(20) UNSIGNED NOT NULL, + `operator` varchar(256) COLLATE utf8mb4_unicode_520_ci NOT NULL DEFAULT "Maintenance Script" +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; \ No newline at end of file diff --git a/install/init-migration-table.sql b/install/init-migration-table.sql index 53edb638..147fe5f4 100644 --- a/install/init-migration-table.sql +++ b/install/init-migration-table.sql @@ -1,9 +1,5 @@ -START TRANSACTION; - CREATE TABLE `ovk_upgrade_history` ( `level` smallint UNSIGNED NOT NULL, `timestamp` bigint(20) UNSIGNED NOT NULL, `operator` varchar(256) COLLATE utf8mb4_unicode_520_ci NOT NULL DEFAULT "Maintenance Script" -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; - -COMMIT; \ No newline at end of file +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; \ No newline at end of file