From 88cfab4f329c7ee1f360be1ad7d90cf767da3720 Mon Sep 17 00:00:00 2001 From: Maxence Lange Date: Wed, 17 Jul 2024 10:49:11 -0100 Subject: feat(upgrade): migration attributes Signed-off-by: Maxence Lange d Signed-off-by: Maxence Lange f Signed-off-by: Maxence Lange d Signed-off-by: Maxence Lange --- .../Db/Migrations/GenerateMetadataCommand.php | 106 ++++++++++++++ core/Command/Db/Migrations/PreviewCommand.php | 162 +++++++++++++++++++++ 2 files changed, 268 insertions(+) create mode 100644 core/Command/Db/Migrations/GenerateMetadataCommand.php create mode 100644 core/Command/Db/Migrations/PreviewCommand.php (limited to 'core/Command') 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 @@ +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 @@ +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' => '%s']) + ] + ) + ] + )->addRow(new TableSeparator()); + + foreach($data as $migration => $attributes) { + if (in_array($migration, $done)) { + continue; + } + + $attributesStr = []; + /** @var MigrationAttribute[] $attributes */ + foreach($attributes as $attribute) { + $definition = '' . $attribute->definition() . ""; + $definition .= empty($attribute->getDescription()) ? '' : "\n " . $attribute->getDescription(); + $definition .= empty($attribute->getNotes()) ? '' : "\n " . implode("\n ", $attribute->getNotes()) . ''; + $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'); + } + } +} -- cgit v1.2.3