diff options
author | Maxence Lange <maxence@artificial-owl.com> | 2024-07-17 10:49:11 -0100 |
---|---|---|
committer | Maxence Lange <maxence@artificial-owl.com> | 2024-07-29 12:44:52 -0100 |
commit | 88cfab4f329c7ee1f360be1ad7d90cf767da3720 (patch) | |
tree | ad460429a5d58ef5ac67450cabc24e54bb9b1d9e /core/Command | |
parent | 10821643645f1e9ab5d6d768bd52b37468722d3f (diff) | |
download | nextcloud-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.php | 106 | ||||
-rw-r--r-- | core/Command/Db/Migrations/PreviewCommand.php | 162 |
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'); + } + } +} |