-
-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Commit
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>
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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() { | ||
Check notice Code scanning / Psalm MissingReturnType Note
Method OC\Core\Command\Db\Migrations\GenerateMetadataCommand::configure does not have a return type, expecting void
|
||
$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; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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() { | ||
Check notice Code scanning / Psalm MissingReturnType Note
Method OC\Core\Command\Db\Migrations\PreviewCommand::configure does not have a return type, expecting 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'); | ||
|
||
$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] ?? []); | ||
Check failure on line 111 in core/Command/Db/Migrations/PreviewCommand.php
|
||
} | ||
|
||
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) | ||
Check notice Code scanning / Psalm RedundantConditionGivenDocblockType Note
Operand of type true is always truthy
|
||
|| !ctype_alpha(substr($class, strlen($namespace)))) { | ||
throw new AttributeException('class name does not looks valid'); | ||
} | ||
|
||
try { | ||
$attribute = new $class(); | ||
Check notice Code scanning / Psalm InvalidStringClass Note
String cannot be used as a class
|
||
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,22 @@ | ||
<?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(Attribute::IS_REPEATABLE | Attribute::TARGET_CLASS)] | ||
Check failure on line 13 in lib/public/Migration/Attributes/AddColumn.php
|
||
class AddColumn extends ColumnMigrationAttribute { | ||
public function definition(): string { | ||
Check failure on line 15 in lib/public/Migration/Attributes/AddColumn.php
|
||
$type = is_null($this->getType()) ? '' : ' (' . $this->getType()->value . ')'; | ||
$table = empty($this->getTable()) ? '' : ' to table \'' . $this->getTable() . '\''; | ||
return empty($this->getName()) ? | ||
'Addition of a new column' . $type . $table | ||
: 'Addition of column \'' . $this->getName() . '\'' . $type . $table; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
<?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(Attribute::IS_REPEATABLE | Attribute::TARGET_CLASS)] | ||
Check failure on line 13 in lib/public/Migration/Attributes/AddIndex.php
|
||
class AddIndex extends IndexMigrationAttribute { | ||
public function definition(): string { | ||
Check failure on line 15 in lib/public/Migration/Attributes/AddIndex.php
|
||
$type = is_null($this->getType()) ? '' : ' (' . $this->getType()->value . ')'; | ||
$table = empty($this->getTable()) ? '' : ' to table \'' . $this->getTable() . '\''; | ||
return 'Addition of a new index' . $type . $table; | ||
} | ||
} |