-
-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Commit
Signed-off-by: Maxence Lange <[email protected]>
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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)]); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 { | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
<?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; | ||
use ReflectionClass; | ||
|
||
/** | ||
* Helps managing DB Migrations' Metadata | ||
* | ||
* @since 30.0.0 | ||
*/ | ||
class MetadataManager { | ||
public function __construct( | ||
private readonly IAppManager $appManager, | ||
Check failure Code scanning / Psalm InvalidDocblock Error
Param1 of OC\Migration\MetadataManager::__construct has invalid syntax
Check failure Code scanning / Psalm ParseError Error
Syntax error, unexpected T_STRING, expecting T_VARIABLE on line 27
Check failure on line 27 in lib/private/Migration/MetadataManager.php
|
||
private readonly Connection $connection, | ||
private readonly LoggerInterface $logger, | ||
) { | ||
} | ||
|
||
/** | ||
* We get all migrations from an app (or 'core'), and | ||
* for each migration files we extract its attributes | ||
* | ||
* @param string $appId | ||
* | ||
* @return array<string, MigrationAttribute[]> | ||
* @since 30.0.0 | ||
*/ | ||
public function extractMigrationAttributes(string $appId): array { | ||
$ms = new MigrationService($appId, $this->connection); | ||
Check failure Code scanning / Psalm UndefinedThisPropertyFetch Error
Instance property OC\Migration\MetadataManager::$connection is not defined
Check failure on line 43 in lib/private/Migration/MetadataManager.php
|
||
|
||
$metadata = []; | ||
foreach($ms->getAvailableVersions() as $version) { | ||
$metadata[$version] = []; | ||
$class = new ReflectionClass($ms->createInstance($version)); | ||
Check failure Code scanning / Psalm InaccessibleMethod Error
Cannot access protected method OC\DB\MigrationService::createInstance from context OC\Migration\MetadataManager
Check failure on line 48 in lib/private/Migration/MetadataManager.php
|
||
$attributes = $class->getAttributes(); | ||
foreach ($attributes as $attribute) { | ||
$item = $attribute->newInstance(); | ||
if ($item instanceof MigrationAttribute) { | ||
$metadata[$version][] = $item; | ||
} | ||
} | ||
} | ||
|
||
return $metadata; | ||
} | ||
|
||
/** | ||
* 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)) { | ||
Check failure Code scanning / Psalm UndefinedThisPropertyFetch Error
Instance property OC\Migration\MetadataManager::$appManager is not defined
Check failure on line 76 in lib/private/Migration/MetadataManager.php
|
||
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)]); | ||
Check failure Code scanning / Psalm UndefinedThisPropertyFetch Error
Instance property OC\Migration\MetadataManager::$logger is not defined
Check failure on line 111 in lib/private/Migration/MetadataManager.php
|
||
$parsed[$entry][] = new GenericMigrationAttribute($item); | ||
Check failure Code scanning / Psalm TooManyArguments Error
Too many arguments for OCP\Migration\Attributes\GenericMigrationAttribute::__construct - expecting 0 but saw 1
Check failure on line 112 in lib/private/Migration/MetadataManager.php
|
||
} | ||
} | ||
} | ||
|
||
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); | ||
Check failure Code scanning / Psalm UndefinedThisPropertyFetch Error
Instance property OC\Migration\MetadataManager::$connection is not defined
Check failure on line 129 in lib/private/Migration/MetadataManager.php
|
||
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'); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 { | ||
} |