diff options
author | Maxence Lange <maxence@artificial-owl.com> | 2024-08-02 01:43:04 -0100 |
---|---|---|
committer | Maxence Lange <maxence@artificial-owl.com> | 2024-08-06 22:41:35 -0100 |
commit | e227d5afa1e9c71062133a13bec8ac051a9ec5b7 (patch) | |
tree | 7bc29cdcd30b759e68a90726b9610d41ede00e63 | |
parent | 89f82fd5378e2f4e53966ce25fbc8a4f02386de6 (diff) | |
download | nextcloud-server-e227d5afa1e9c71062133a13bec8ac051a9ec5b7.tar.gz nextcloud-server-e227d5afa1e9c71062133a13bec8ac051a9ec5b7.zip |
feat(upgrade): migration attributes
Signed-off-by: Maxence Lange <maxence@artificial-owl.com>
21 files changed, 1138 insertions, 0 deletions
diff --git a/core/Command/Db/Migrations/PreviewCommand.php b/core/Command/Db/Migrations/PreviewCommand.php new file mode 100644 index 00000000000..6440ed5835c --- /dev/null +++ b/core/Command/Db/Migrations/PreviewCommand.php @@ -0,0 +1,102 @@ +<?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\Migration\MetadataManager; +use OC\Updater\ReleaseMetadata; +use OCP\Migration\Attributes\MigrationAttribute; +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; + +/** + * @since 30.0.0 + */ +class PreviewCommand extends Command { + private bool $initiated = false; + public function __construct( + private MetadataManager $metadataManager, + private ReleaseMetadata $releaseMetadata, + ) { + parent::__construct(); + } + + protected function configure(): void { + $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'); + if (filter_var($version, FILTER_VALIDATE_URL)) { + $metadata = $this->releaseMetadata->downloadMetadata($version); + } elseif (str_starts_with($version, '/')) { + $metadata = json_decode(file_get_contents($version), true, flags: JSON_THROW_ON_ERROR); + } else { + $metadata = $this->releaseMetadata->getMetadata($version); + } + + $parsed = $this->metadataManager->getMigrationsAttributesFromReleaseMetadata($metadata['migrations'] ?? [], true); + + $table = new Table($output); + $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 { + if (empty($data)) { + return; + } + + if ($this->initiated) { + $table->addRow(new TableSeparator()); + } + $this->initiated = true; + + $table->addRow( + [ + new TableCell( + $appId, + [ + 'colspan' => 2, + 'style' => new TableCellStyle(['cellFormat' => '<info>%s</info>']) + ] + ) + ] + )->addRow(new TableSeparator()); + + /** @var MigrationAttribute[] $attributes */ + foreach($data as $migration => $attributes) { + $attributesStr = []; + 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)]); + } + } +} diff --git a/core/register_command.php b/core/register_command.php index 4a84e551ce0..6d1abb52fea 100644 --- a/core/register_command.php +++ b/core/register_command.php @@ -110,6 +110,7 @@ if ($config->getSystemValueBool('installed', false)) { $application->add(Server::get(Command\Db\AddMissingIndices::class)); $application->add(Server::get(Command\Db\AddMissingPrimaryKeys::class)); + $application->add(Server::get(Command\Db\Migrations\PreviewCommand::class)); if ($config->getSystemValueBool('debug', false)) { $application->add(Server::get(Command\Db\Migrations\StatusCommand::class)); $application->add(Server::get(Command\Db\Migrations\MigrateCommand::class)); diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 6defb0f12fb..a09bbb25038 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -555,6 +555,18 @@ return array( 'OCP\\Mail\\IEMailTemplate' => $baseDir . '/lib/public/Mail/IEMailTemplate.php', 'OCP\\Mail\\IMailer' => $baseDir . '/lib/public/Mail/IMailer.php', 'OCP\\Mail\\IMessage' => $baseDir . '/lib/public/Mail/IMessage.php', + 'OCP\\Migration\\Attributes\\AddColumn' => $baseDir . '/lib/public/Migration/Attributes/AddColumn.php', + 'OCP\\Migration\\Attributes\\AddIndex' => $baseDir . '/lib/public/Migration/Attributes/AddIndex.php', + 'OCP\\Migration\\Attributes\\ColumnMigrationAttribute' => $baseDir . '/lib/public/Migration/Attributes/ColumnMigrationAttribute.php', + 'OCP\\Migration\\Attributes\\CreateTable' => $baseDir . '/lib/public/Migration/Attributes/CreateTable.php', + 'OCP\\Migration\\Attributes\\DropColumn' => $baseDir . '/lib/public/Migration/Attributes/DropColumn.php', + 'OCP\\Migration\\Attributes\\DropIndex' => $baseDir . '/lib/public/Migration/Attributes/DropIndex.php', + 'OCP\\Migration\\Attributes\\DropTable' => $baseDir . '/lib/public/Migration/Attributes/DropTable.php', + 'OCP\\Migration\\Attributes\\GenericMigrationAttribute' => $baseDir . '/lib/public/Migration/Attributes/GenericMigrationAttribute.php', + 'OCP\\Migration\\Attributes\\IndexMigrationAttribute' => $baseDir . '/lib/public/Migration/Attributes/IndexMigrationAttribute.php', + 'OCP\\Migration\\Attributes\\MigrationAttribute' => $baseDir . '/lib/public/Migration/Attributes/MigrationAttribute.php', + 'OCP\\Migration\\Attributes\\ModifyColumn' => $baseDir . '/lib/public/Migration/Attributes/ModifyColumn.php', + 'OCP\\Migration\\Attributes\\TableMigrationAttribute' => $baseDir . '/lib/public/Migration/Attributes/TableMigrationAttribute.php', 'OCP\\Migration\\BigIntMigration' => $baseDir . '/lib/public/Migration/BigIntMigration.php', 'OCP\\Migration\\IMigrationStep' => $baseDir . '/lib/public/Migration/IMigrationStep.php', 'OCP\\Migration\\IOutput' => $baseDir . '/lib/public/Migration/IOutput.php', @@ -1071,6 +1083,7 @@ return array( 'OC\\Core\\Command\\Db\\Migrations\\ExecuteCommand' => $baseDir . '/core/Command/Db/Migrations/ExecuteCommand.php', 'OC\\Core\\Command\\Db\\Migrations\\GenerateCommand' => $baseDir . '/core/Command/Db/Migrations/GenerateCommand.php', 'OC\\Core\\Command\\Db\\Migrations\\MigrateCommand' => $baseDir . '/core/Command/Db/Migrations/MigrateCommand.php', + 'OC\\Core\\Command\\Db\\Migrations\\PreviewCommand' => $baseDir . '/core/Command/Db/Migrations/PreviewCommand.php', 'OC\\Core\\Command\\Db\\Migrations\\StatusCommand' => $baseDir . '/core/Command/Db/Migrations/StatusCommand.php', 'OC\\Core\\Command\\Encryption\\ChangeKeyStorageRoot' => $baseDir . '/core/Command/Encryption/ChangeKeyStorageRoot.php', 'OC\\Core\\Command\\Encryption\\DecryptAll' => $baseDir . '/core/Command/Encryption/DecryptAll.php', @@ -1565,6 +1578,8 @@ return array( 'OC\\MemoryInfo' => $baseDir . '/lib/private/MemoryInfo.php', 'OC\\Migration\\BackgroundRepair' => $baseDir . '/lib/private/Migration/BackgroundRepair.php', 'OC\\Migration\\ConsoleOutput' => $baseDir . '/lib/private/Migration/ConsoleOutput.php', + 'OC\\Migration\\Exceptions\\AttributeException' => $baseDir . '/lib/private/Migration/Exceptions/AttributeException.php', + 'OC\\Migration\\MetadataManager' => $baseDir . '/lib/private/Migration/MetadataManager.php', 'OC\\Migration\\NullOutput' => $baseDir . '/lib/private/Migration/NullOutput.php', 'OC\\Migration\\SimpleOutput' => $baseDir . '/lib/private/Migration/SimpleOutput.php', 'OC\\NaturalSort' => $baseDir . '/lib/private/NaturalSort.php', @@ -1844,6 +1859,8 @@ return array( 'OC\\Updater\\Changes' => $baseDir . '/lib/private/Updater/Changes.php', 'OC\\Updater\\ChangesCheck' => $baseDir . '/lib/private/Updater/ChangesCheck.php', 'OC\\Updater\\ChangesMapper' => $baseDir . '/lib/private/Updater/ChangesMapper.php', + 'OC\\Updater\\Exceptions\\ReleaseMetadataException' => $baseDir . '/lib/private/Updater/Exceptions/ReleaseMetadataException.php', + 'OC\\Updater\\ReleaseMetadata' => $baseDir . '/lib/private/Updater/ReleaseMetadata.php', 'OC\\Updater\\VersionCheck' => $baseDir . '/lib/private/Updater/VersionCheck.php', 'OC\\UserStatus\\ISettableProvider' => $baseDir . '/lib/private/UserStatus/ISettableProvider.php', 'OC\\UserStatus\\Manager' => $baseDir . '/lib/private/UserStatus/Manager.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index db8baeb6341..16fb699964c 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -588,6 +588,18 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OCP\\Mail\\IEMailTemplate' => __DIR__ . '/../../..' . '/lib/public/Mail/IEMailTemplate.php', 'OCP\\Mail\\IMailer' => __DIR__ . '/../../..' . '/lib/public/Mail/IMailer.php', 'OCP\\Mail\\IMessage' => __DIR__ . '/../../..' . '/lib/public/Mail/IMessage.php', + 'OCP\\Migration\\Attributes\\AddColumn' => __DIR__ . '/../../..' . '/lib/public/Migration/Attributes/AddColumn.php', + 'OCP\\Migration\\Attributes\\AddIndex' => __DIR__ . '/../../..' . '/lib/public/Migration/Attributes/AddIndex.php', + 'OCP\\Migration\\Attributes\\ColumnMigrationAttribute' => __DIR__ . '/../../..' . '/lib/public/Migration/Attributes/ColumnMigrationAttribute.php', + 'OCP\\Migration\\Attributes\\CreateTable' => __DIR__ . '/../../..' . '/lib/public/Migration/Attributes/CreateTable.php', + 'OCP\\Migration\\Attributes\\DropColumn' => __DIR__ . '/../../..' . '/lib/public/Migration/Attributes/DropColumn.php', + 'OCP\\Migration\\Attributes\\DropIndex' => __DIR__ . '/../../..' . '/lib/public/Migration/Attributes/DropIndex.php', + 'OCP\\Migration\\Attributes\\DropTable' => __DIR__ . '/../../..' . '/lib/public/Migration/Attributes/DropTable.php', + 'OCP\\Migration\\Attributes\\GenericMigrationAttribute' => __DIR__ . '/../../..' . '/lib/public/Migration/Attributes/GenericMigrationAttribute.php', + 'OCP\\Migration\\Attributes\\IndexMigrationAttribute' => __DIR__ . '/../../..' . '/lib/public/Migration/Attributes/IndexMigrationAttribute.php', + 'OCP\\Migration\\Attributes\\MigrationAttribute' => __DIR__ . '/../../..' . '/lib/public/Migration/Attributes/MigrationAttribute.php', + 'OCP\\Migration\\Attributes\\ModifyColumn' => __DIR__ . '/../../..' . '/lib/public/Migration/Attributes/ModifyColumn.php', + 'OCP\\Migration\\Attributes\\TableMigrationAttribute' => __DIR__ . '/../../..' . '/lib/public/Migration/Attributes/TableMigrationAttribute.php', 'OCP\\Migration\\BigIntMigration' => __DIR__ . '/../../..' . '/lib/public/Migration/BigIntMigration.php', 'OCP\\Migration\\IMigrationStep' => __DIR__ . '/../../..' . '/lib/public/Migration/IMigrationStep.php', 'OCP\\Migration\\IOutput' => __DIR__ . '/../../..' . '/lib/public/Migration/IOutput.php', @@ -1104,6 +1116,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\Core\\Command\\Db\\Migrations\\ExecuteCommand' => __DIR__ . '/../../..' . '/core/Command/Db/Migrations/ExecuteCommand.php', 'OC\\Core\\Command\\Db\\Migrations\\GenerateCommand' => __DIR__ . '/../../..' . '/core/Command/Db/Migrations/GenerateCommand.php', 'OC\\Core\\Command\\Db\\Migrations\\MigrateCommand' => __DIR__ . '/../../..' . '/core/Command/Db/Migrations/MigrateCommand.php', + 'OC\\Core\\Command\\Db\\Migrations\\PreviewCommand' => __DIR__ . '/../../..' . '/core/Command/Db/Migrations/PreviewCommand.php', 'OC\\Core\\Command\\Db\\Migrations\\StatusCommand' => __DIR__ . '/../../..' . '/core/Command/Db/Migrations/StatusCommand.php', 'OC\\Core\\Command\\Encryption\\ChangeKeyStorageRoot' => __DIR__ . '/../../..' . '/core/Command/Encryption/ChangeKeyStorageRoot.php', 'OC\\Core\\Command\\Encryption\\DecryptAll' => __DIR__ . '/../../..' . '/core/Command/Encryption/DecryptAll.php', @@ -1598,6 +1611,8 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\MemoryInfo' => __DIR__ . '/../../..' . '/lib/private/MemoryInfo.php', 'OC\\Migration\\BackgroundRepair' => __DIR__ . '/../../..' . '/lib/private/Migration/BackgroundRepair.php', 'OC\\Migration\\ConsoleOutput' => __DIR__ . '/../../..' . '/lib/private/Migration/ConsoleOutput.php', + 'OC\\Migration\\Exceptions\\AttributeException' => __DIR__ . '/../../..' . '/lib/private/Migration/Exceptions/AttributeException.php', + 'OC\\Migration\\MetadataManager' => __DIR__ . '/../../..' . '/lib/private/Migration/MetadataManager.php', 'OC\\Migration\\NullOutput' => __DIR__ . '/../../..' . '/lib/private/Migration/NullOutput.php', 'OC\\Migration\\SimpleOutput' => __DIR__ . '/../../..' . '/lib/private/Migration/SimpleOutput.php', 'OC\\NaturalSort' => __DIR__ . '/../../..' . '/lib/private/NaturalSort.php', @@ -1877,6 +1892,8 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\Updater\\Changes' => __DIR__ . '/../../..' . '/lib/private/Updater/Changes.php', 'OC\\Updater\\ChangesCheck' => __DIR__ . '/../../..' . '/lib/private/Updater/ChangesCheck.php', 'OC\\Updater\\ChangesMapper' => __DIR__ . '/../../..' . '/lib/private/Updater/ChangesMapper.php', + 'OC\\Updater\\Exceptions\\ReleaseMetadataException' => __DIR__ . '/../../..' . '/lib/private/Updater/Exceptions/ReleaseMetadataException.php', + 'OC\\Updater\\ReleaseMetadata' => __DIR__ . '/../../..' . '/lib/private/Updater/ReleaseMetadata.php', 'OC\\Updater\\VersionCheck' => __DIR__ . '/../../..' . '/lib/private/Updater/VersionCheck.php', 'OC\\UserStatus\\ISettableProvider' => __DIR__ . '/../../..' . '/lib/private/UserStatus/ISettableProvider.php', 'OC\\UserStatus\\Manager' => __DIR__ . '/../../..' . '/lib/private/UserStatus/Manager.php', diff --git a/lib/private/Migration/Exceptions/AttributeException.php b/lib/private/Migration/Exceptions/AttributeException.php new file mode 100644 index 00000000000..3daf99032ad --- /dev/null +++ b/lib/private/Migration/Exceptions/AttributeException.php @@ -0,0 +1,17 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OC\Migration\Exceptions; + +use Exception; + +/** + * @since 30.0.0 + */ +class AttributeException extends Exception { +} diff --git a/lib/private/Migration/MetadataManager.php b/lib/private/Migration/MetadataManager.php new file mode 100644 index 00000000000..cf569755ecd --- /dev/null +++ b/lib/private/Migration/MetadataManager.php @@ -0,0 +1,127 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OC\Migration; + +use OC\DB\Connection; +use OC\DB\MigrationService; +use OC\Migration\Exceptions\AttributeException; +use OCP\App\IAppManager; +use OCP\Migration\Attributes\GenericMigrationAttribute; +use OCP\Migration\Attributes\MigrationAttribute; +use Psr\Log\LoggerInterface; + +/** + * Helps managing DB Migrations' Metadata + * + * @since 30.0.0 + */ +class MetadataManager { + public function __construct( + private IAppManager $appManager, + private Connection $connection, + private LoggerInterface $logger, + ) { + } + + /** + * convert direct data from release metadata into a list of Migrations' Attribute + * + * @param array<array-key, array<array-key, array>> $metadata + * @param bool $filterKnownMigrations ignore metadata already done in local instance + * + * @return array{apps: array<array-key, array<string, MigrationAttribute[]>>, core: array<string, MigrationAttribute[]>} + * @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<string, MigrationAttribute[]> + */ + 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($item['table'] ?? ''); + 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 00000000000..bc82e4e03df --- /dev/null +++ b/lib/private/Updater/Exceptions/ReleaseMetadataException.php @@ -0,0 +1,17 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OC\Updater\Exceptions; + +use Exception; + +/** + * @since 30.0.0 + */ +class ReleaseMetadataException extends Exception { +} diff --git a/lib/private/Updater/ReleaseMetadata.php b/lib/private/Updater/ReleaseMetadata.php new file mode 100644 index 00000000000..4596d1d3fb7 --- /dev/null +++ b/lib/private/Updater/ReleaseMetadata.php @@ -0,0 +1,79 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OC\Updater; + +use Exception; +use JsonException; +use OC\Updater\Exceptions\ReleaseMetadataException; +use OCP\Http\Client\IClientService; + +/** retrieve releases metadata from official servers + * + * @since 30.0.0 + */ +class ReleaseMetadata { + public function __construct( + private IClientService $clientService, + ) { + } + + /** + * returns metadata based on release version + * + * - version is a stable release, metadata is downloaded from official releases folder + * - version is not a table release, metadata is downloaded from official prereleases folder + * - version is a major version (30, 31, 32, ...), latest metadata are downloaded + * + * @param string $version + * + * @return array + * @throws ReleaseMetadataException + * @since 30.0.0 + */ + public function getMetadata(string $version): array { + if (!str_contains($version, '.')) { + $url = 'https://download.nextcloud.com/server/releases/latest-' . $version . '.metadata'; + } else { + [,,$minor] = explode('.', $version); + if (ctype_digit($minor)) { + $url = 'https://download.nextcloud.com/server/releases/nextcloud-' . $version . '.metadata'; + } else { + $url = 'https://download.nextcloud.com/server/prereleases/nextcloud-' . $version . '.metadata'; + } + } + return $this->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 + ]); + } 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'); + } + } +} diff --git a/lib/public/Migration/Attributes/AddColumn.php b/lib/public/Migration/Attributes/AddColumn.php new file mode 100644 index 00000000000..3ecae8fde0d --- /dev/null +++ b/lib/public/Migration/Attributes/AddColumn.php @@ -0,0 +1,29 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\Migration\Attributes; + +use Attribute; + +/** + * attribute on new column creation + * + * @since 30.0.0 + */ +#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_CLASS)] +class AddColumn extends ColumnMigrationAttribute { + /** + * @return string + * @since 30.0.0 + */ + public function definition(): string { + return empty($this->getName()) ? + 'Addition of a new column to table \'' . $this->getTable() . '\'' + : 'Addition of column \'' . $this->getName() . '\' to table \'' . $this->getTable() . '\''; + } +} diff --git a/lib/public/Migration/Attributes/AddIndex.php b/lib/public/Migration/Attributes/AddIndex.php new file mode 100644 index 00000000000..a7da4173b29 --- /dev/null +++ b/lib/public/Migration/Attributes/AddIndex.php @@ -0,0 +1,27 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\Migration\Attributes; + +use Attribute; + +/** + * attribute on index creation + * + * @since 30.0.0 + */ +#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_CLASS)] +class AddIndex extends IndexMigrationAttribute { + /** + * @return string + * @since 30.0.0 + */ + public function definition(): string { + return 'Addition of a new index to table \'' . $this->getTable() . '\''; + } +} diff --git a/lib/public/Migration/Attributes/ColumnMigrationAttribute.php b/lib/public/Migration/Attributes/ColumnMigrationAttribute.php new file mode 100644 index 00000000000..8784f5bf2cf --- /dev/null +++ b/lib/public/Migration/Attributes/ColumnMigrationAttribute.php @@ -0,0 +1,78 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\Migration\Attributes; + +use JsonSerializable; + +/** + * generic class related to migration attribute about column changes + * + * @since 30.0.0 + */ +class ColumnMigrationAttribute extends MigrationAttribute implements JsonSerializable { + /** + * @param string $table name of the database table + * @param string $name name of the column + * @param string $description description of the migration + * @param array $notes notes about the migration/column + * @since 30.0.0 + */ + public function __construct( + string $table, + private string $name = '', + string $description = '', + array $notes = [], + ) { + parent::__construct($table, $description, $notes); + } + + /** + * @param string $name + * + * @return $this + * @since 30.0.0 + */ + public function setName(string $name): self { + $this->name = $name; + return $this; + } + + /** + * @return string + * @since 30.0.0 + */ + public function getName(): string { + return $this->name; + } + + /** + * @param array $data + * + * @return $this + * @since 30.0.0 + */ + public function import(array $data): self { + parent::import($data); + $this->setName($data['name'] ?? ''); + return $this; + } + + /** + * @return array + * @since 30.0.0 + */ + public function jsonSerialize(): array { + return array_merge( + parent::jsonSerialize(), + [ + 'name' => $this->getName(), + ] + ); + } +} diff --git a/lib/public/Migration/Attributes/CreateTable.php b/lib/public/Migration/Attributes/CreateTable.php new file mode 100644 index 00000000000..df0418fa4bc --- /dev/null +++ b/lib/public/Migration/Attributes/CreateTable.php @@ -0,0 +1,29 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\Migration\Attributes; + +use Attribute; + +/** + * attribute on table creation + * + * @since 30.0.0 + */ +#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_CLASS)] +class CreateTable extends TableMigrationAttribute { + /** + * @return string + * @since 30.0.0 + */ + public function definition(): string { + $definition = 'Creation of new table \'' . $this->getTable() . '\''; + $definition .= empty($this->getColumns()) ? '' : ' with columns ' . implode(', ', $this->getColumns()); + return $definition; + } +} diff --git a/lib/public/Migration/Attributes/DropColumn.php b/lib/public/Migration/Attributes/DropColumn.php new file mode 100644 index 00000000000..1de0ba58489 --- /dev/null +++ b/lib/public/Migration/Attributes/DropColumn.php @@ -0,0 +1,29 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\Migration\Attributes; + +use Attribute; + +/** + * attribute on column drop + * + * @since 30.0.0 + */ +#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_CLASS)] +class DropColumn extends ColumnMigrationAttribute { + /** + * @return string + * @since 30.0.0 + */ + public function definition(): string { + return empty($this->getName()) ? + 'Deletion of a column from table \'' . $this->getTable() . '\'' + : 'Deletion of column \'' . $this->getName() . '\' from table \'' . $this->getTable() . '\''; + } +} diff --git a/lib/public/Migration/Attributes/DropIndex.php b/lib/public/Migration/Attributes/DropIndex.php new file mode 100644 index 00000000000..2702cbed9a7 --- /dev/null +++ b/lib/public/Migration/Attributes/DropIndex.php @@ -0,0 +1,27 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\Migration\Attributes; + +use Attribute; + +/** + * attribute on index drop + * + * @since 30.0.0 + */ +#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_CLASS)] +class DropIndex extends IndexMigrationAttribute { + /** + * @return string + * @since 30.0.0 + */ + public function definition(): string { + return 'Deletion of an index from table \'' . $this->getTable() . '\''; + } +} diff --git a/lib/public/Migration/Attributes/DropTable.php b/lib/public/Migration/Attributes/DropTable.php new file mode 100644 index 00000000000..e90e4804a3c --- /dev/null +++ b/lib/public/Migration/Attributes/DropTable.php @@ -0,0 +1,27 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\Migration\Attributes; + +use Attribute; + +/** + * attribute on table drop + * + * @since 30.0.0 + */ +#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_CLASS)] +class DropTable extends TableMigrationAttribute { + /** + * @return string + * @since 30.0.0 + */ + public function definition(): string { + return 'Deletion of table \'' . $this->getTable() . '\''; + } +} diff --git a/lib/public/Migration/Attributes/GenericMigrationAttribute.php b/lib/public/Migration/Attributes/GenericMigrationAttribute.php new file mode 100644 index 00000000000..62d3bbf84d1 --- /dev/null +++ b/lib/public/Migration/Attributes/GenericMigrationAttribute.php @@ -0,0 +1,49 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\Migration\Attributes; + +use JsonSerializable; + +/** + * generic entry, used to replace migration attribute not yet known in current version + * but used in a future release + * + * @since 30.0.0 + */ +class GenericMigrationAttribute extends MigrationAttribute implements JsonSerializable { + /** + * @param array $details + * @since 30.0.0 + */ + public function __construct( + private array $details = [] + ) { + parent::__construct( + $details['table'] ?? '', + $details['description'] ?? '', + $details['notes'] ?? [] + ); + } + + /** + * @return string + * @since 30.0.0 + */ + public function definition(): string { + return json_encode($this->jsonSerialize(), JSON_UNESCAPED_SLASHES); + } + + /** + * @return array + * @since 30.0.0 + */ + public function jsonSerialize(): array { + return $this->details; + } +} diff --git a/lib/public/Migration/Attributes/IndexMigrationAttribute.php b/lib/public/Migration/Attributes/IndexMigrationAttribute.php new file mode 100644 index 00000000000..14f800466ea --- /dev/null +++ b/lib/public/Migration/Attributes/IndexMigrationAttribute.php @@ -0,0 +1,32 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\Migration\Attributes; + +use JsonSerializable; + +/** + * generic class related to migration attribute about index changes + * + * @since 30.0.0 + */ +class IndexMigrationAttribute extends MigrationAttribute implements JsonSerializable { + /** + * @param string $table name of the database table + * @param string $description description of the migration + * @param array $notes notes abour the migration/index + * @since 30.0.0 + */ + public function __construct( + string $table, + string $description = '', + array $notes = [], + ) { + parent::__construct($table, $description, $notes); + } +} diff --git a/lib/public/Migration/Attributes/MigrationAttribute.php b/lib/public/Migration/Attributes/MigrationAttribute.php new file mode 100644 index 00000000000..5b4550c4db5 --- /dev/null +++ b/lib/public/Migration/Attributes/MigrationAttribute.php @@ -0,0 +1,118 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\Migration\Attributes; + +use JsonSerializable; + +/** + * @since 30.0.0 + */ +class MigrationAttribute implements JsonSerializable { + /** + * @param string $table name of the database table + * @param string $description description of the migration + * @param array $notes notes about the migration + * @since 30.0.0 + */ + public function __construct( + private string $table, + private string $description = '', + private array $notes = [], + ) { + } + + /** + * @param string $table + * + * @return $this + * @since 30.0.0 + */ + public function setTable(string $table): self { + $this->table = $table; + return $this; + } + + /** + * @return string + * @since 30.0.0 + */ + public function getTable(): string { + return $this->table; + } + + /** + * @param string $description + * + * @return $this + * @since 30.0.0 + */ + public function setDescription(string $description): self { + $this->description = $description; + return $this; + } + + /** + * @return string + * @since 30.0.0 + */ + public function getDescription(): string { + return $this->description; + } + + /** + * @param array $notes + * + * @return $this + * @since 30.0.0 + */ + public function setNotes(array $notes): self { + $this->notes = $notes; + return $this; + } + + /** + * @return array + * @since 30.0.0 + */ + public function getNotes(): array { + return $this->notes; + } + + /** + * @return string + * @since 30.0.0 + */ + public function definition(): string { + return json_encode($this->jsonSerialize(), JSON_UNESCAPED_SLASHES); + } + + /** + * @param array $data + * + * @return self + * @since 30.0.0 + */ + public function import(array $data): self { + return $this->setDescription($data['description'] ?? '') + ->setNotes($data['notes'] ?? []); + } + + /** + * @return array + * @since 30.0.0 + */ + public function jsonSerialize(): array { + return [ + 'class' => get_class($this), + 'table' => $this->getTable(), + 'description' => $this->getDescription(), + 'notes' => $this->getNotes() + ]; + } +} diff --git a/lib/public/Migration/Attributes/ModifyColumn.php b/lib/public/Migration/Attributes/ModifyColumn.php new file mode 100644 index 00000000000..d90d0db4ec4 --- /dev/null +++ b/lib/public/Migration/Attributes/ModifyColumn.php @@ -0,0 +1,29 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\Migration\Attributes; + +use Attribute; + +/** + * attribute on column modification + * + * @since 30.0.0 + */ +#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_CLASS)] +class ModifyColumn extends ColumnMigrationAttribute { + /** + * @return string + * @since 30.0.0 + */ + public function definition(): string { + return empty($this->getName()) ? + 'Modification of a column from table \'' . $this->getTable() . '\'' + : 'Modification of column \'' . $this->getName() . '\' from table \'' . $this->getTable() . '\''; + } +} diff --git a/lib/public/Migration/Attributes/TableMigrationAttribute.php b/lib/public/Migration/Attributes/TableMigrationAttribute.php new file mode 100644 index 00000000000..0776e50387e --- /dev/null +++ b/lib/public/Migration/Attributes/TableMigrationAttribute.php @@ -0,0 +1,78 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\Migration\Attributes; + +use JsonSerializable; + +/** + * generic class related to migration attribute about table changes + * + * @since 30.0.0 + */ +class TableMigrationAttribute extends MigrationAttribute implements JsonSerializable { + /** + * @param string $table name of the database table + * @param array $columns list of columns + * @param string $description description of the migration + * @param array $notes notes about the migration/table + * @since 30.0.0 + */ + public function __construct( + string $table, + private array $columns = [], + string $description = '', + array $notes = [], + ) { + parent::__construct($table, $description, $notes); + } + + /** + * @param array $columns + * + * @return $this + * @since 30.0.0 + */ + public function setColumns(array $columns): self { + $this->columns = $columns; + return $this; + } + + /** + * @return array + * @since 30.0.0 + */ + public function getColumns(): array { + return $this->columns; + } + + /** + * @param array $data + * + * @return $this + * @since 30.0.0 + */ + public function import(array $data): self { + parent::import($data); + $this->setColumns($data['columns'] ?? []); + return $this; + } + + /** + * @return array + * @since 30.0.0 + */ + public function jsonSerialize(): array { + return array_merge( + parent::jsonSerialize(), + [ + 'columns' => $this->getColumns(), + ] + ); + } +} diff --git a/tests/lib/Updater/ReleaseMetadataTest.php b/tests/lib/Updater/ReleaseMetadataTest.php new file mode 100644 index 00000000000..25fe3949152 --- /dev/null +++ b/tests/lib/Updater/ReleaseMetadataTest.php @@ -0,0 +1,209 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace Test\Updater; + +use OC\Updater\ReleaseMetadata; +use OCP\Http\Client\IClient; +use OCP\Http\Client\IClientService; +use OCP\Http\Client\IResponse; +use PHPUnit\Framework\MockObject\MockObject; + +class ReleaseMetadataTest extends \Test\TestCase { + private IClientService| MockObject $clientService; + + protected function setUp(): void { + parent::setUp(); + $this->clientService = $this->getMockBuilder(IClientService::class) + ->disableOriginalConstructor() + ->getMock(); + } + + public function testDownloadMetadata() { + $client = $this->createMock(IClient::class); + $response = $this->createMock(IResponse::class); + $this->clientService->expects($this->once()) + ->method('newClient') + ->with() + ->willReturn($client); + $client->expects($this->once()) + ->method('get') + ->willReturn($response); + $response->expects($this->once()) + ->method('getBody') + ->with() + ->willReturn($this->resultRequest()); + + + $releaseMetadata = new ReleaseMetadata($this->clientService); + $this->assertSame($this->resultRequestArray(), $releaseMetadata->downloadMetadata('ouila')); + } + + /** + * @dataProvider getMetadataUrlProvider + * + * @param string $version + * @param string $url + */ + public function testGetMetadata(string $version, string $url) { + $client = $this->createMock(IClient::class); + $response = $this->createMock(IResponse::class); + $this->clientService->expects($this->once()) + ->method('newClient') + ->with() + ->willReturn($client); + $client->expects($this->once()) + ->method('get') + ->with($url) + ->willReturn($response); + + $response->expects($this->once()) + ->method('getBody') + ->with() + ->willReturn('{}'); + + $releaseMetadata = new ReleaseMetadata($this->clientService); + $releaseMetadata->getMetadata($version); + } + + /** + * @return array + */ + public function getMetadataUrlProvider(): array { + return [ + [ + '30.0.0', + 'https://download.nextcloud.com/server/releases/nextcloud-30.0.0.metadata' + ], + [ + '30.0.0-beta1', + 'https://download.nextcloud.com/server/prereleases/nextcloud-30.0.0-beta1.metadata' + ], + [ + '30', + 'https://download.nextcloud.com/server/releases/latest-30.metadata' + ] + ]; + } + + private function resultRequest(): string { + return json_encode($this->resultRequestArray()); + } + + private function resultRequestArray(): array { + return [ + 'migrations' => [ + 'core' => [], + 'apps' => [ + 'testing' => [ + '30000Date20240102030405' => [ + 'class' => 'OCP\\Migration\\Attributes\\DropTable', + 'table' => 'old_table', + 'description' => '', + 'notes' => [], + 'columns' => [] + ], + [ + 'class' => 'OCP\\Migration\\Attributes\\CreateTable', + 'table' => 'new_table', + 'description' => 'Table is used to store things, but also to get more things', + 'notes' => [ + 'this is a notice', + 'and another one, if really needed' + ], + 'columns' => [] + ], + [ + 'class' => 'OCP\\Migration\\Attributes\\AddColumn', + 'table' => 'my_table', + 'description' => '', + 'notes' => [], + 'name' => '', + 'type' => '' + ], + [ + 'class' => 'OCP\\Migration\\Attributes\\AddColumn', + 'table' => 'my_table', + 'description' => '', + 'notes' => [], + 'name' => 'another_field', + 'type' => '' + ], + [ + 'class' => 'OCP\\Migration\\Attributes\\AddColumn', + 'table' => 'other_table', + 'description' => '', + 'notes' => [], + 'name' => 'last_one', + 'type' => 'date' + ], + [ + 'class' => 'OCP\\Migration\\Attributes\\AddIndex', + 'table' => 'my_table', + 'description' => '', + 'notes' => [], + 'type' => '' + ], + [ + 'class' => 'OCP\\Migration\\Attributes\\AddIndex', + 'table' => 'my_table', + 'description' => '', + 'notes' => [], + 'type' => 'primary' + ], + [ + 'class' => 'OCP\\Migration\\Attributes\\DropColumn', + 'table' => 'other_table', + 'description' => '', + 'notes' => [], + 'name' => '', + 'type' => '' + ], + [ + 'class' => 'OCP\\Migration\\Attributes\\DropColumn', + 'table' => 'other_table', + 'description' => 'field is not used anymore and replaced by \'last_one\'', + 'notes' => [], + 'name' => 'old_column', + 'type' => '' + ], + [ + 'class' => 'OCP\\Migration\\Attributes\\DropIndex', + 'table' => 'other_table', + 'description' => '', + 'notes' => [], + 'type' => '' + ], + [ + 'class' => 'OCP\\Migration\\Attributes\\ModifyColumn', + 'table' => 'other_table', + 'description' => '', + 'notes' => [], + 'name' => '', + 'type' => '' + ], + [ + 'class' => 'OCP\\Migration\\Attributes\\ModifyColumn', + 'table' => 'other_table', + 'description' => '', + 'notes' => [], + 'name' => 'this_field', + 'type' => '' + ], + [ + 'class' => 'OCP\\Migration\\Attributes\\ModifyColumn', + 'table' => 'other_table', + 'description' => '', + 'notes' => [], + 'name' => 'this_field', + 'type' => 'bigint' + ] + ] + ] + ] + ]; + } +} |