Skip to content

Commit

Permalink
feat(updater): release metadata
Browse files Browse the repository at this point in the history
Signed-off-by: Maxence Lange <maxence@artificial-owl.com>
  • Loading branch information
ArtificialOwl committed Jul 18, 2024
1 parent 2b8e50a commit adb01b2
Show file tree
Hide file tree
Showing 5 changed files with 245 additions and 121 deletions.
37 changes: 7 additions & 30 deletions core/Command/Db/Migrations/GenerateMetadataCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,15 @@
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();
Expand All @@ -45,15 +44,17 @@ 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()
];
}

private function extractMigrationMetadataFromCore(): array {
return $this->extractMigrationAttributes('core');
return $this->metadataManager->extractMigrationAttributes('core');
}

/**
Expand All @@ -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;
}
}
119 changes: 28 additions & 91 deletions core/Command/Db/Migrations/PreviewCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -24,9 +21,10 @@
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();
}
Expand All @@ -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(
[
Expand All @@ -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 = '<info>' . $attribute->definition() . "</info>";
$definition .= empty($attribute->getDescription()) ? '' : "\n " . $attribute->getDescription();
Expand All @@ -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');
}
}
}
126 changes: 126 additions & 0 deletions lib/private/Migration/MetadataManager.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
<?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 OCP\App\IAppManager;
use OCP\Migration\Attributes\GenericMigrationAttribute;
use OCP\Migration\Attributes\MigrationAttribute;
use OCP\Migration\Exceptions\AttributeException;
use Psr\Log\LoggerInterface;
use ReflectionClass;

class MetadataManager {
public function __construct(
private readonly IAppManager $appManager,
private readonly Connection $connection,
private readonly LoggerInterface $logger,
) {
}

/**
* We get all migrations from an app, and for each migration we extract attributes
*
* @param string $appId
*
* @return array
* @throws \Exception
*/
public 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;
}

/**
* @param string $version
* @param bool $filterKnownMigrations
*
* @return array
*/
public function getMigrationsAttributesFromReleaseMetadata(
array $metadata,
bool $filterKnownMigrations = false
): array {
$appsAttributes = [];
foreach (array_keys($metadata['apps']) as $appId) {
if ($filterKnownMigrations && !$this->appManager->isInstalled($appId)) {
continue;
}
$done = ($filterKnownMigrations) ? $this->getKnownMigrations($appId) : [];
$appsAttributes[$appId] = $this->parseMigrations($metadata['apps'][$appId] ?? [], $done);

Check failure on line 68 in lib/private/Migration/MetadataManager.php

View workflow job for this annotation

GitHub Actions / static-code-analysis

InvalidArrayOffset

lib/private/Migration/MetadataManager.php:68:4: InvalidArrayOffset: Cannot access value on variable $appsAttributes[$appId] using a key-of<array<array-key, mixed>> offset, expecting array-key (see https://psalm.dev/115)

Check failure

Code scanning / Psalm

InvalidArrayOffset Error

Cannot access value on variable $appsAttributes[$appId] using a key-of<array<array-key, mixed>> offset, expecting array-key
}

$done = ($filterKnownMigrations) ? $this->getKnownMigrations('core') : [];
return [
'core' => $this->parseMigrations($metadata['core'] ?? [], $done),
'apps' => $appsAttributes
];
}

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;
}

private function getKnownMigrations(string $appId): array {
$ms = new MigrationService($appId, $this->connection);
return $ms->getMigratedVersions();
}


/**
* @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');
}
}
}
17 changes: 17 additions & 0 deletions lib/private/Updater/Exceptions/ReleaseMetadataException.php
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 {
}
Loading

0 comments on commit adb01b2

Please sign in to comment.