aboutsummaryrefslogtreecommitdiffstats
path: root/core/Command
diff options
context:
space:
mode:
authorMaxence Lange <maxence@artificial-owl.com>2024-07-17 10:49:11 -0100
committerMaxence Lange <maxence@artificial-owl.com>2024-07-29 12:44:52 -0100
commit88cfab4f329c7ee1f360be1ad7d90cf767da3720 (patch)
treead460429a5d58ef5ac67450cabc24e54bb9b1d9e /core/Command
parent10821643645f1e9ab5d6d768bd52b37468722d3f (diff)
downloadnextcloud-server-88cfab4f329c7ee1f360be1ad7d90cf767da3720.tar.gz
nextcloud-server-88cfab4f329c7ee1f360be1ad7d90cf767da3720.zip
feat(upgrade): migration attributes
Signed-off-by: Maxence Lange <maxence@artificial-owl.com> d Signed-off-by: Maxence Lange <maxence@artificial-owl.com> f Signed-off-by: Maxence Lange <maxence@artificial-owl.com> d Signed-off-by: Maxence Lange <maxence@artificial-owl.com>
Diffstat (limited to 'core/Command')
-rw-r--r--core/Command/Db/Migrations/GenerateMetadataCommand.php106
-rw-r--r--core/Command/Db/Migrations/PreviewCommand.php162
2 files changed, 268 insertions, 0 deletions
diff --git a/core/Command/Db/Migrations/GenerateMetadataCommand.php b/core/Command/Db/Migrations/GenerateMetadataCommand.php
new file mode 100644
index 00000000000..addcb59e68b
--- /dev/null
+++ b/core/Command/Db/Migrations/GenerateMetadataCommand.php
@@ -0,0 +1,106 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Core\Command\Db\Migrations;
+
+use OC\DB\Connection;
+use OC\DB\MigrationService;
+use OCP\App\IAppManager;
+use ReflectionClass;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class GenerateMetadataCommand extends Command {
+ public function __construct(
+ private readonly Connection $connection,
+ private readonly IAppManager $appManager,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure() {
+ $this->setName('migrations:generate-metadata')
+ ->setHidden(true)
+ ->setDescription('Generate metadata from DB migrations - internal and should not be used');
+
+ parent::configure();
+ }
+
+ public function execute(InputInterface $input, OutputInterface $output): int {
+ $output->writeln(
+ json_encode(
+ [
+ 'migrations' => $this->extractMigrationMetadata()
+ ],
+ JSON_PRETTY_PRINT
+ )
+ );
+
+ return 0;
+ }
+
+ private function extractMigrationMetadata(): array {
+ return [
+ 'core' => $this->extractMigrationMetadataFromCore(),
+ 'apps' => $this->extractMigrationMetadataFromApps()
+ ];
+ }
+
+ private function extractMigrationMetadataFromCore(): array {
+ return $this->extractMigrationAttributes('core');
+ }
+
+ /**
+ * get all apps and extract attributes
+ *
+ * @return array
+ * @throws \Exception
+ */
+ private function extractMigrationMetadataFromApps(): array {
+ $allApps = \OC_App::getAllApps();
+ $metadata = [];
+ foreach ($allApps as $appId) {
+ // We need to load app before being able to extract Migrations
+ // If app was not enabled before, we will disable it afterward.
+ $alreadyLoaded = $this->appManager->isInstalled($appId);
+ if (!$alreadyLoaded) {
+ $this->appManager->loadApp($appId);
+ }
+ $metadata[$appId] = $this->extractMigrationAttributes($appId);
+ if (!$alreadyLoaded) {
+ $this->appManager->disableApp($appId);
+ }
+ }
+ return $metadata;
+ }
+
+ /**
+ * We get all migrations from an app, and for each migration we extract attributes
+ *
+ * @param string $appId
+ *
+ * @return array
+ * @throws \Exception
+ */
+ private function extractMigrationAttributes(string $appId): array {
+ $ms = new MigrationService($appId, $this->connection);
+
+ $metadata = [];
+ foreach($ms->getAvailableVersions() as $version) {
+ $metadata[$version] = [];
+ $class = new ReflectionClass($ms->createInstance($version));
+ $attributes = $class->getAttributes();
+ foreach ($attributes as $attribute) {
+ $metadata[$version][] = $attribute->newInstance();
+ }
+ }
+
+ return $metadata;
+ }
+}
diff --git a/core/Command/Db/Migrations/PreviewCommand.php b/core/Command/Db/Migrations/PreviewCommand.php
new file mode 100644
index 00000000000..17d1d8b01ec
--- /dev/null
+++ b/core/Command/Db/Migrations/PreviewCommand.php
@@ -0,0 +1,162 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Core\Command\Db\Migrations;
+
+use OC\DB\Connection;
+use OC\DB\MigrationService;
+use OCP\Migration\Attributes\GenericMigrationAttribute;
+use OCP\Migration\Attributes\MigrationAttribute;
+use OCP\Migration\Exceptions\AttributeException;
+use Psr\Log\LoggerInterface;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Helper\Table;
+use Symfony\Component\Console\Helper\TableCell;
+use Symfony\Component\Console\Helper\TableCellStyle;
+use Symfony\Component\Console\Helper\TableSeparator;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class PreviewCommand extends Command {
+ public function __construct(
+ private readonly Connection $connection,
+ private readonly LoggerInterface $logger,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure() {
+ $this
+ ->setName('migrations:preview')
+ ->setDescription('Get preview of available DB migrations in case of initiating an upgrade')
+ ->addArgument('version', InputArgument::REQUIRED, 'The destination version number');
+
+ parent::configure();
+ }
+
+ public function execute(InputInterface $input, OutputInterface $output): int {
+ $version = $input->getArgument('version');
+
+ $metadata = $this->getMetadata($version);
+ $parsed = $this->getMigrationsAttributes($metadata);
+
+ $table = new Table($output);
+ $this->displayMigrations($table, 'core', $parsed['core']);
+
+ $table->render();
+
+ return 0;
+ }
+
+ private function displayMigrations(Table $table, string $appId, array $data): void {
+ $done = $this->getDoneMigrations($appId);
+ $done = array_diff($done, ['30000Date20240429122720']);
+
+ $table->addRow(
+ [
+ new TableCell(
+ $appId,
+ [
+ 'colspan' => 2,
+ 'style' => new TableCellStyle(['cellFormat' => '<info>%s</info>'])
+ ]
+ )
+ ]
+ )->addRow(new TableSeparator());
+
+ foreach($data as $migration => $attributes) {
+ if (in_array($migration, $done)) {
+ continue;
+ }
+
+ $attributesStr = [];
+ /** @var MigrationAttribute[] $attributes */
+ foreach($attributes as $attribute) {
+ $definition = '<info>' . $attribute->definition() . "</info>";
+ $definition .= empty($attribute->getDescription()) ? '' : "\n " . $attribute->getDescription();
+ $definition .= empty($attribute->getNotes()) ? '' : "\n <comment>" . implode("</comment>\n <comment>", $attribute->getNotes()) . '</comment>';
+ $attributesStr[] = $definition;
+ }
+ $table->addRow([$migration, implode("\n", $attributesStr)]);
+ }
+
+ }
+
+
+
+
+
+ private function getMetadata(string $version): array {
+ $metadata = json_decode(file_get_contents('/tmp/nextcloud-' . $version . '.metadata'), true);
+ if (!$metadata) {
+ throw new \Exception();
+ }
+ return $metadata['migrations'] ?? [];
+ }
+
+ private function getDoneMigrations(string $appId): array {
+ $ms = new MigrationService($appId, $this->connection);
+ return $ms->getMigratedVersions();
+ }
+
+ private function getMigrationsAttributes(array $metadata): array {
+ $appsAttributes = [];
+ foreach (array_keys($metadata['apps']) as $appId) {
+ $appsAttributes[$appId] = $this->parseMigrations($metadata['apps'][$appId] ?? []);
+ }
+
+ return [
+ 'core' => $this->parseMigrations($metadata['core'] ?? []),
+ 'apps' => $appsAttributes
+ ];
+ }
+
+ private function parseMigrations(array $migrations): array {
+ $parsed = [];
+ foreach (array_keys($migrations) as $entry) {
+ $items = $migrations[$entry];
+ $parsed[$entry] = [];
+ foreach ($items as $item) {
+ try {
+ $parsed[$entry][] = $this->createAttribute($item);
+ } catch (AttributeException $e) {
+ $this->logger->warning(
+ 'exception while trying to create attribute',
+ ['exception' => $e, 'item' => json_encode($item)]
+ );
+ $parsed[$entry][] = new GenericMigrationAttribute($item);
+ }
+ }
+ }
+
+ return $parsed;
+ }
+
+ /**
+ * @param array $item
+ *
+ * @return MigrationAttribute|null
+ * @throws AttributeException
+ */
+ private function createAttribute(array $item): ?MigrationAttribute {
+ $class = $item['class'] ?? '';
+ $namespace = 'OCP\Migration\Attributes\\';
+ if (!str_starts_with($class, $namespace)
+ || !ctype_alpha(substr($class, strlen($namespace)))) {
+ throw new AttributeException('class name does not looks valid');
+ }
+
+ try {
+ $attribute = new $class();
+ return $attribute->import($item);
+ } catch (\Error) {
+ throw new AttributeException('cannot import Attribute');
+ }
+ }
+}