diff --git a/core/Command/Db/Migrations/GenerateMetadataCommand.php b/core/Command/Db/Migrations/GenerateMetadataCommand.php index addcb59e68b03..effd064480dd0 100644 --- a/core/Command/Db/Migrations/GenerateMetadataCommand.php +++ b/core/Command/Db/Migrations/GenerateMetadataCommand.php @@ -9,22 +9,21 @@ namespace OC\Core\Command\Db\Migrations; use OC\DB\Connection; -use OC\DB\MigrationService; +use OC\Migration\MetadataManager; 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 MetadataManager $metadataManager, private readonly IAppManager $appManager, ) { parent::__construct(); } - protected function configure() { + protected function configure(): void { $this->setName('migrations:generate-metadata') ->setHidden(true) ->setDescription('Generate metadata from DB migrations - internal and should not be used'); @@ -45,7 +44,9 @@ public function execute(InputInterface $input, OutputInterface $output): int { return 0; } - private function extractMigrationMetadata(): array { + + + public function extractMigrationMetadata(): array { return [ 'core' => $this->extractMigrationMetadataFromCore(), 'apps' => $this->extractMigrationMetadataFromApps() @@ -53,7 +54,7 @@ private function extractMigrationMetadata(): array { } private function extractMigrationMetadataFromCore(): array { - return $this->extractMigrationAttributes('core'); + return $this->metadataManager->extractMigrationAttributes('core'); } /** @@ -72,35 +73,11 @@ private function extractMigrationMetadataFromApps(): array { if (!$alreadyLoaded) { $this->appManager->loadApp($appId); } - $metadata[$appId] = $this->extractMigrationAttributes($appId); + $metadata[$appId] = $this->metadataManager->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 index 17d1d8b01ec61..7ab4c75e19c01 100644 --- a/core/Command/Db/Migrations/PreviewCommand.php +++ b/core/Command/Db/Migrations/PreviewCommand.php @@ -8,12 +8,9 @@ */ namespace OC\Core\Command\Db\Migrations; -use OC\DB\Connection; -use OC\DB\MigrationService; -use OCP\Migration\Attributes\GenericMigrationAttribute; +use OC\Migration\MetadataManager; +use OC\Updater\ReleaseMetadata; 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; @@ -24,14 +21,15 @@ use Symfony\Component\Console\Output\OutputInterface; class PreviewCommand extends Command { + private bool $initiated = false; public function __construct( - private readonly Connection $connection, - private readonly LoggerInterface $logger, + private readonly MetadataManager $metadataManager, + private readonly ReleaseMetadata $releaseMetadata, ) { parent::__construct(); } - protected function configure() { + protected function configure(): void { $this ->setName('migrations:preview') ->setDescription('Get preview of available DB migrations in case of initiating an upgrade') @@ -42,21 +40,37 @@ protected function configure() { public function execute(InputInterface $input, OutputInterface $output): int { $version = $input->getArgument('version'); + if (filter_var($version, FILTER_VALIDATE_URL)) { + $metadata = $this->releaseMetadata->downloadMetadata($version); + } else if (str_starts_with($version, '/')) { + $metadata = json_decode(file_get_contents($version), true, flags: JSON_THROW_ON_ERROR); + } else { + $metadata = $this->releaseMetadata->getMetadata($version); + } - $metadata = $this->getMetadata($version); - $parsed = $this->getMigrationsAttributes($metadata); + $parsed = $this->metadataManager->getMigrationsAttributesFromReleaseMetadata($metadata['migrations'] ?? [], true); $table = new Table($output); - $this->displayMigrations($table, 'core', $parsed['core']); - + $this->displayMigrations($table, 'core', $parsed['core'] ?? []); + foreach ($parsed['apps'] as $appId => $migrations) { + if (!empty($migrations)) { + $this->displayMigrations($table, $appId, $migrations); + } + } $table->render(); return 0; } private function displayMigrations(Table $table, string $appId, array $data): void { - $done = $this->getDoneMigrations($appId); - $done = array_diff($done, ['30000Date20240429122720']); + if (empty($data)) { + return; + } + + if ($this->initiated) { + $table->addRow(new TableSeparator()); + } + $this->initiated = true; $table->addRow( [ @@ -70,13 +84,9 @@ private function displayMigrations(Table $table, string $appId, array $data): vo ] )->addRow(new TableSeparator()); + /** @var MigrationAttribute[] $attributes */ 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(); @@ -85,78 +95,5 @@ private function displayMigrations(Table $table, string $appId, array $data): vo } $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'); - } } } diff --git a/lib/private/Migration/MetadataManager.php b/lib/private/Migration/MetadataManager.php new file mode 100644 index 0000000000000..c5061b6fe0c1c --- /dev/null +++ b/lib/private/Migration/MetadataManager.php @@ -0,0 +1,153 @@ +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; + } + + /** + * convert direct data from release metadata into a list of Migrations' Attribute + * + * @param array $metadata + * @param bool $filterKnownMigrations ignore metadata already done in local instance + * + * @return array + * @since 30.0.0 + */ + public function getMigrationsAttributesFromReleaseMetadata( + array $metadata, + bool $filterKnownMigrations = false + ): array { + $appsAttributes = []; + foreach (array_keys($metadata['apps']) as $appId) { + if ($filterKnownMigrations && !$this->appManager->isInstalled($appId)) { + continue; // if not interested and app is not installed + } + $done = ($filterKnownMigrations) ? $this->getKnownMigrations($appId) : []; + $appsAttributes[$appId] = $this->parseMigrations($metadata['apps'][$appId] ?? [], $done); + } + + $done = ($filterKnownMigrations) ? $this->getKnownMigrations('core') : []; + return [ + 'core' => $this->parseMigrations($metadata['core'] ?? [], $done), + 'apps' => $appsAttributes + ]; + } + + /** + * convert raw data to a list of MigrationAttribute + * + * @param array $migrations + * @param array $ignoreMigrations + * + * @return array + */ + private function parseMigrations(array $migrations, array $ignoreMigrations = []): array { + $parsed = []; + foreach (array_keys($migrations) as $entry) { + if (in_array($entry, $ignoreMigrations)) { + continue; + } + + $parsed[$entry] = []; + foreach ($migrations[$entry] 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; + } + + /** + * returns migrations already done + * + * @param string $appId + * + * @return array + * @throws \Exception + */ + private function getKnownMigrations(string $appId): array { + $ms = new MigrationService($appId, $this->connection); + return $ms->getMigratedVersions(); + } + + + /** + * generate (deserialize) a MigrationAttribute from a serialized version + * + * @param array $item + * + * @return MigrationAttribute + * @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'); + } + } +} diff --git a/lib/private/Updater/Exceptions/ReleaseMetadataException.php b/lib/private/Updater/Exceptions/ReleaseMetadataException.php new file mode 100644 index 0000000000000..bc82e4e03df7a --- /dev/null +++ b/lib/private/Updater/Exceptions/ReleaseMetadataException.php @@ -0,0 +1,17 @@ +downloadMetadata($url); + } + + /** + * download Metadata from a link + * + * @param string $url + * + * @return array + * @throws ReleaseMetadataException + * @since 30.0.0 + */ + public function downloadMetadata(string $url): array { + $client = $this->clientService->newClient(); + try { + $response = $client->get($url, [ + 'timeout' => 10, + 'connect_timeout' => 10, + 'verify' => false, + ]); + } catch (Exception $e) { + throw new ReleaseMetadataException('could not reach metadata at ' . $url, previous: $e); + } + + try { + return json_decode($response->getBody(), true, flags: JSON_THROW_ON_ERROR); + } catch (JsonException) { + throw new ReleaseMetadataException('remote document is not valid'); + } + } +}